Files
benilla-octo/crates/benilla-ui/src/script/worldstate.rs
T
SamandClaude Fable 5 1c5db5c2c1 landmarks and the Plaguelands towers on the map, no ultrawide crop
- the world map's landmarks: city icons and the Eastern Plaguelands towers, from AreaPOI.dbc through the reference's own three gates (level flags, exploration, world state) — the tower rows are one per (tower, owner, state), so the server flipping a world state swaps which icon exists; and the always-up PvP readout ("Towers Controlled: n") over a new WorldStateUI.dbc loader, the init clearing the table on a zone change as the client does
- the character-select scene holds its authored 4:3 view box: the diagonal-FOV relation was fed the display's aspect, so every extra pixel of width came out of height (1.55× zoom at 21:9, head and feet gone) — now width grows and height never shrinks; the reference does the same and is wrong on the same panel
- a trail particle is stored in world space and drawn raw: Sprint's trail was welded to the chest bone (turn and it swings, run and it wobbles) because the trail was faked by subtracting translation inside a bone-local store; the correction machinery deleted itself, gravity is plain world down, and the emitters' live attach frame is retired
- the spellbook can shrink: `SMSG_REMOVED_SPELL` had no handler, so a talent wipe took the points back and left every rank drawn; and the chat dock's hover driver rides a frame the dock cannot hide, so selecting the Combat Log no longer freezes the tab alphas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 18:43:04 +07:00

145 lines
7.0 KiB
Rust

//! The world-state UI seam — `GetNumWorldStateUI` / `GetWorldStateUIInfo`, the two bindings behind
//! the always-up PvP readout (`WorldStateFrame`; decision 1586).
//!
//! The list is built app-side ([`crate::world_state_ui`](benilla_app)) because every one of its
//! gates reads app state — the last init's `(map, area)` scope, the joined chat channels, the live
//! world-state table — and pushed here as already-resolved rows. That is the same split every
//! other feed uses; what the reference keeps in a process global (`0xb71e7c`, an array of DBC row
//! ids re-walked on demand) we keep as the resolved answer.
//!
//! **Return shape VERIFIED** at `0x4c5a70` (wow-re `system/ui/scratch/worldstate-ui-law.md`).
//! Worth stating what it is *not*: this is a **ten**-value return, and it carries **no `uiType` and
//! no `hidden`** — those are a later expansion of the API, and a client that answered twelve values
//! here would hand `WorldStateFrame` its icon path where it expects a number.
use mlua::{Lua, MultiValue, Value};
use super::Model;
/// One row of the always-up world-state readout — the ten values `GetWorldStateUIInfo` returns,
/// in its order.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WorldStateUiView {
/// `uiState` — the row's `StateVariable` world state resolved through the table, or **`1`**
/// when it carries none (`0x4c5ad8`: the miss is a literal `1.0`, not `0`, so a row without a
/// state reads as "on" rather than "off").
pub ui_state: i32,
/// The label, already macro-expanded (`"Towers Controlled: 3"`). The **only** expanded string
/// of the ten.
pub text: String,
/// The static icon's texture path, or `""`.
pub icon: String,
/// The icon that replaces it while the row's state is live (Warsong Gulch's enemy flag), or
/// `""`.
pub dynamic_icon: String,
pub tooltip: String,
pub dynamic_tooltip: String,
/// A token naming an extra widget the row drives — `"CAPTUREPOINT"` on the one row that has
/// one — or `""`.
pub extended_ui: String,
/// That widget's three world states, **resolved to values** (the DBC holds ids; the binding
/// answers what they read).
pub extended_ui_state: [i32; 3],
}
#[derive(Default)]
pub(crate) struct WorldStateUiState {
pub(crate) rows: Vec<WorldStateUiView>,
}
impl super::UiScript {
/// Push the always-up world-state readout's rows (see
/// [`WorldStateUiView`](WorldStateUiView)). On change, queues
/// **`UPDATE_WORLD_STATES`** — event `0x20e`, the reference's own name for it.
///
/// **Named divergence, in the *trigger*, not the shape.** The reference fires that event from
/// exactly two sites (`0x48fa12`, after an init rebuilds the list; `0x49bdd6`, when the
/// zone-defense channel membership flips) and, verified by a whole-`.text` scan for the id,
/// nowhere else — so a plain `SMSG_UPDATE_WORLD_STATE` changing a tower count fires nothing.
/// It can afford that because its Lua re-reads live values off the table at whatever moment it
/// repaints; we push resolved values, so a silent value change would simply never reach the
/// frame. We therefore fire on any change to what the readout would *display*, which is the
/// reference's two triggers plus the value updates its own frame must already be reacting to
/// by some means the binary does not show.
pub fn set_world_state_ui(&mut self, rows: Vec<WorldStateUiView>) {
let mut model = self.model_mut();
if model.worldstate.rows != rows {
model.worldstate.rows = rows;
model
.pending_events
.push(("UPDATE_WORLD_STATES".to_string(), Vec::new()));
}
}
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// GetNumWorldStateUI() — how many always-up rows the current scope admits (`0x4c5a40`: pushes
// the count, no validation, always one return).
g.set(
"GetNumWorldStateUI",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model.worldstate.rows.len() as i64)
})?,
)?;
// GetWorldStateUIInfo(index) — the i-th (1-based) row, ten values (`0x4c5a70`).
//
// Two legs of the reference's behaviour are easy to get wrong and are reproduced here:
//
// - a **non-number** argument RAISES `"Usage: GetWorldStateUIInfo(index)"`. The binding's
// `luaL_error` leg looks like it returns — its callee has a `ret` — but that `ret` is an
// MSVC epilogue, and closing the range two levels down (`luaG_errormsg`, then `luaD_throw`,
// neither of which contains a single `ret`) proves the longjmp. So this is an error, not a
// quiet nil.
// - an **out-of-range** index returns exactly ONE value, the number `0` — not nil, and not a
// run of ten nils (`0x4c5be5`). The index falls through a row-id substitution of `0`, whose
// id-index slot is NULL, and lands on that shared bail.
//
// No string return is ever nil: an empty DBC column resolves to the string block's leading
// NUL, so Lua gets `""`. The `__ftol` on the index truncates toward zero.
g.set(
"GetWorldStateUIInfo",
lua.create_function(|lua, index: Value| {
let index = match index {
Value::Integer(i) => i as f64,
Value::Number(n) => n,
// The reference accepts a numeric STRING too — `lua_isnumber` coerces.
Value::String(ref s) => match s.to_str().ok().and_then(|s| s.trim().parse().ok()) {
Some(n) => n,
None => return Err(mlua::Error::runtime("Usage: GetWorldStateUIInfo(index)")),
},
_ => return Err(mlua::Error::runtime("Usage: GetWorldStateUIInfo(index)")),
};
let row = {
let model = lua.app_data_ref::<Model>().expect("model app_data");
// Truncate toward zero, then 1-based.
usize::try_from(index.trunc() as i64)
.ok()
.and_then(|i| i.checked_sub(1))
.and_then(|i| model.worldstate.rows.get(i).cloned())
};
let Some(r) = row else {
return Ok(MultiValue::from_vec(vec![Value::Integer(0)]));
};
let s = |v: &str| -> mlua::Result<Value> { Ok(Value::String(lua.create_string(v)?)) };
Ok(MultiValue::from_vec(vec![
Value::Integer(i64::from(r.ui_state)),
s(&r.text)?,
s(&r.icon)?,
s(&r.dynamic_icon)?,
s(&r.tooltip)?,
s(&r.dynamic_tooltip)?,
s(&r.extended_ui)?,
Value::Integer(i64::from(r.extended_ui_state[0])),
Value::Integer(i64::from(r.extended_ui_state[1])),
Value::Integer(i64::from(r.extended_ui_state[2])),
]))
})?,
)?;
Ok(())
}