The cursor now runs on the client's two primitives. `ClearCursor(a,b)` (`0x495190`) cancels an armed gift wrap, then runs the payload's arm (an item's unlock fires `ITEM_LOCK_CHANGED`; coins fire `PLAYER_MONEY`), then the grid hide and `CURSOR_UPDATE`. Each setter runs that clear first, then its own tail (`0x494b60`, `0x4950f0`, `0x494d20`, `0x494f80`, `0x494e20`, `0x495010`). Every pickup, place, split, sale, trade, mail and auction path goes through them. So a `CURSOR_UPDATE` handler reads the emptied cursor, and `PickupSpell`/`PickupMacro`/`PickupPetAction`/`SplitContainerItem` clear what is held instead of refusing. An item attached to mail or an auction stays locked until it leaves the slot (`0x4adef0`). Attaching to an occupied slot puts the old item back on the cursor. Event-order tests cover each setter, the coin clear, and the attach lock and swap. Fixes #1011 Fixes #1012 Fixes #1006 Fixes #1007 Part of #156 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2448 lines
120 KiB
Rust
2448 lines
120 KiB
Rust
use std::collections::{HashMap, HashSet};
|
||
|
||
use crate::layout::{LayoutInput, LayoutSolver, Rect};
|
||
use crate::widget::{FrameHandle, RegionHandle, WidgetArena};
|
||
|
||
use super::{
|
||
auction, backdrop, bank, bind_confirm, camera_view, char_stats, chat_window, colorselect,
|
||
container, craft, cursor, death, duel, follow, gossip, guild, inspect, item_text, loot,
|
||
loot_roll, macros, mail, merchant, parse_unit_token, party, petition, pvp, quest, quest_log,
|
||
reputation, session, simplehtml, skills, slider, social, spellbook, stable, taxi, trade,
|
||
tradeskill, trainer, weapon_enchant, ActionSlot, AuraState, FontObject, ItemTemplateView,
|
||
MusicRequest, PlayerReqState, RegionData, ScriptValue, SoundRequest, UnitState, UnitTokenParse,
|
||
};
|
||
|
||
// ── The Rust-side model ──
|
||
|
||
/// The host's answer to whether a texture path resolves to a file.
|
||
pub type TextureProbe = Box<dyn Fn(&str) -> bool>;
|
||
|
||
/// The host's world-to-map projection (`0x4a7360`): the displayed map's selection as
|
||
/// [`super::UiScript::world_map_selection`] reads it, a map id and a world `(x, y)`, to the map UV,
|
||
/// `None` where the reference's outputs stay at their `(0, 0)`.
|
||
pub type WorldLocProjector =
|
||
Box<dyn Fn((u32, u32, Option<u32>), u32, f32, f32) -> Option<(f32, f32)>>;
|
||
|
||
/// The host's answer to a texture path's size in texels.
|
||
pub type TextureSizeProbe = Box<dyn Fn(&str) -> Option<(u32, u32)>>;
|
||
|
||
/// The host's answer to whether a font path loads.
|
||
pub type FontProbe = Box<dyn Fn(&str) -> bool>;
|
||
|
||
/// Minted id to handle, indexed directly: ids are dense and every scripted call looks one up.
|
||
#[derive(Debug, Clone)]
|
||
pub(crate) struct IdMap<T>(Vec<Option<T>>);
|
||
|
||
impl<T> Default for IdMap<T> {
|
||
fn default() -> Self {
|
||
Self(Vec::new())
|
||
}
|
||
}
|
||
|
||
impl<T: Copy> IdMap<T> {
|
||
pub(crate) fn get(&self, id: &u32) -> Option<&T> {
|
||
self.0.get(*id as usize).and_then(Option::as_ref)
|
||
}
|
||
pub(crate) fn contains_key(&self, id: &u32) -> bool {
|
||
self.get(id).is_some()
|
||
}
|
||
pub(crate) fn insert(&mut self, id: u32, v: T) -> Option<T> {
|
||
let i = id as usize;
|
||
if self.0.len() <= i {
|
||
self.0.resize(i + 1, None);
|
||
}
|
||
self.0[i].replace(v)
|
||
}
|
||
pub(crate) fn remove(&mut self, id: &u32) -> Option<T> {
|
||
self.0.get_mut(*id as usize).and_then(Option::take)
|
||
}
|
||
}
|
||
|
||
/// [`Model::layout_hold`]: the node is in [`Model::layout_pending`].
|
||
pub(crate) const HOLD_QUEUED: u8 = 1;
|
||
/// The node is in [`Model::layout_force`].
|
||
pub(crate) const HOLD_QUEUED_FORCE: u8 = 2;
|
||
/// The node is pending under its deferral bit, its resolve waiting.
|
||
pub(crate) const HOLD_HELD: u8 = 4;
|
||
/// The held node's resolved bit was cleared, so its resolve stores without the keep test.
|
||
pub(crate) const HOLD_FORCE: u8 = 8;
|
||
|
||
/// Whether layout node `id`'s deferral bit is set, on its frame or region.
|
||
pub(crate) fn node_deferred(
|
||
arena: &WidgetArena,
|
||
id_to_frame: &IdMap<FrameHandle>,
|
||
id_to_region: &IdMap<RegionHandle>,
|
||
id: u32,
|
||
) -> bool {
|
||
match id_to_frame.get(&id) {
|
||
Some(&h) => arena.frame(h).is_some_and(|f| f.layout_deferred),
|
||
None => id_to_region
|
||
.get(&id)
|
||
.and_then(|&rh| arena.region(rh))
|
||
.is_some_and(|r| r.layout_deferred),
|
||
}
|
||
}
|
||
|
||
/// The Rust-side model behind the Lua VM, in `lua.app_data`. It holds no mlua handles, whose
|
||
/// persistent count the vendored `LUAI_MAXCSTACK` caps.
|
||
pub(crate) struct Model {
|
||
/// Which FrameScript state this VM is: it picks the event table and the frame types.
|
||
pub(crate) vm: super::glue::VmKind,
|
||
/// Every discovered addon in load order: the AddOn API's registry, filled at world entry.
|
||
pub(crate) addons: Vec<super::addon::AddOnInfo>,
|
||
/// The Lua index space into `addons`, a title-sorted, hidden-filtered list (`0x51da70`).
|
||
pub(crate) addon_index: Vec<usize>,
|
||
/// Lowercased names `SMSG_ADDON_INFO` marked `status = 2`; `None` until the reply arrives, and
|
||
/// until then `GetNumAddOns()` is 0, as in the reference (`[0xbe1b90]`, reset at `0x51fad1`).
|
||
pub(crate) addon_info_hidden: Option<Vec<String>>,
|
||
/// The current character's enable hash, which every enable verb writes through and the
|
||
/// shutdown writer (`0x490c88` into `0x51ef20`) saves only while dirty.
|
||
pub(crate) addon_enable: super::EnableHash,
|
||
/// The hash as the host read it from the file, what `ResetDisabledAddOns` reloads (`0x48e830`).
|
||
pub(crate) addon_enable_saved: super::EnableHash,
|
||
/// The AddOns folder, so `LoadAddOn` can read an addon's files from inside a Lua binding.
|
||
pub(crate) addons_root: Option<std::path::PathBuf>,
|
||
/// The host's reader for chain-sourced addon files; without one such an addon is `MISSING`.
|
||
pub(crate) addons_chain_reader: Option<super::addon::SharedChainReader>,
|
||
/// The host's font engine, so a metric answers inside the asking call, not a frame later.
|
||
pub(crate) measurer: Option<Box<dyn super::TextMeasure>>,
|
||
/// Whether a texture path resolves (patch chain or loose addon file), so the path form of
|
||
/// `SetTexture` returns the reference's 1 or nil inline (`0x79bb40`). `None` answers nil.
|
||
pub(crate) texture_probe: Option<TextureProbe>,
|
||
/// `GetWorldLocMapPosition`'s projection; `None` answers `(0, 0)`.
|
||
pub(crate) world_loc_projector: Option<WorldLocProjector>,
|
||
/// A path's texel size, which an axis authored as 0 takes, one texel per unit, as the client's
|
||
/// `GetWidth` (`0x770720`) and `GetHeight` (`0x770790`) do; `None` leaves it as authored.
|
||
pub(crate) texture_size_probe: Option<TextureSizeProbe>,
|
||
/// Whether a font path loads: `SetFont`'s 1 or nil, nil meaning a load failure (`0x79f345`,
|
||
/// `0x79f361`, from `0x5c1ae0` in the `0x44d040` cache). Unlike `texture_probe`, `None` answers
|
||
/// 1 for a non-empty path: without a font backend nothing failed, and addons revert their font
|
||
/// on a nil.
|
||
pub(crate) font_probe: Option<FontProbe>,
|
||
/// Saved-variables folders, one `<Addon>.lua` each: the account's, then this character's.
|
||
pub(crate) addons_saved_account: Option<std::path::PathBuf>,
|
||
pub(crate) addons_saved_character: Option<std::path::PathBuf>,
|
||
/// FrameXML templates, global across files as the client's (`0x6ee500`); held here so the
|
||
/// loader runs from a bare `&Lua`, which lets `LoadAddOn` load inside a binding.
|
||
pub(crate) framexml_templates: std::cell::RefCell<crate::framexml::Templates>,
|
||
/// The script-execution context word `[0xceeac0]`, as far as template registration reads it
|
||
/// (`0x6ee1ae`): `None` is its resting 0, under which the UI's own files load; an insecure
|
||
/// addon's load sets its name ([`crate::script::addon_load_context`]).
|
||
pub(crate) exec_context: std::cell::RefCell<Option<String>>,
|
||
/// `FrameXML_Debug`'s flag (`0x488440` over `[0xceea30]`, boots 0); the loader's trace lines
|
||
/// print while it is greater than 0 (`0x6ee298`), so it is signed.
|
||
pub(crate) framexml_debug: std::cell::Cell<i32>,
|
||
/// The loads' records for `Logs\FrameXML.log` and the drains the host has yet to write.
|
||
pub(crate) load_log: crate::status::LoadLog,
|
||
/// The FrameXML font registry, a namespace apart from templates (a font inherits only a font).
|
||
pub(crate) framexml_fonts:
|
||
std::cell::RefCell<std::collections::HashMap<String, crate::framexml::Element>>,
|
||
pub(crate) arena: WidgetArena,
|
||
/// Per-frame anchors, size and scale; every live frame has one from creation on.
|
||
pub(crate) layout_inputs: HashMap<FrameHandle, LayoutInput>,
|
||
/// Each resolvable frame's rect from the last resolve.
|
||
pub(crate) resolved: HashMap<FrameHandle, Rect>,
|
||
/// The anchor solver, kept across resolves so a steady round allocates nothing; scratch only.
|
||
pub(crate) solver: LayoutSolver,
|
||
/// Tier 2 of the resolve gate, the last converged inputs' fingerprint; `None` forces a resolve.
|
||
pub(crate) layout_fingerprint: Option<super::layout::InputFingerprint>,
|
||
/// Tier 1 of the resolve gate: every layout write bumps it, and a resolve returns at once while
|
||
/// it equals `layout_epoch_resolved`. Never bump it per frame: tier 2 hashes the whole UI.
|
||
pub(crate) layout_epoch: u64,
|
||
/// Layout ids named by writes since the last converged resolve: the resolve re-derives each
|
||
/// in the cached graph (born, freed, re-pointed or re-valued alike) and seeds its dirty
|
||
/// closure from them. `None` derives the graph in full; `WOW_LAYOUT_VERIFY` checks every
|
||
/// incremental pass's rects and graph against a fresh derivation.
|
||
pub(crate) layout_touched: Option<Vec<u32>>,
|
||
/// Regions a write may have re-keyed for text measure (text, font, outline, wrap, scale) since
|
||
/// the last sweep, which asks only these; `None` walks all. A site that forgets to name its
|
||
/// region is caught by `WOW_LAYOUT_VERIFY`, on for every test here, which panics on the miss.
|
||
pub(crate) measure_dirty: Option<Vec<crate::widget::RegionHandle>>,
|
||
/// Per message frame, `(lines_gen, env hash)` at its last request-free sweep; a match skips it.
|
||
/// A destroyed frame's entry is inert: generational handles never match again.
|
||
pub(crate) msg_swept: std::collections::HashMap<crate::widget::FrameHandle, (u64, u64)>,
|
||
/// Lines the message sweep hashed; a settled second sweep hashes none.
|
||
pub(crate) msg_lines_hashed: u64,
|
||
/// Layout ids whose next resolve stores the rect without the keep test
|
||
/// ([`crate::layout::keeps_rect`]), the reference's resolved bit cleared: a frame whose
|
||
/// effective scale moved (`0x768360`), whose regions follow it, and a FontString whose new
|
||
/// measure landed, benilla's moment for `SetText` and `SetFont` (`0x771ea4`, `0x7715e0`). Each
|
||
/// is named on the layout too, so the resolve that drains this list solves it.
|
||
pub(crate) layout_force: Vec<u32>,
|
||
/// Layout ids invalidated since the last resolve (`0x7680e0` on the node): a write the
|
||
/// reference queues a resolve for, `SetPoint`, `SetWidth` and the like, but not
|
||
/// `ClearAllPoints` (`0x767620`). The next resolve solves each one from its inputs, where a
|
||
/// node nothing invalidated keeps a state a recompute could change
|
||
/// ([`Model::invalidate_frame`]).
|
||
pub(crate) layout_pending: Vec<u32>,
|
||
/// Layout ids whose inputs a write moved without invalidating them since they were last
|
||
/// invalidated: a dying target's unhook (`0x768e20` → `0x767e70` with a zero flag), new art
|
||
/// (`SetTexture` `0x770200`), a re-link into another frame (`0x77fd10`). Each holds a state a
|
||
/// re-resolve need not give back, so a write the reference invalidates for is not skipped as
|
||
/// a no-op on it ([`Model::invalidate_frame`]).
|
||
pub(crate) layout_unsettled: HashSet<u32>,
|
||
/// Per layout id, what its resolve owes beside the deferral bit itself (which the frame and
|
||
/// region carry, [`crate::widget::Frame::layout_deferred`]): `HOLD_*` bits. A node in
|
||
/// [`Self::layout_pending`] or [`Self::layout_force`] is queued; a deferred node pending is
|
||
/// held, the reference's `0x4` under `0x2`, whose resolve waits until its show, a read or a
|
||
/// reader's anchor read.
|
||
pub(crate) layout_hold: Vec<u8>,
|
||
/// Layout ids invalidated while deferred since the last resolve: each passes the invalidate to
|
||
/// what anchors to it, as the reference does at the write (`0x768178`–`0x76819c`), which the
|
||
/// next resolve applies.
|
||
pub(crate) layout_pass: Vec<u32>,
|
||
/// Layout ids the next resolve takes as undeferred: a show or a load's end cleared the bit of
|
||
/// a pending one, which resolves at once (`0x7680c4` → `0x7680e0(1)`), or a getter reads a
|
||
/// pending one, resolving it without clearing the bit (`0x7a1a1d`).
|
||
pub(crate) layout_pull: Vec<u32>,
|
||
/// A show's resolve: the ids it resolves as it shows them going in, and the ids resolved for
|
||
/// them coming out, each pulled by an anchor read of one before it was, in the order their
|
||
/// `ApplyRect` runs, so the show fires their `OnSizeChanged` ([`super::visibility`]).
|
||
pub(crate) layout_chain: Option<Vec<u32>>,
|
||
/// The screen the last converged resolve laid out against: a frame anchored to it is reached
|
||
/// when it moves, as by any target's new rect.
|
||
pub(crate) layout_screen: Option<Rect>,
|
||
/// Under `WOW_LAYOUT_VERIFY`, the resolve just taken was incremental and owes a full re-run.
|
||
pub(crate) layout_verify_recheck: bool,
|
||
/// Per-node input hashes and dirty-closure scratch: which nodes a let-through resolve solves.
|
||
pub(crate) layout_scope: super::layout::LayoutScope,
|
||
/// The epoch the last converged resolve closed on; `None` sends the next one past tier 1.
|
||
pub(crate) layout_epoch_resolved: Option<u64>,
|
||
/// Resolves the gate let through, counted at its decision, so verify mode reads the same.
|
||
pub(crate) layout_solves: u64,
|
||
/// Resolves that got past tier 1, including those that then skipped: the whole-roster walk's
|
||
/// cost, which `layout_solves` misses. Verify mode walks anyway, so assert on this only from a
|
||
/// crate that depends on `benilla-ui`; inside this crate, assert on `layout_solves`.
|
||
pub(crate) layout_gate_walks: u64,
|
||
/// Full layout-graph derivations: 0 on a steady UI, births and frees included, so a rise means
|
||
/// a write named no node (`touch_layout`) or a re-derive gave up. The `WOW_LAYOUT_VERIFY`
|
||
/// re-run's derivation is not counted.
|
||
pub(crate) layout_derives: u64,
|
||
/// The last solve's width, `(frames solved, regions swept)`.
|
||
pub(crate) layout_last_scope: (usize, usize),
|
||
/// Fixpoint rounds run across all solves.
|
||
pub(crate) layout_rounds: u64,
|
||
/// Per text frame, its pool of hyperlink buttons (`[+0x314]`), the shown ones first
|
||
/// ([`super::hyperlink`]).
|
||
pub(crate) link_buttons: HashMap<FrameHandle, Vec<FrameHandle>>,
|
||
/// Region visuals (texture, colour, text) and layout (anchors, size, justify).
|
||
pub(crate) region_data: HashMap<RegionHandle, RegionData>,
|
||
/// Each frame's backdrop plate (`<Backdrop>` or `SetBackdrop`, the client's `frame+0x1ac`):
|
||
/// its spec and the nine Texture regions it built.
|
||
pub(crate) backdrops: HashMap<FrameHandle, backdrop::Installed>,
|
||
/// `CSimpleHTML`'s state: element fonts `+0x350`, link format `+0x360`, content nodes `+0x340`.
|
||
pub(crate) simple_html: simplehtml::SimpleHtmlStates,
|
||
/// Named `<Font>` objects by lowercased name, as 1.12 matches them with `SStrCmpI` (`0x783870`,
|
||
/// `0x7838c7`); read them only through `font_object`, where the fold lives.
|
||
pub(crate) font_objects_by_lower: HashMap<String, FontObject>,
|
||
/// Each region's rect while its valid bit is set: what its getters and a FontString's draw
|
||
/// read (`GetRect 0x768320`, `0x7730b7`). A cleared region keeps its rect until its own next
|
||
/// invalidate, as `ClearAllPoints` queues no resolve (`0x767620`).
|
||
pub(crate) region_resolved: HashMap<RegionHandle, Rect>,
|
||
/// A Texture whose resolve failed: the rect its last good resolve placed it at, which it keeps
|
||
/// drawing, as the failed resolve (`0x768d55`) leaves the quad `0x770670` wrote and its emit
|
||
/// checks only its texture (`0x7706e0`). Dropped when it resolves again.
|
||
pub(crate) region_kept: HashMap<RegionHandle, Rect>,
|
||
|
||
/// The next object id to mint; ids start at 1, since 0 is `SCREEN`.
|
||
pub(crate) next_id: u32,
|
||
pub(crate) id_to_frame: IdMap<FrameHandle>,
|
||
pub(crate) frame_to_id: HashMap<FrameHandle, u32>,
|
||
pub(crate) id_to_region: IdMap<RegionHandle>,
|
||
pub(crate) region_to_id: HashMap<RegionHandle, u32>,
|
||
/// Region name to the first region that took it, for name lookups only (`SetPoint` anchoring
|
||
/// to a sibling by name, after frames): first come, as the `_G` entry is (`0x701bd0`). A
|
||
/// region's own name, for `GetName` and error strings, is [`crate::widget::Region::name`].
|
||
pub(crate) region_by_name: HashMap<String, u32>,
|
||
|
||
/// Which handlers each frame has; the closures themselves live in Lua's `REG_SCRIPTS` table.
|
||
pub(crate) scripts: HashMap<FrameHandle, HashSet<&'static str>>,
|
||
/// Frames with an `OnUpdate` script, kept by `SetScript`, from which the update walk starts.
|
||
pub(crate) on_update_frames: Vec<FrameHandle>,
|
||
/// Bumped each time a frame joins [`Self::on_update_frames`], so the walk notices a frame that
|
||
/// gains an `OnUpdate` before its turn.
|
||
pub(crate) on_update_joins: u64,
|
||
/// Frames with an `OnSizeChanged` script, kept by `SetScript`, for the resolve's size snapshot.
|
||
pub(crate) on_size_changed_frames: Vec<FrameHandle>,
|
||
/// Frames with an `OnUpdateModel` script, fired as a visible model pane paints (`0x76d1a0`).
|
||
pub(crate) on_update_model_frames: Vec<FrameHandle>,
|
||
/// Per model file, a pane clock's sequences and bounds, read by the host off the `MD20`.
|
||
pub(crate) model_facts: HashMap<String, std::sync::Arc<crate::widget::ModelFileFacts>>,
|
||
/// Model files still waiting for their facts, for the host to load; deduplicated.
|
||
pub(crate) model_facts_wanted: Vec<String>,
|
||
/// Event name to its frames in registration order, never a set: `SignalEvent` (`0x703e50`)
|
||
/// dispatches in that order across frames. Re-registering keeps a frame's position.
|
||
pub(crate) event_to_frames: HashMap<String, Vec<FrameHandle>>,
|
||
pub(crate) frame_events: HashMap<FrameHandle, HashSet<String>>,
|
||
/// `RegisterAllEvents` frames in registration order, dispatched after an event's own list and
|
||
/// cleared only by `UnregisterAllEvents`. Deviation: a flag, not the reference's append to
|
||
/// every event's list (`0x7023e0`, which orders by registration time and lets `UnregisterEvent`
|
||
/// drop one event), because events register here by name and no list of every name exists.
|
||
pub(crate) all_event_frames: Vec<FrameHandle>,
|
||
|
||
/// The focused edit box (`0xcf4dc8`), taking all keys while visible; `None` admits `autoFocus`.
|
||
pub(crate) focused_editbox: Option<FrameHandle>,
|
||
/// ALT is held (`0x41f8f0(2)`), as the host reads it: an alt-arrow EditBox takes the arrows
|
||
/// only then.
|
||
pub(crate) alt_down: bool,
|
||
/// Per key name, the frame that consumed its last key-down: where its key-up and repeats go
|
||
/// (`[root+code*4+0x84]`, `0x765fa2`; [`super::keyboard`]).
|
||
pub(crate) key_consumers: HashMap<String, FrameHandle>,
|
||
|
||
/// The mouse-enabled frame under the cursor, for the next move's `OnLeave`/`OnEnter`.
|
||
pub(crate) mouseover: Option<FrameHandle>,
|
||
/// The hovered frame hid, or a mouse-enabled frame showed, so the tick re-runs the hover walk
|
||
/// at the saved position and a newly exposed frame gets `OnEnter` without a mouse move: the
|
||
/// reference's `[root+0x1100]` flag (set at `0x764cbb`/`0x764b8d`, pumped at `0x7657a1` into
|
||
/// `0x7660d0`).
|
||
pub(crate) hover_repick: bool,
|
||
/// The world mouseover as last published (`[0xb4e2c8]`, with the WorldFrame's own copy
|
||
/// `[wf+0x340]`): the pick's change check and the hover leaves read and write it
|
||
/// ([`super::mouseover`]).
|
||
pub(crate) world_mouseover: Option<u64>,
|
||
/// The blip whose tooltip the Minimap's probe put up, its stand-in for the hit set it keeps
|
||
/// (`0xbc81e4`, `0xbdcbac`), which a probe that misses the Minimap drops (`0x4eb12b`).
|
||
pub(crate) minimap_blip: Option<String>,
|
||
/// Per button held, the frame its press went to, the capture else the hover (`0x7662c0`), for
|
||
/// the release; `None` once that frame left the mouse index (hide or mouse disable emptied
|
||
/// the capture), so the release has no target but the button still counts as held.
|
||
pub(crate) mouse_down_on: HashMap<String, Option<FrameHandle>>,
|
||
/// The client's one mouse-capture slot (`root+0x80`), read by the mouse-down raise. Set at
|
||
/// `0x7663e6` to the capture, else the hovered frame, so a chorded press keeps the capture;
|
||
/// cleared at `0x7664bb` once no button is down. A title-region press captures nothing.
|
||
pub(crate) mouse_capture: Option<FrameHandle>,
|
||
/// Per frame, when its last single `OnClick` fired: the client's `[CButton+0x334]`, the whole
|
||
/// double-click state. It has no button identity (left then right makes a double), is zeroed
|
||
/// when a double fires, and nothing else clears it, not hide, disable or the cursor leaving.
|
||
pub(crate) last_click: HashMap<FrameHandle, f64>,
|
||
|
||
/// Frames the last resolve resized, `(id, width, height)`, owed `OnSizeChanged` outside it.
|
||
pub(crate) pending_size_changed: Vec<(u32, f32, f32)>,
|
||
/// Regions whose text emptied or filled since the last resolve, which names each one whose
|
||
/// node hash moved ([`Model::touch_layout_text`]).
|
||
pub(crate) text_flips: Vec<RegionHandle>,
|
||
|
||
/// Script errors caught from `pcall`'d handlers, drained by the host each frame.
|
||
pub(crate) errors: Vec<String>,
|
||
/// Errors owed to `geterrorhandler()`, which the reference calls on each caught error; one the
|
||
/// handler itself raises skips this queue, which stops the recursion.
|
||
pub(crate) pending_error_dispatch: Vec<String>,
|
||
/// The session's retained diagnostic log, deduplicated and bounded; `errors` drains each frame.
|
||
pub(crate) diagnostics: super::diagnostics::DiagnosticLog,
|
||
/// Non-fatal warnings for the host, such as a dropped `inherits=` template.
|
||
pub(crate) warnings: Vec<String>,
|
||
/// The screen-root rect (`[bottom, left, top, right]`), the anchor base for top-level frames:
|
||
/// 768 tall at scale 1, whatever the UI scale ([`super::ui_scale`]).
|
||
pub(crate) screen: Rect,
|
||
/// The host's `uiScale`, which [`super::UiScript::apply_ui_scale`] makes `UIParent`'s scale.
|
||
pub(crate) ui_scale_request: Option<f32>,
|
||
|
||
/// Each unit token's state as pushed this frame, keyed lowercased because 1.12's resolver
|
||
/// (`0x515970`) matches with `SStrCmpI`: read it only through `unit`, where the fold lives.
|
||
pub(crate) units_by_lower: HashMap<String, UnitState>,
|
||
/// The snapshot of each unit a `target` chain can end on, by guid, which the app pushes for
|
||
/// [`super::UnitGuids::chain_ends`]: a token with a hop that no push names reads its
|
||
/// resolved guid's entry here.
|
||
pub(crate) units_by_guid: HashMap<u64, UnitState>,
|
||
/// The player's auras in the reference cache's insertion order (`0xbc6040`), durations joined:
|
||
/// the `GetPlayerBuff` family's list, and any token naming the player's.
|
||
pub(crate) player_auras: Vec<AuraState>,
|
||
/// Every other unit's auras by guid, ascending aura slot: one list per unit, whatever token
|
||
/// names it.
|
||
pub(crate) unit_auras: HashMap<u64, Vec<AuraState>>,
|
||
/// The unit-token resolver's inputs (`0x515970`), which the aura bindings resolve through.
|
||
pub(crate) unit_guids: super::UnitGuids,
|
||
/// Spell ids the cancel verbs queued (`CancelPlayerBuff`, `CancelTrackingBuff`, …), one
|
||
/// `CMSG_CANCEL_AURA` each.
|
||
pub(crate) cancel_aura_requests: Vec<u32>,
|
||
/// The player's active tracking aura, behind `GetTrackingTexture`.
|
||
pub(crate) tracking: Option<super::aura::TrackingState>,
|
||
/// The calls that touch the selection, the cast or the targeting cursor, in call order.
|
||
pub(crate) script_calls: Vec<super::calls::ScriptCall>,
|
||
/// The binding functions' queue, gate depth and mouselook state.
|
||
pub(crate) input: super::input_verbs::InputState,
|
||
/// `DropItemOnUnit` tokens (`0x48d960`), gated by the app; a refusal silently keeps the item.
|
||
pub(crate) drop_item_on_unit: Vec<String>,
|
||
|
||
/// Channels the server confirmed, in join order: the numbers `GetChannelName` answers.
|
||
pub(crate) joined_channels: Vec<Option<String>>,
|
||
/// The party or raid roster the app pushes; the default is not in a group.
|
||
pub(crate) party: party::PartyState,
|
||
/// Party and loot-method calls (`AcceptGroup`, `InviteToParty`, `SetLootMethod`, …) queued.
|
||
pub(crate) party_requests: Vec<party::PartyRequest>,
|
||
/// The ready-check deadline and the unanswered flags the timeout tick reads.
|
||
pub(crate) ready_check: party::ReadyCheckState,
|
||
/// Saved raid lockouts from `SMSG_RAID_INSTANCE_INFO`, which outlive any roster change.
|
||
pub(crate) saved_instances: Vec<party::SavedInstanceInfo>,
|
||
/// `SetRaidRosterSelection`'s raid row index, client-side only.
|
||
pub(crate) raid_selection: i64,
|
||
/// `GetTime()`'s session seconds, which only the host moves ([`super::UiScript::tick`],
|
||
/// [`super::UiScript::set_now`]); off `_G`, as the reference's clock is the OS tick count.
|
||
pub(crate) now: f64,
|
||
/// `GetGameTime()`'s `(hour, minute)`, pushed by the host as the game clock ticks.
|
||
pub(crate) game_time: (u32, u32),
|
||
/// The zone caches the four zone-text getters and `GetZonePVPInfo` read.
|
||
pub(crate) zone: super::ZoneTexts,
|
||
/// The current map's `Map.dbc` `InstanceType`, all `IsInInstance()` reads; `None` for no row.
|
||
pub(crate) instance_type: Option<u32>,
|
||
/// `CanShowResetInstances()`, the reference's four-term predicate (`0x495c90`), app-computed.
|
||
pub(crate) can_reset_instances: bool,
|
||
/// `ResetInstances()` calls queued.
|
||
pub(crate) reset_instance_asks: u32,
|
||
/// Friends, ignores and the last `/who`, display-resolved as the reference resolves them.
|
||
pub(crate) social: social::SocialState,
|
||
/// Social calls (`AddFriend`, `RemoveFriend`, `SendWho`, …) queued.
|
||
pub(crate) social_requests: Vec<social::SocialRequest>,
|
||
/// The client-local LFG slot words (`[0xbc70a0]`) and comment (`[0xbc6e98]`, 0x80 bytes) that
|
||
/// `SetLookingForGroup` writes and `GetLookingForGroup` reads; the reference's slots stay 0.
|
||
pub(crate) lfg_slots: [u32; 3],
|
||
pub(crate) lfg_comment: String,
|
||
/// Guild roster, ranks, MOTD and info, sorted and filtered by the app, which holds the toggles.
|
||
pub(crate) guild: guild::GuildState,
|
||
/// The rank editor's unsaved edits, apart from `guild` so a push cannot discard them.
|
||
pub(crate) guild_control: guild::GuildRankEdit,
|
||
/// Guild calls (`GuildInviteByName`, `GuildSetMOTD`, `GuildControlSaveRank`, …) queued.
|
||
pub(crate) guild_requests: Vec<guild::GuildRequest>,
|
||
/// The charter price and open petition, names resolved from caches as the reference does.
|
||
pub(crate) petition: petition::PetitionState,
|
||
/// Charter calls (`BuyGuildCharter`, `SignPetition`, `TurnInGuildCharter`, …) queued.
|
||
pub(crate) petition_requests: Vec<petition::PetitionRequest>,
|
||
/// Per chat window from `ChatFrame1`, the tint, alpha and font size its tab menu can change.
|
||
pub(crate) chat_window_looks: [chat_window::ChatWindowLook; chat_window::NUM_CHAT_WINDOWS],
|
||
/// 0-based windows whose look Lua changed, the persist cue.
|
||
pub(crate) chat_window_changes: HashSet<usize>,
|
||
/// Chat-type colours (`0xb4e518`): 94 fixed and 10 `CHANNELn`, overlaid by `chat-cache.txt`.
|
||
pub(crate) chat_colors: Vec<super::chat_types::ChatTypeColor>,
|
||
pub(crate) chat_colors_changed: bool,
|
||
/// The languages this character knows, in `Languages.dbc` row order.
|
||
pub(crate) known_languages: Vec<String>,
|
||
/// `Languages.dbc`'s `(ID, Name_lang)` rows in file order, whatever the character knows.
|
||
pub(crate) language_table: Vec<(u32, String)>,
|
||
pub(crate) zone_channel_catalog: Vec<super::channel::ZoneChannelRow>,
|
||
pub(crate) channel_commands: Vec<super::channel::ChannelCommand>,
|
||
/// The recruitment auto-join latch `[0x843608]`: 0 `STANDARD`, 1 `AUTO` (`0x49ea70` maps any
|
||
/// other word to 1). Boots at 1, as the reference writes `AUTO` for an untouched option.
|
||
pub(crate) guild_recruitment_mode: u8,
|
||
/// Any `SetGuildRecruitmentMode(1)`, even unchanged: the reference runs the cascade `0x49ea90`.
|
||
pub(crate) guild_recruitment_cascade: bool,
|
||
/// Lua changed the recruitment mode: the chat cache's dirty signal.
|
||
pub(crate) guild_recruitment_changed: bool,
|
||
/// `DoEmote` calls queued.
|
||
pub(crate) emote_requests: Vec<super::chat_misc::EmoteRequest>,
|
||
/// `RandomRoll` calls queued.
|
||
pub(crate) roll_requests: Vec<(u32, u32)>,
|
||
/// `UninviteByName` calls queued.
|
||
pub(crate) uninvite_requests: Vec<String>,
|
||
/// `ConsoleExec` lines that were not CVar writes.
|
||
pub(crate) console_lines: Vec<String>,
|
||
/// `LoggingChat` and `LoggingCombat`'s flags, and their change cue.
|
||
pub(crate) logging_chat: bool,
|
||
pub(crate) logging_combat: bool,
|
||
pub(crate) logging_changed: bool,
|
||
/// A user-placed frame moved or resized, or `SetUserPlaced` ran: the layout cache's save cue.
|
||
pub(crate) user_placed_changed: bool,
|
||
/// The default chat language; `None` is no player, so `GetDefaultLanguage()` returns nothing.
|
||
pub(crate) default_language: Option<String>,
|
||
/// `AcceptDuel`, `CancelDuel` and `StartDuel*` calls; Lua sees duels only through events.
|
||
pub(crate) duel_requests: Vec<duel::DuelRequest>,
|
||
/// `FollowUnit` and `FollowByName` calls; Lua sees following only as `AUTOFOLLOW_*` events.
|
||
pub(crate) follow_requests: Vec<follow::FollowRequest>,
|
||
/// `SetView`, `SaveView`, `ResetView`, `NextView`, `PrevView` and `FlipCameraYaw` calls; the
|
||
/// views reach Lua only as CVars, `cameraView` and `camera{Distance,Pitch,Yaw}{,A..D}`.
|
||
pub(crate) camera_view_requests: Vec<camera_view::CameraViewRequest>,
|
||
/// `Logout`, `Quit`, `CancelLogout`, `ForceQuit` calls; Lua sees the countdown only as events.
|
||
pub(crate) session_requests: Vec<session::SessionRequest>,
|
||
/// The glue core's slots and request queue; empty in an in-game VM.
|
||
pub(crate) glue: super::glue_core::GlueCore,
|
||
/// `PLAYER_LOGIN`'s one-shot, the reference's `[0xb4e260]`: armed by the UI load, spent by
|
||
/// the next world-enter cascade ([`super::UiScript::fire_world_enter`]).
|
||
pub(crate) player_login_armed: bool,
|
||
/// `TogglePVP` calls, a count because `CMSG_TOGGLE_PVP` has no body.
|
||
pub(crate) pvp_toggles: u32,
|
||
/// The player's private honor fields; before the first push the six self getters read zeros.
|
||
pub(crate) honor: Option<pvp::HonorState>,
|
||
/// The last `MSG_INSPECT_HONOR_STATS` reply; its presence is `HasInspectHonorData`'s answer.
|
||
pub(crate) inspect_honor: Option<pvp::InspectHonorData>,
|
||
/// `RequestInspectHonorData` calls; the pending gate keeps this at 0 or 1.
|
||
pub(crate) inspect_honor_requests: u32,
|
||
/// An inspect-honor query is out (`0xb71fcc`); `RequestInspectHonorData` (`0x4c80a0`) refuses
|
||
/// while set. Cleared by a reply (`0x4c6f4c`) or by the app clearing the slot (`0x4c6f9d`).
|
||
pub(crate) inspect_honor_pending: bool,
|
||
/// `ShowingHelm()`: `PLAYER_FLAGS`' `HIDE_HELM` bit as pushed, set ahead on `ShowHelm` because
|
||
/// the wire verb is a blind toggle. Starts shown, as a fresh character's flags are 0.
|
||
pub(crate) helm_shown: bool,
|
||
/// `ShowingCloak()`, the same for `HIDE_CLOAK`.
|
||
pub(crate) cloak_shown: bool,
|
||
/// Flips from `ShowHelm`/`ShowCloak`, one `CMSG_TOGGLE_HELM`/`CMSG_TOGGLE_CLOAK` each.
|
||
pub(crate) worn_display_toggles: Vec<super::worn_display::WornDisplay>,
|
||
|
||
/// `PLAYER_FIELD_BYTES` byte 2 (descriptor `+0x102a`), which extra action bars are on. Only
|
||
/// the server moves it, as in the reference: `SetActionBarToggles` leaves it a round trip late.
|
||
pub(crate) action_bar_toggles: Option<u8>,
|
||
/// One `CMSG_SET_ACTIONBAR_TOGGLES` payload per `SetActionBarToggles` call, ungated.
|
||
pub(crate) action_bar_toggle_sends: Vec<u8>,
|
||
|
||
/// `PlaySound` and `PlaySoundFile` calls queued.
|
||
pub(crate) sound_queue: Vec<SoundRequest>,
|
||
|
||
/// `PlayMusic` and `StopMusic` calls in call order, for the Lua music stream (`[0xb06ccc]`).
|
||
pub(crate) music_queue: Vec<MusicRequest>,
|
||
|
||
/// The UI-load sound-suppression depth, the reference's `[0xb05fa0]` (`0x458f50` inc,
|
||
/// `0x458f60` dec), held across the whole UI load `0x48fbf0`, so login and `/reloadui` are
|
||
/// silent. Only name-keyed `PlaySound` (`0x458030`) reads it: the call is dropped before any
|
||
/// lookup yet answers `(willPlay, handle)` as usual. `PlaySound(kitId)` (`0x457fb0`),
|
||
/// `PlaySoundFile` and music play normally.
|
||
pub(crate) sound_suppression: u32,
|
||
|
||
/// The CVar table, by lowercased name.
|
||
pub(crate) cvars: HashMap<String, super::cvars::CvarSlot>,
|
||
/// Saved CVar values, set before any registration so a CVar starts at its saved value: the
|
||
/// reference's table outlives `ReloadUI`, while ours is per VM.
|
||
pub(crate) cvars_saved_base: HashMap<String, String>,
|
||
/// `(registered name, new value)` per Lua `SetCVar`, the app's sync and save cue.
|
||
pub(crate) cvar_changes: Vec<(String, String)>,
|
||
/// `(name, default)` per addon `RegisterCVar` that created a slot.
|
||
pub(crate) cvar_registrations: Vec<(String, String)>,
|
||
pub(crate) cvars_warned: HashSet<String>,
|
||
/// The lowercased names a Lua `SetCVar` refuses, the reference's flag bit2 (`rec+0x1c & 4`,
|
||
/// `CVar::SetReadOnly 0x63e030`). Kept beside the rows, so a host re-seed never clears it.
|
||
pub(crate) cvars_read_only: HashSet<String>,
|
||
|
||
/// The adapter's multisample formats in dropdown order, the reference's list `[0xb4b444]` of
|
||
/// `{colorBits, depthBits, multisample}` (count `[0xb4b440]`, built by `0x48c3e0`).
|
||
pub(crate) multisample_formats: Vec<super::cvars::MultisampleFormat>,
|
||
|
||
/// This display's resolutions, ascending, behind `GetScreenResolutions`; empty until pushed.
|
||
pub(crate) screen_resolutions: Vec<super::cvars::ScreenResolution>,
|
||
/// The live size's index in that list, which the host always includes; `None` while the list
|
||
/// is empty. `GetCurrentResolution` answers it 1-based.
|
||
pub(crate) current_resolution: Option<usize>,
|
||
/// What this device and presentation path offer, behind `GetVideoCaps`.
|
||
pub(crate) video_caps: super::cvars::VideoCaps,
|
||
/// `RestartGx()` calls, the video window's apply button.
|
||
pub(crate) restart_gx_asks: u32,
|
||
|
||
/// Globals `RegisterForSave` declared, in order: written at logout and re-run at load.
|
||
pub(crate) saved_names: Vec<String>,
|
||
/// Saved-variables files that failed to load; the shutdown write leaves them untouched.
|
||
pub(crate) held_saved_files: Vec<std::path::PathBuf>,
|
||
|
||
/// The key-binding table the Key Bindings window edits, with its account and character sets.
|
||
pub(crate) keybinds: super::keybind::KeybindState,
|
||
|
||
/// Action slots by Lua action id, 1..120.
|
||
pub(crate) actions: HashMap<u32, ActionSlot>,
|
||
/// Per action, its usable, range, current and cooldown state; an absent one reads cold.
|
||
pub(crate) action_states: HashMap<u32, super::action::StoredActionState>,
|
||
pub(crate) bonus_bar_offset: u8,
|
||
/// `(action id, packed)` per slot `PickupAction`/`PlaceAction` changed, 0 clearing it: one
|
||
/// `CMSG_SET_ACTION_BUTTON` each, so a drag swap is two sends.
|
||
pub(crate) action_sets: Vec<(u32, u32)>,
|
||
/// GlobalStrings keys of refusals this crate raises, fired by the app as `UI_ERROR_MESSAGE`
|
||
/// (the reference's `DisplayError` `0x496720`), each a literal from `0xb4b498` (stride `0x14`).
|
||
pub(crate) ui_errors: Vec<&'static str>,
|
||
|
||
pub(crate) spellbook: spellbook::SpellBookState,
|
||
/// The pet's spellbook, a second array as in the reference (`0xb700f0`, `0xb6f098`).
|
||
pub(crate) pet_book: spellbook::PetBookState,
|
||
/// The player's macros, owned here as 1.12 macros have no server side; the app persists them.
|
||
pub(crate) macros: macros::MacroState,
|
||
/// A script changed the macros: the save and `UPDATE_MACROS` cue.
|
||
pub(crate) macros_dirty: bool,
|
||
/// Bumped by every seed and change, for readers that must not drain `macros_dirty`.
|
||
pub(crate) macros_generation: u64,
|
||
/// Each macro's cached cast by 1-based index, from the app; an absent macro reads unbound.
|
||
pub(crate) macro_bindings: HashMap<u32, macros::MacroBinding>,
|
||
/// The macro icon paths from `SpellIcon.dbc`, behind `GetMacroIconInfo`.
|
||
pub(crate) macro_icons: Vec<String>,
|
||
/// `ToggleSpellAutocast` ids for `CMSG_PET_SPELL_AUTOCAST` (`0x2F3`), which names a spell.
|
||
pub(crate) pet_spell_autocasts: Vec<u32>,
|
||
/// An auto-repeat or cast that `SpellStopCasting()` can stop, not a channel (`0x6e6e80`). Its
|
||
/// 1 or nil matters: the ESC chain (`UIParent.lua:1489`) reaches `CloseAllWindows()` on nil.
|
||
pub(crate) casting: bool,
|
||
/// Spell targeting is on (`SpellIsTargeting`, `0x6e6cd0`); it gates `SpellStopTargeting()`,
|
||
/// whose nil the ESC chain falls through on (`UIParent.lua:1490`).
|
||
pub(crate) spell_targeting: bool,
|
||
/// The units, by guid, for which `SpellCanTargetUnit`'s armed unit word clears fully, which
|
||
/// stock unit frames ask before they call `SpellTargetUnit`; a token resolves to its guid
|
||
/// through [`Self::unit_guids`].
|
||
pub(crate) spell_targetable_units: HashSet<u64>,
|
||
|
||
pub(crate) talents: super::talent::TalentUiState,
|
||
/// `LearnTalent(tab, index)` calls queued.
|
||
pub(crate) talent_learns: Vec<(u32, u32)>,
|
||
/// `ConfirmTalentWipe()` calls, one `MSG_TALENT_WIPE_CONFIRM` each; the app holds the trainer.
|
||
pub(crate) talent_wipe_confirms: u32,
|
||
/// `CheckTalentMasterDist()`: the respec question is live and in range; false hides its dialog.
|
||
pub(crate) talent_master_pending: bool,
|
||
// ── The dialog engine's verbs (`0x48dca0` is the first below) ──
|
||
/// `ConfirmPetUnlearn()` calls; the app holds the trainer and the money gate.
|
||
pub(crate) pet_unlearn_confirms: u32,
|
||
/// `CheckPetUntrainerDist()`: the pet trainer's question is pending and in reach.
|
||
pub(crate) pet_untrainer_pending: bool,
|
||
/// `GetInstanceBootTimeRemaining()` in whole seconds, from the `SMSG_RAID_GROUP_ONLY` deadline.
|
||
pub(crate) instance_boot_secs: u32,
|
||
/// A cached area spirit healer (`[0xb4e330/334]`) and seconds to its wave (`[0xb4e338]`).
|
||
pub(crate) area_spirit_healer_cached: bool,
|
||
pub(crate) area_spirit_secs: u32,
|
||
/// `AcceptAreaSpiritHeal()` calls, one `CMSG_AREA_SPIRIT_HEALER_QUEUE` each.
|
||
pub(crate) area_spirit_accepts: u32,
|
||
/// `AcceptBattlefieldPort(index, accept)` calls: the 1-based slot and the normalised answer.
|
||
pub(crate) battlefield_port_requests: Vec<(u8, bool)>,
|
||
/// The battleground scoreboard: pushed rows, faction filter, sort order.
|
||
pub(crate) battlefield_board: super::battlefield_score::ScoreBoard,
|
||
/// `GetBattlefieldInstanceRunTime()`, pushed each frame.
|
||
pub(crate) battlefield_run_time_ms: u32,
|
||
/// `RequestBattlefieldScoreData()` calls.
|
||
pub(crate) battlefield_score_requests: u32,
|
||
/// `LeaveBattlefield()` calls that passed the "ended" gate.
|
||
pub(crate) battlefield_leave_requests: u32,
|
||
/// The battleground instance list and its scalars, as pushed.
|
||
pub(crate) battlefield_list: super::battlefield_queue::BattlefieldListView,
|
||
/// The selected instance id (`[0xb6eba0]`), not an index; written by `SetSelectedBattlefield`.
|
||
pub(crate) battlefield_selected: u32,
|
||
/// The three queue slots, pushed each frame with their clocks reduced to values. Zeroed
|
||
/// until the first push, as the reference's static slots (`0xb6e9d0`) are, so each answers
|
||
/// "none" from the first frame.
|
||
pub(crate) battlefield_slots: [super::battlefield_queue::BattlefieldQueueSlot; 3],
|
||
/// `GetBattlefieldInstanceExpiration()`, pushed each frame.
|
||
pub(crate) battlefield_instance_expiration_ms: u32,
|
||
/// `JoinBattlefield` calls, `(instance id, as group)`.
|
||
pub(crate) battlefield_join_requests: Vec<(u32, bool)>,
|
||
/// `ShowBattlefieldList` calls that passed their gates: the queued slot's map id.
|
||
pub(crate) battlefield_list_requests: Vec<u32>,
|
||
/// Teammates' map positions, the flag carrier, the icon scale and position requests.
|
||
pub(crate) battlefield_positions: Vec<super::battlefield_positions::BattlefieldPositionView>,
|
||
pub(crate) battlefield_flag: Option<super::battlefield_positions::BattlefieldFlagView>,
|
||
pub(crate) battlefield_icon_scale: f32,
|
||
pub(crate) battlefield_position_requests: u32,
|
||
/// `CancelMeetingStoneRequest()` calls; the app gates on leadership.
|
||
pub(crate) meeting_stone_cancels: u32,
|
||
/// The meeting stone's queued area (`[0xb72038]`) and status text (`[0xb7203c]`), as pushed.
|
||
pub(crate) meeting_stone_area: u32,
|
||
pub(crate) meeting_stone_status_text: Option<String>,
|
||
/// Acknowledged tutorials, `None` before `SMSG_TUTORIAL_FLAGS`; all `TutorialsEnabled()` reads.
|
||
pub(crate) tutorial_bank: Option<Vec<u8>>,
|
||
/// `FlagTutorial(n)` calls, 0-based and in range.
|
||
pub(crate) tutorial_flag_requests: Vec<u32>,
|
||
pub(crate) tutorial_clears: u32,
|
||
pub(crate) tutorial_resets: u32,
|
||
|
||
/// The stance bar's forms, in bar order.
|
||
pub(crate) shapeshift_forms: Vec<super::shapeshift::StoredShapeshiftForm>,
|
||
|
||
/// The pet bar's ten slots and two bar-wide bits, replaced whole by every `SMSG_PET_SPELLS`.
|
||
pub(crate) pet_bar: super::pet::PetBarState,
|
||
/// 1-based slot indices `TogglePetAutocast` queued.
|
||
pub(crate) pet_autocast_toggles: Vec<u32>,
|
||
/// `PetStopAttack()` calls.
|
||
pub(crate) pet_stop_attacks: u32,
|
||
/// `HasFullControl`, the reference's `[0xb4b3e4]`: set by `SMSG_CLIENT_CONTROL_UPDATE` for the
|
||
/// player, boots 1, and every cast, item and cursor gate refuses at 0.
|
||
pub(crate) player_control: bool,
|
||
/// One `CMSG_PET_SET_ACTION` per entry, its one or two `(position, packed word)` pairs: the
|
||
/// server tells the forms apart by body size, so a move's two pairs must not be flattened.
|
||
pub(crate) pet_set_actions: Vec<Vec<(u32, u32)>>,
|
||
/// `PetAbandon()` and `PetDismiss()` calls, counted apart though both end at one opcode.
|
||
pub(crate) pet_abandons: u32,
|
||
pub(crate) pet_dismisses: u32,
|
||
/// Names `PetRename` queued, from the `PETRENAMECONFIRM` popup.
|
||
pub(crate) pet_renames: Vec<String>,
|
||
|
||
/// Bag contents by API bag id (0 is the backpack).
|
||
pub(crate) containers: HashMap<i64, container::ContainerState>,
|
||
/// Per `(bag, slot)`, `(start, duration, enabled)` in `GetTime` seconds, stamped at push.
|
||
pub(crate) container_cooldowns: HashMap<(i64, u32), (f64, f64, bool)>,
|
||
/// `HasKey()`: a keys-family item in equipment, bags, bank or keyring; gates the keyring UI.
|
||
pub(crate) has_key: bool,
|
||
/// What the cursor carries; nothing moves locally, the server's updates settle the bags.
|
||
pub(crate) cursor: Option<cursor::CursorPayload>,
|
||
/// The cursor's one grid latch, `[0xb4da24]`: set by a setter that showed a bar's drop grid,
|
||
/// cleared by `ClearCursor`'s tail, which hides the pet bar's for a held pet action and the
|
||
/// action bar's otherwise (`0x49529b`).
|
||
pub(crate) cursor_grid: bool,
|
||
/// The world pick under the cursor (`[this+0x350]`): a world click drops no payload on an
|
||
/// object, only an item on terrain, and anything over nothing.
|
||
pub(crate) world_pick: cursor::WorldPick,
|
||
/// Held-item bag moves, sent as `CMSG_SWAP_INV_ITEM` or `CMSG_SPLIT_ITEM`.
|
||
pub(crate) container_moves: Vec<container::ContainerMove>,
|
||
/// `(bag, slot)` clicks made in repair mode.
|
||
pub(crate) container_repairs: Vec<(i64, u32)>,
|
||
/// Targeting wants an item (`0x6e6330`): a bag or paper-doll click becomes a pick.
|
||
pub(crate) item_pick_armed: bool,
|
||
/// Picked `(bag, slot)`s; a paper-doll click is `EQUIPMENT_BAG` and its 1-based inventory slot.
|
||
pub(crate) item_picks: Vec<(i64, u32)>,
|
||
/// `BindEnchant()` and `ReplaceEnchant()`, the enchant popups' answers.
|
||
pub(crate) enchant_confirms: Vec<cursor::EnchantConfirm>,
|
||
/// `DeleteCursorItem`'s `(bag, slot, count)`, 0 for the whole stack, as `CMSG_DESTROYITEM`.
|
||
pub(crate) container_destroys: Vec<(i64, u32, u32)>,
|
||
/// The wrapping paper a right-click armed (`0x5edea0`: lock it, cursor mode 2, no packet). Not
|
||
/// a cursor payload: the reference sets only the cursor mode, so `CursorHasItem()` stays nil.
|
||
pub(crate) pending_wrap: Option<container::PendingWrap>,
|
||
/// `(giftBag, giftSlot, itemBag, itemSlot)` per completed wrap, in `CMSG_WRAP_ITEM`'s order.
|
||
pub(crate) container_wraps: Vec<(i64, u32, i64, u32)>,
|
||
/// The cursor mode Lua set (`0xbe2c2c`); `ResetCursor` returns it to the world's mode.
|
||
pub(crate) ui_cursor: Option<container::UiCursorMode>,
|
||
/// A FrameXML cursor call ran. The mode is a write, not a level: `0xbe2c2c` keeps its value
|
||
/// until the world or a hover handler writes it, so a handler-less frame leaves it be.
|
||
pub(crate) ui_cursor_dirty: bool,
|
||
/// `AutoEquipCursorItem`'s `(bag, slot)` sources, sent as `CMSG_AUTOEQUIP_ITEM`.
|
||
pub(crate) container_autoequips: Vec<(i64, u32)>,
|
||
/// `PutItemInBag` and `PutItemInBackpack` stores, sent as `CMSG_AUTOSTORE_BAG_ITEM`, or
|
||
/// `CMSG_SPLIT_ITEM` for a split stack.
|
||
pub(crate) bag_autostores: Vec<container::BagAutoStore>,
|
||
|
||
/// `RegisterForDrag` buttons per frame, compared case-insensitively. Nothing destroys a frame,
|
||
/// so this, `scripts` and `frame_events` are never pruned; a destroy path must prune all three.
|
||
pub(crate) drag_registered: HashMap<FrameHandle, HashSet<String>>,
|
||
/// The drag gesture, armed by a press on a drag-registered frame, started past the threshold.
|
||
pub(crate) drag: Option<cursor::DragGesture>,
|
||
/// The one move or size in flight, the client's `root+0xcfc` slot with its grip (`+0xd04`),
|
||
/// apart from `drag` as in the reference: a `StartMoving` or `StartSizing` outlives the button,
|
||
/// since mouse-up's auto-stop skips a Lua drag.
|
||
pub(crate) moving: Option<super::object::FrameMove>,
|
||
/// `SetLayoutMode`'s value, the root's `+0xcf8` (0 at start, `0x7642a5`): while non-zero, a
|
||
/// press with Control or Alt held is a layout drag (`0x766301`).
|
||
pub(crate) layout_mode: i32,
|
||
/// A Slider thumb drag, left press to release or pointer leave, handled here with no Lua.
|
||
pub(crate) slider_drag: Option<slider::SliderDrag>,
|
||
/// A ColorSelect wheel or value-bar drag, press to release.
|
||
pub(crate) color_drag: Option<colorselect::ColorDrag>,
|
||
|
||
/// The open gossip menu, the `SelectGossipOption` calls and whether `CloseGossip` ran.
|
||
pub(crate) gossip: Option<gossip::GossipMenu>,
|
||
pub(crate) gossip_selects: Vec<u32>,
|
||
pub(crate) gossip_close: bool,
|
||
/// `SelectGossipQuest` 1-based rows, each sent as `CMSG_QUESTGIVER_QUERY_QUEST` for its quest.
|
||
pub(crate) gossip_quest_selects: Vec<u32>,
|
||
|
||
/// The open vendor's stock, the `BuyMerchantItem` calls and whether `CloseMerchant` ran.
|
||
pub(crate) merchant: Option<merchant::MerchantState>,
|
||
/// `GetRepairAllCost`'s total; 0 unless a repairer is open.
|
||
pub(crate) repair_all_cost: u32,
|
||
/// The repair costs `SetInventoryItem` and `SetBagItem` return.
|
||
pub(crate) repair_costs: merchant::RepairCosts,
|
||
pub(crate) merchant_buys: Vec<(u32, u32)>,
|
||
/// The held `(bag, slot)` when `PickupMerchantItem` sells, sent as `CMSG_SELL_ITEM`.
|
||
pub(crate) merchant_cursor_sells: Vec<(i64, u32)>,
|
||
/// A held vendor row dropped in a bag, `(bag, slot, item entry)`, for `CMSG_BUY_ITEM_IN_SLOT`.
|
||
pub(crate) merchant_slot_buys: Vec<(i64, u32, u32)>,
|
||
pub(crate) merchant_close: bool,
|
||
/// `BuybackItem` 1-based slots, the `RepairAllItems` flag and repair mode (`InRepairMode`).
|
||
pub(crate) merchant_buybacks: Vec<u32>,
|
||
pub(crate) repair_all: bool,
|
||
pub(crate) repair_mode: bool,
|
||
|
||
/// The stable window: snapshot, selection, drag and queued calls, reset together on close.
|
||
pub(crate) stable: stable::StableModel,
|
||
|
||
/// The open bank's slot-purchase row and calls; its contents are bags -1 and 5..=10.
|
||
pub(crate) bank: Option<bank::BankState>,
|
||
pub(crate) bank_purchase: bool,
|
||
pub(crate) bank_close: bool,
|
||
|
||
/// The open trainer, its buys, selection and close. The selection is a spell id, since
|
||
/// filters, collapses and re-lists all move row numbers.
|
||
pub(crate) trainer: Option<trainer::TrainerState>,
|
||
pub(crate) trainer_buys: Vec<u32>,
|
||
pub(crate) trainer_selection: Option<u32>,
|
||
pub(crate) trainer_close: bool,
|
||
/// Available, unavailable and used filters, all on by default; they hide services, not headers.
|
||
pub(crate) trainer_filter: [bool; 3],
|
||
/// Collapsed skill-line ids, kept across a re-push and cleared when the trainer closes.
|
||
pub(crate) trainer_collapsed: HashSet<u32>,
|
||
|
||
/// The open taxi map, the `TakeTaxiNode` calls, whether `CloseTaxiMap` ran and whether we ride.
|
||
pub(crate) taxi: Option<taxi::TaxiUiState>,
|
||
pub(crate) taxi_takes: Vec<usize>,
|
||
pub(crate) taxi_close: bool,
|
||
pub(crate) taxi_riding: bool,
|
||
|
||
/// The open tradeskill window's recipes.
|
||
pub(crate) trade_skill: Option<tradeskill::TradeSkillState>,
|
||
/// `DoTradeSkill` calls, `(spell id, count)`.
|
||
pub(crate) trade_skill_dos: Vec<(u32, u32)>,
|
||
/// The 1-based selection, 0 for none, carried across a re-push by the recipe's spell id.
|
||
pub(crate) trade_skill_selection: u32,
|
||
pub(crate) trade_skill_close: bool,
|
||
/// Collapsed recipe groups by `(ItemClass, ItemSubClass)` (`0x55ba30`), kept across a re-push
|
||
/// and a same-profession reopen like the mask `0x84dd68`; a profession switch resets them.
|
||
pub(crate) trade_skill_collapsed: HashSet<(u32, u32)>,
|
||
/// Groups the SubClass filter hides, empty for all, kept by key like the client's header flag
|
||
/// (`header+0xc`, `0x4fca20`, mask `0x84dd60`); a profession switch resets them.
|
||
pub(crate) trade_skill_subclass_hidden: HashSet<(u32, u32)>,
|
||
/// The InvSlot filter mask (`0x84dd64`), a set bit shown and all ones for all slots. Never
|
||
/// pruned on a re-push, as in the client, so a later slot stays hidden; a switch resets it.
|
||
pub(crate) trade_skill_invslot_mask: u32,
|
||
/// The skill line the state was last built for (`0xbde064`): a push for another line resets
|
||
/// the filters, collapses and selection; the same line keeps them, even across a reopen.
|
||
pub(crate) trade_skill_last_line: u32,
|
||
/// The selected recipe's spell id (`0xbde044`), which carries it across reopen and re-push.
|
||
pub(crate) trade_skill_selected_spell: u32,
|
||
/// A filter or subclass collapse changed: `TRADE_SKILL_UPDATE` (`0x13a`) is owed this frame, as
|
||
/// the client fires it inside the call (`0x4fd710`/`0x4fd730`/`0x4fd750`, `0x4fd180`).
|
||
pub(crate) trade_skill_touched: bool,
|
||
|
||
/// The open craft window's recipes.
|
||
pub(crate) craft: Option<craft::CraftState>,
|
||
/// `DoCraft` spell ids; no count, as the 1.12 CraftFrame has no Create All.
|
||
pub(crate) craft_dos: Vec<u32>,
|
||
/// The 1-based selection, 0 for none, carried across a re-push by the recipe's spell id.
|
||
pub(crate) craft_selection: u32,
|
||
pub(crate) craft_close: bool,
|
||
|
||
/// `EquipPendingItem`/`CancelPendingEquip` answers in order, and `ConfirmBindOnUse()` calls.
|
||
pub(crate) pending_equip_answers: Vec<bind_confirm::PendingEquipAnswer>,
|
||
pub(crate) bind_on_use_confirms: u32,
|
||
|
||
/// The open loot, the row clicks and whether `CloseLoot` ran. A click is the reference's take
|
||
/// `0x4c2790` with flag 0, raising `LOOT_BIND` for a bind-on-pickup row (a `LootButton`'s own
|
||
/// click, `0x4c1820`); `LootSlot` passes 1 (`0x4c2e70`), taking it after the confirm.
|
||
pub(crate) loot: Option<loot::LootState>,
|
||
pub(crate) loot_picks: Vec<u32>,
|
||
/// `LootSlot(slot)` rows, 1-based, apart from clicks for the reference's pending-slot gate.
|
||
pub(crate) loot_confirms: Vec<u32>,
|
||
pub(crate) loot_close: bool,
|
||
/// `GiveMasterLoot(slot, candidateIndex)`, both 1-based display positions the app translates.
|
||
pub(crate) loot_master_gives: Vec<(u32, u32)>,
|
||
|
||
/// The open group-loot rolls, and the `RollOnLoot` `(roll_id, roll_type)` votes.
|
||
pub(crate) loot_rolls: loot_roll::LootRollsState,
|
||
pub(crate) loot_roll_votes: Vec<(u32, u8)>,
|
||
/// Need or Greed on a bind-on-pickup roll: a `CONFIRM_LOOT_ROLL` popup ask, not a vote.
|
||
pub(crate) loot_roll_confirms: Vec<(u32, u8)>,
|
||
|
||
/// The item-text reader: the pushed page, then the close and page-turn calls.
|
||
pub(crate) item_text: Option<item_text::ItemTextState>,
|
||
pub(crate) item_text_close: bool,
|
||
pub(crate) item_text_page_turns: Vec<i32>,
|
||
/// The open inbox and mail calls, rows 1-based: `mail_opens` are rows `GetInboxText` read.
|
||
pub(crate) mail: Option<mail::MailState>,
|
||
pub(crate) mail_check_inbox: bool,
|
||
pub(crate) mail_opens: Vec<u32>,
|
||
pub(crate) mail_take_items: Vec<u32>,
|
||
pub(crate) mail_take_money: Vec<u32>,
|
||
pub(crate) mail_deletes: Vec<u32>,
|
||
pub(crate) mail_returns: Vec<u32>,
|
||
pub(crate) mail_take_texts: Vec<u32>,
|
||
pub(crate) mail_close: bool,
|
||
pub(crate) mail_send: Option<(String, String, String)>,
|
||
pub(crate) mail_send_money: u32,
|
||
pub(crate) mail_send_cod: u32,
|
||
/// The Send tab's attached item, resolved to its guid when the send fires.
|
||
pub(crate) mail_send_item: Option<cursor::CursorItem>,
|
||
/// The usable stationery, in the picker's order.
|
||
pub(crate) mail_stationeries: Vec<mail::StationeryView>,
|
||
/// `SelectStationery`'s `Stationery.dbc` id; 0 is none, which silences `SendMail`.
|
||
pub(crate) mail_stationery: u32,
|
||
/// `Package.dbc`'s rows in file order, `GetPackageInfo`'s list.
|
||
pub(crate) mail_packages: Vec<mail::PackageView>,
|
||
/// `SelectPackage`'s `Package.dbc` id (`[0xb6efb8]`); 0 is none.
|
||
pub(crate) mail_package: u32,
|
||
/// `HasNewMail()` (`0x4afea0`), from `MSG_QUERY_NEXT_MAIL_TIME` and `SMSG_RECEIVED_MAIL`.
|
||
pub(crate) has_new_mail: bool,
|
||
|
||
/// The open auction house and the auction calls. `auction_selected` is each list's selected
|
||
/// auction id, 0 for none (`0x4cfda0`/`0x4cfec0`); `auction_can_query` is the 5 s throttle.
|
||
pub(crate) auction: Option<auction::AuctionState>,
|
||
/// The Browse tab's class tree, pushed once per login.
|
||
pub(crate) auction_item_classes: Vec<auction::AuctionCategory>,
|
||
pub(crate) auction_selected: [u32; 3],
|
||
pub(crate) auction_can_query: bool,
|
||
pub(crate) auction_query: Option<auction::AuctionQuery>,
|
||
pub(crate) auction_owner_query: Option<u32>,
|
||
pub(crate) auction_bidder_query: Option<u32>,
|
||
pub(crate) auction_bids: Vec<auction::AuctionBid>,
|
||
pub(crate) auction_cancels: Vec<u32>,
|
||
pub(crate) auction_start: Option<auction::AuctionStartRequest>,
|
||
pub(crate) auction_sorts: Vec<(usize, String)>,
|
||
pub(crate) auction_close: bool,
|
||
/// The sell slot's item, resolved to its guid when `StartAuction` fires.
|
||
pub(crate) auction_sell_item: Option<cursor::CursorItem>,
|
||
|
||
/// The open trade and its calls: `InitiateTrade` tokens, the accept, unaccept and cancel flags,
|
||
/// and `SetTradeMoney` copper (`CMSG_SET_TRADE_GOLD`).
|
||
pub(crate) trade: Option<trade::TradeState>,
|
||
pub(crate) trade_initiates: Vec<String>,
|
||
pub(crate) trade_accept: bool,
|
||
pub(crate) trade_unaccept: bool,
|
||
pub(crate) trade_close: bool,
|
||
/// `BeginTrade()` and `CancelTrade()` calls.
|
||
pub(crate) trade_begin: bool,
|
||
pub(crate) trade_cancel: bool,
|
||
pub(crate) trade_set_money: Option<u32>,
|
||
/// Items dropped on our slots, `(1-based trade slot, bag, slot)`, and the slots clicks cleared.
|
||
pub(crate) trade_set_items: Vec<(u32, i64, u32)>,
|
||
pub(crate) trade_clear_items: Vec<u32>,
|
||
|
||
/// The open questgiver panel, its greeting-row selects and its button calls.
|
||
pub(crate) quest: Option<quest::QuestState>,
|
||
pub(crate) quest_selects: Vec<quest::QuestSelect>,
|
||
pub(crate) quest_actions: Vec<quest::QuestAction>,
|
||
|
||
/// Death countdowns and offers, and the release, reclaim and resurrect calls.
|
||
pub(crate) death: death::DeathUiState,
|
||
pub(crate) death_actions: Vec<death::DeathAction>,
|
||
|
||
/// The quest log, selection (1-based), abandon mark (a quest id, `0xbb7484`) and abandons.
|
||
pub(crate) quest_log: quest_log::QuestLogState,
|
||
pub(crate) quest_log_selection: u32,
|
||
pub(crate) quest_log_abandon_mark: u32,
|
||
pub(crate) quest_log_abandons: Vec<u32>,
|
||
/// `QuestLogPushQuest()` quest ids, resolved at the click so a log change cannot retarget one.
|
||
pub(crate) quest_log_pushes: Vec<u32>,
|
||
/// `ConfirmAcceptQuest()` calls, the escort confirm; the app holds the pending quest.
|
||
pub(crate) quest_confirms: u32,
|
||
/// Item id to its tooltip view.
|
||
pub(crate) item_templates: HashMap<u32, ItemTemplateView>,
|
||
/// Item ids asked for and missing, for the app to answer.
|
||
pub(crate) item_stat_asks: HashSet<u32>,
|
||
/// Set id to its tooltip set block (`0x854b1c`), and the misses asked for.
|
||
pub(crate) item_sets: HashMap<u32, super::ItemSetView>,
|
||
pub(crate) item_set_asks: HashSet<u32>,
|
||
/// `ItemRandomProperties` id to its view, pushed whole at startup: a clicked tooltip cannot
|
||
/// repaint on a late answer. The reference reads its loaded store `0xc0dbd4` (`0x52c991`).
|
||
pub(crate) random_properties: HashMap<u32, super::RandomPropertyView>,
|
||
/// The player's level, class, race and skills, for a tooltip's red requirement lines.
|
||
pub(crate) player_req: PlayerReqState,
|
||
/// Spell id to its tooltip view, and the misses asked for.
|
||
pub(crate) spell_tooltips: HashMap<u32, super::SpellTooltipView>,
|
||
pub(crate) spell_tooltip_asks: HashSet<u32>,
|
||
/// The same for the views built against the player's pet, which the spell builder's unit
|
||
/// selector asks for (`0x52e610`'s fourth argument).
|
||
pub(crate) pet_spell_tooltips: HashMap<u32, super::SpellTooltipView>,
|
||
pub(crate) pet_spell_tooltip_asks: HashSet<u32>,
|
||
/// The tooltips whose spell render missed its view, re-rendered when the app answers.
|
||
pub(crate) spell_tooltip_waits: HashMap<FrameHandle, super::tooltip_spell::SpellWait>,
|
||
/// `CollapseQuestHeader`/`ExpandQuestHeader` as `(1-based entry, collapse)`, entry 0 for all.
|
||
pub(crate) quest_log_collapses: Vec<(u32, bool)>,
|
||
/// Watched quest ids in watch order, pruned when a quest leaves the log.
|
||
pub(crate) quest_log_watched: Vec<u32>,
|
||
/// The server's clock in Unix seconds, `None` before `SMSG_QUERY_TIME_RESPONSE`. A quest
|
||
/// deadline is a stamp in it, and `GetQuestTimers` subtracts per call, as the reference does.
|
||
pub(crate) server_unix_time: Option<f64>,
|
||
/// `GetMoney()` in copper, from `PLAYER_FIELD_COINAGE`.
|
||
pub(crate) money: u64,
|
||
|
||
/// `GetNetStats`'s third return, the average round trip in ms; 0 before any sample.
|
||
pub(crate) net_latency_ms: u32,
|
||
|
||
/// `UnitXP("player")` and `UnitXPMax("player")`, from the private `PLAYER_XP` fields.
|
||
pub(crate) player_xp: u32,
|
||
pub(crate) player_next_level_xp: u32,
|
||
|
||
/// Raw combo points (`PLAYER_FIELD_BYTES` byte 1) and target, ungated: a warrior banks them too
|
||
/// (Overpower); `GetComboPoints` (`0x51a190`) applies the class and current-target gates.
|
||
pub(crate) combo_points: u8,
|
||
pub(crate) combo_target: u64,
|
||
|
||
/// `PLAYER_BYTES_2` byte 3 (1 rested, 2 normal), defaulting to 2 as every 1.12 character has
|
||
/// it: a 0 has no Exhaustion row, and `GetRestState()`'s nil would raise at the unguarded
|
||
/// `>= 3` of `MainMenuBar.lua:25`. `rest_pool` is in base kill-XP units, and `resting` is
|
||
/// `PLAYER_FLAGS_RESTING` (0x20).
|
||
pub(crate) rest_state: u8,
|
||
pub(crate) rest_pool: u32,
|
||
pub(crate) resting: bool,
|
||
|
||
/// `PLAYER_FLAGS` bit 12, `PartialPlayTime()` (`0x48eb70`), an anti-addiction regime's flag.
|
||
pub(crate) partial_play_time: bool,
|
||
|
||
/// `PLAYER_FLAGS` bit 13, `NoPlayTime()` (`0x48ebe0`), the same regime's harsher half.
|
||
pub(crate) no_play_time: bool,
|
||
|
||
/// `GetBillingTimeRested()` (`0x48ec50`) in minutes from `SMSG_AUTH_RESPONSE`, unconverted.
|
||
pub(crate) billing_time_rested: u32,
|
||
/// `InCinematic()`. While set, `StaticPopup_Show` refuses any dialog without
|
||
/// `interruptCinematic` (`StaticPopup.lua:1454`).
|
||
pub(crate) in_cinematic: bool,
|
||
/// `Exhaustion.dbc` by rest byte, `(name, factor)`: `GetRestState` indexes it and row 1 scales
|
||
/// `GetXPExhaustion` (`0x48d3f0`). enUS rows seed it until the app loads the install's own.
|
||
pub(crate) exhaustion: HashMap<u8, (String, f64)>,
|
||
|
||
/// The paper doll's combat stats, `None` until the first push.
|
||
pub(crate) player_combat_stats: Option<char_stats::UnitCombatStats>,
|
||
/// The pet's combat stats, which the reference's pet sheet reads as `UnitStat("pet", …)`.
|
||
pub(crate) pet_combat_stats: Option<char_stats::UnitCombatStats>,
|
||
pub(crate) inventory_slots: char_stats::InventorySlots,
|
||
/// The six bank-bag slots, ids 64..=69, fed from the player descriptor's guids.
|
||
pub(crate) bank_bag_slots: char_stats::BankBagSlots,
|
||
/// `GetInventoryAlertStatus` in `0x806eb8` order; every push fires `UPDATE_INVENTORY_ALERTS`.
|
||
pub(crate) inventory_alerts: [u8; 12],
|
||
/// Equipped slot ids clicked while the merchant repair cursor is armed.
|
||
pub(crate) inventory_repairs: Vec<u32>,
|
||
/// Main- and off-hand temporary enchants in `GetWeaponEnchantInfo`'s order, pushed each frame.
|
||
pub(crate) weapon_enchants: [Option<weapon_enchant::WeaponEnchant>; 2],
|
||
|
||
/// The inspected unit's gear, from its public `PLAYER_VISIBLE_ITEM_*` fields.
|
||
pub(crate) inspect: Option<inspect::InspectView>,
|
||
/// `NotifyInspect` unit tokens, sent as `CMSG_INSPECT`.
|
||
pub(crate) inspect_notifies: Vec<String>,
|
||
/// `ClearInspectPlayer` ran; the app drops its inspect target.
|
||
pub(crate) inspect_clear: bool,
|
||
/// `DressUpModel` `SetUnit`, `Dress`, `Undress` and `TryOn` calls, applied in call order.
|
||
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
|
||
/// Tabard designs per `TabardModel`, the app's preview five, the host facts and queued calls.
|
||
pub(crate) tabard_designs: HashMap<crate::widget::FrameHandle, [i32; 5]>,
|
||
pub(crate) tabard_preview: Option<[i32; 5]>,
|
||
pub(crate) tabard_host: super::tabard::TabardHost,
|
||
pub(crate) tabard_intents: Vec<super::tabard::TabardIntent>,
|
||
/// The `WorldFrame` type's one-shot record: true once the first is made.
|
||
pub(crate) world_frame_made: bool,
|
||
/// Per live-unit guid, reach for `CanInspect`/`CheckInteractDistance`; absent: nil.
|
||
pub(crate) unit_reach: HashMap<u64, super::UnitReach>,
|
||
|
||
/// The skills pane's snapshot and the display tree built from it.
|
||
pub(crate) skills: skills::SkillsState,
|
||
pub(crate) skills_groups: Vec<skills::SkillGroup>,
|
||
/// Collapsed skill categories by id, kept across a re-push.
|
||
pub(crate) skills_collapsed: HashSet<u32>,
|
||
/// The selection as a skill id, not a row.
|
||
pub(crate) skills_selected: Option<u32>,
|
||
/// `AbandonSkill` ids for `CMSG_UNLEARN_SKILL`; nothing changes until the server's update.
|
||
pub(crate) skill_abandons: Vec<u32>,
|
||
|
||
/// The reputation pane's snapshot and the display tree built from it.
|
||
pub(crate) reputation: reputation::ReputationState,
|
||
pub(crate) reputation_groups: Vec<reputation::FactionGroup>,
|
||
/// Folded headers by key (a `Faction.dbc` id, 0 "Other", -1 "Inactive"), reset on every push
|
||
/// to all open but Inactive, as the client's rebuild does.
|
||
pub(crate) reputation_collapsed: HashSet<i64>,
|
||
/// The selection as a reputation-list slot, not a row.
|
||
pub(crate) reputation_selected: Option<u32>,
|
||
/// Reputation calls queued, applied locally first because none of the three sends is acked.
|
||
pub(crate) reputation_sends: Vec<reputation::ReputationSend>,
|
||
|
||
/// Lines for the app's slash-command parser: a probe's, or a host `SlashCmdList` row's.
|
||
pub(crate) chat_input: Vec<String>,
|
||
|
||
/// The world map's pushed catalog and feed, and its selection.
|
||
pub(crate) worldmap: super::worldmap::WorldMapState,
|
||
/// Nameplate `Button`s under `WorldFrame`, never shrunk, visible to `GetChildren()`.
|
||
pub(crate) nameplates: super::nameplate::NamePlates,
|
||
/// Chat bubble `Frame`s under `WorldFrame`, pooled the same way.
|
||
pub(crate) chat_bubbles: super::chat_bubble::ChatBubbles,
|
||
/// The world-state readout's rows, gated and resolved by the app.
|
||
pub(crate) worldstate: super::worldstate::WorldStateUiState,
|
||
/// The host's event queue, fired at the start of the next tick ahead of its `OnUpdate` pass
|
||
/// (`UiScript::tick`). Host producers only: the app's setters (the world map's, the
|
||
/// world-state rows, a dropped mail attachment), `UiScript::queue_event`, and an engine path
|
||
/// such as a world click that clears the cursor, which has no verb to fire inside and queues
|
||
/// so it stays behind the input already queued. A Lua verb never lands here: the reference's
|
||
/// `SignalEvent` (`0x703e50`, `0x703f50`) runs every handler before it returns, so a verb's
|
||
/// events fire inside the call, through [`super::tick::verb`].
|
||
pub(crate) pending_events: Vec<(String, Vec<ScriptValue>)>,
|
||
/// The events the running [`super::tick::verb`] segment has raised, fired in order once its
|
||
/// model borrow drops; `None` outside a segment, where [`Model::raise`] queues instead.
|
||
pub(crate) verb_events: Option<Vec<(String, Vec<ScriptValue>)>>,
|
||
/// The last cursor position in UI space (logical px, y-up), behind `GetCursorPosition()`.
|
||
pub(crate) cursor_pos: (f32, f32),
|
||
|
||
/// A `Minimap:PingLocation(x, y)`, drained the same frame: centre-relative offsets in the screen
|
||
/// root's units, `GetCursorPosition`'s space, not the window pixels of the app's minimap
|
||
/// geometry.
|
||
pub(crate) minimap_ping_request: Option<(f32, f32)>,
|
||
/// The ping's offsets as fractions of the minimap's side, updated by the app every frame.
|
||
/// `GetPingPosition()` always answers both, from statics never cleared (`0x4eefd0`).
|
||
pub(crate) minimap_ping: (f32, f32),
|
||
/// `SendChatMessage` lines, apart from chat-box input, whose drain runs the slash grammar.
|
||
pub(crate) chat_sends: Vec<super::chat_send::ChatSend>,
|
||
/// `SendAddonMessage` broadcasts: `LANG_ADDON`, four distributions, `prefix` TAB `message`.
|
||
pub(crate) addon_sends: Vec<super::addon_message::AddonSend>,
|
||
/// `RequestTimePlayed()` calls, one empty `CMSG_PLAYED_TIME` each.
|
||
pub(crate) played_time_asks: u32,
|
||
/// `OpeningCinematic()` calls, one empty `CMSG_OPENING_CINEMATIC` each.
|
||
pub(crate) opening_cinematic_asks: u32,
|
||
/// `Screenshot()` calls, one capture each.
|
||
pub(crate) screenshot_asks: u32,
|
||
/// `GetRealmName()`: `""` until pushed, never nil, since addons index tables with it at load.
|
||
pub(crate) realm_name: String,
|
||
/// The local player record (`0xc27d80`), seeded at world entry before addons load, then kept by
|
||
/// `"player"` pushes that carry it: corrected, never blanked. A per-login rebuild is unseen, as
|
||
/// its verbs exist only in game (`0x850438`), not at the glue screen (`0x46abb0`).
|
||
pub(crate) player_record: super::PlayerRecord,
|
||
/// `GetBindLocation()`, the bind point's area name: `""` before it lands, never nil.
|
||
pub(crate) bind_location: String,
|
||
/// `GMTicketCategory.dbc` `(id, name)` rows in file order; the ids are wire values.
|
||
pub(crate) gm_ticket_categories: Vec<(u32, String)>,
|
||
/// GM ticket calls in call order, so a delete then a get reach the wire in that order.
|
||
pub(crate) gm_ticket_intents: Vec<super::gm_ticket::GmTicketIntent>,
|
||
/// `Stuck()` calls, each a cast of spell 7355, the Help window's Auto-Unstuck.
|
||
pub(crate) stuck_casts: u32,
|
||
/// `ConfirmBinder()` calls, one `CMSG_BINDER_ACTIVATE` each; the app holds the innkeeper.
|
||
pub(crate) binder_confirms: u32,
|
||
/// `CheckBinderDist()`: the innkeeper's question is live and in range; false hides its dialog.
|
||
pub(crate) binder_pending: bool,
|
||
/// The summon getters' answers, defaulting to the reference's `""`, `""`, 0 with none pending.
|
||
pub(crate) summon_confirm: super::summon::SummonConfirmUiState,
|
||
/// `ConfirmSummon()` calls, one `CMSG_SUMMON_RESPONSE` each; the app holds the summoner.
|
||
pub(crate) summon_confirms: u32,
|
||
/// `GetFramerate()`, pushed each tick.
|
||
pub(crate) framerate: f64,
|
||
/// `(shift, ctrl, alt)` for `Is*KeyDown`, pushed before the frame's mouse events.
|
||
pub(crate) modifiers: (bool, bool, bool),
|
||
}
|
||
|
||
impl Model {
|
||
/// The selected quest-log row's detail, resolved per call as the reference's bindings do.
|
||
pub(crate) fn selected_quest_detail(&self) -> Option<&quest_log::QuestLogDetail> {
|
||
let index = usize::try_from(self.quest_log_selection.checked_sub(1)?).ok()?;
|
||
self.quest_log.entries.get(index)?.detail.as_ref()
|
||
}
|
||
}
|
||
|
||
impl Model {
|
||
/// A named font object, matched case-insensitively (ASCII) as the client's `SStrCmpI` does.
|
||
pub(crate) fn font_object(&self, name: &str) -> Option<&FontObject> {
|
||
if name.bytes().any(|b| b.is_ascii_uppercase()) {
|
||
self.font_objects_by_lower.get(&name.to_ascii_lowercase())
|
||
} else {
|
||
self.font_objects_by_lower.get(name)
|
||
}
|
||
}
|
||
|
||
/// Records a caught script error for the host, the retained log and the Lua error handler: `log`
|
||
/// is the decorated text the host and the retained log keep, `handler` the one value the error
|
||
/// handler is called with. An error raised by the handler itself goes to `errors` directly,
|
||
/// which stops the recursion.
|
||
pub(crate) fn record_script_error(&mut self, log: String, handler: String) {
|
||
self.diagnostics
|
||
.record(super::diagnostics::DiagnosticKind::Error, &log);
|
||
self.pending_error_dispatch.push(handler);
|
||
self.errors.push(log);
|
||
}
|
||
|
||
/// [`Self::record_script_error`] for an error mlua caught: the log keeps mlua's `Display` (its
|
||
/// word and traceback), the handler gets the bare Lua message `lua_pcall` leaves (`0x703b40`).
|
||
pub(crate) fn record_lua_error(&mut self, e: &mlua::Error) {
|
||
self.record_script_error(e.to_string(), super::stdlib::lua_message(e));
|
||
}
|
||
|
||
/// Records a non-fatal warning for the host's terminal and the retained diagnostic log.
|
||
pub(crate) fn record_warning(&mut self, msg: impl Into<String>) {
|
||
let msg = msg.into();
|
||
self.diagnostics
|
||
.record(super::diagnostics::DiagnosticKind::Warning, &msg);
|
||
self.warnings.push(msg);
|
||
}
|
||
|
||
/// A warning for the host's terminal only, for a message already retained under another kind.
|
||
pub(crate) fn warn_host_only(&mut self, msg: String) {
|
||
self.warnings.push(msg);
|
||
}
|
||
|
||
/// The state of the unit `token` names, as the `Unit*` getters read it: the snapshot the app
|
||
/// pushed under the token, else, for a token with a `target` hop, the chain's end.
|
||
pub(crate) fn unit(&self, token: &str) -> Option<&UnitState> {
|
||
self.pushed_unit(token).or_else(|| self.chained_unit(token))
|
||
}
|
||
|
||
/// The snapshot pushed under `token` itself, folded as 1.12's resolver `0x515970` folds
|
||
/// (`_strnicmp`: `A`..`Z` only, never a byte of 0x80 or more), so `to_ascii_lowercase`, never
|
||
/// `to_lowercase`.
|
||
pub(crate) fn pushed_unit(&self, token: &str) -> Option<&UnitState> {
|
||
if token.bytes().any(|b| b.is_ascii_uppercase()) {
|
||
self.units_by_lower.get(&token.to_ascii_lowercase())
|
||
} else {
|
||
self.units_by_lower.get(token)
|
||
}
|
||
}
|
||
|
||
/// The unit a token with a `target` hop and no push of its own names: the resolver's guid
|
||
/// (`0x515970`, which the getters call before their own object lookup, `0x468460`), then that
|
||
/// guid's snapshot. A token without a hop is the app's to push, base by base, and reads
|
||
/// nothing here.
|
||
fn chained_unit(&self, token: &str) -> Option<&UnitState> {
|
||
match parse_unit_token(token) {
|
||
UnitTokenParse::Unit { base, hops } if hops > 0 => {
|
||
let guid = self.unit_guids.resolve_unit(base, hops)?;
|
||
self.units_by_guid.get(&guid)
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn new() -> Model {
|
||
Model {
|
||
vm: super::glue::VmKind::InGame,
|
||
addons: Vec::new(),
|
||
addon_index: Vec::new(),
|
||
addon_info_hidden: None,
|
||
addon_enable: Default::default(),
|
||
addon_enable_saved: Default::default(),
|
||
addons_root: None,
|
||
addons_chain_reader: None,
|
||
measurer: None,
|
||
texture_probe: None,
|
||
world_loc_projector: None,
|
||
texture_size_probe: None,
|
||
font_probe: None,
|
||
addons_saved_account: None,
|
||
addons_saved_character: None,
|
||
framexml_templates: Default::default(),
|
||
exec_context: Default::default(),
|
||
framexml_debug: Default::default(),
|
||
load_log: Default::default(),
|
||
framexml_fonts: Default::default(),
|
||
arena: WidgetArena::new(),
|
||
layout_inputs: HashMap::new(),
|
||
solver: LayoutSolver::new(),
|
||
layout_fingerprint: None,
|
||
layout_epoch: 0,
|
||
layout_touched: None,
|
||
measure_dirty: None,
|
||
msg_swept: std::collections::HashMap::new(),
|
||
msg_lines_hashed: 0,
|
||
layout_verify_recheck: false,
|
||
layout_derives: 0,
|
||
layout_scope: super::layout::LayoutScope::default(),
|
||
layout_last_scope: (0, 0),
|
||
layout_epoch_resolved: None,
|
||
layout_solves: 0,
|
||
layout_gate_walks: 0,
|
||
layout_rounds: 0,
|
||
resolved: HashMap::new(),
|
||
link_buttons: HashMap::new(),
|
||
region_data: HashMap::new(),
|
||
backdrops: HashMap::new(),
|
||
simple_html: simplehtml::SimpleHtmlStates::new(),
|
||
font_objects_by_lower: HashMap::new(),
|
||
region_resolved: HashMap::new(),
|
||
region_kept: HashMap::new(),
|
||
next_id: 1,
|
||
id_to_frame: IdMap::default(),
|
||
frame_to_id: HashMap::new(),
|
||
id_to_region: IdMap::default(),
|
||
region_to_id: HashMap::new(),
|
||
region_by_name: HashMap::new(),
|
||
scripts: HashMap::new(),
|
||
on_update_frames: Vec::new(),
|
||
on_update_joins: 0,
|
||
on_size_changed_frames: Vec::new(),
|
||
on_update_model_frames: Vec::new(),
|
||
model_facts: HashMap::new(),
|
||
model_facts_wanted: Vec::new(),
|
||
event_to_frames: HashMap::new(),
|
||
frame_events: HashMap::new(),
|
||
all_event_frames: Vec::new(),
|
||
focused_editbox: None,
|
||
alt_down: false,
|
||
key_consumers: HashMap::new(),
|
||
mouseover: None,
|
||
hover_repick: false,
|
||
world_mouseover: None,
|
||
minimap_blip: None,
|
||
mouse_down_on: HashMap::new(),
|
||
mouse_capture: None,
|
||
last_click: HashMap::new(),
|
||
pending_size_changed: Vec::new(),
|
||
text_flips: Vec::new(),
|
||
layout_force: Vec::new(),
|
||
layout_pending: Vec::new(),
|
||
layout_unsettled: HashSet::new(),
|
||
layout_hold: Vec::new(),
|
||
layout_pass: Vec::new(),
|
||
layout_pull: Vec::new(),
|
||
layout_chain: None,
|
||
layout_screen: None,
|
||
errors: Vec::new(),
|
||
pending_error_dispatch: Vec::new(),
|
||
diagnostics: Default::default(),
|
||
warnings: Vec::new(),
|
||
// 1024x768 until the host calls `set_screen_size`; y-up `[bottom, left, top, right]`.
|
||
screen: Rect::new(0.0, 0.0, 768.0, 1024.0),
|
||
ui_scale_request: None,
|
||
units_by_lower: HashMap::new(),
|
||
units_by_guid: HashMap::new(),
|
||
player_auras: Vec::new(),
|
||
unit_auras: HashMap::new(),
|
||
unit_guids: Default::default(),
|
||
cancel_aura_requests: Vec::new(),
|
||
tracking: None,
|
||
script_calls: Vec::new(),
|
||
input: Default::default(),
|
||
drop_item_on_unit: Vec::new(),
|
||
joined_channels: Vec::new(),
|
||
party: party::PartyState::default(),
|
||
party_requests: Vec::new(),
|
||
ready_check: party::ReadyCheckState::default(),
|
||
saved_instances: Vec::new(),
|
||
raid_selection: 0,
|
||
now: 0.0,
|
||
game_time: (0, 0),
|
||
zone: Default::default(),
|
||
instance_type: None,
|
||
can_reset_instances: false,
|
||
reset_instance_asks: 0,
|
||
social: social::SocialState::default(),
|
||
social_requests: Vec::new(),
|
||
lfg_slots: [0; 3],
|
||
lfg_comment: String::new(),
|
||
guild: guild::GuildState::default(),
|
||
guild_control: guild::GuildRankEdit::default(),
|
||
guild_requests: Vec::new(),
|
||
petition: petition::PetitionState::default(),
|
||
petition_requests: Vec::new(),
|
||
// Per window, as the stock dock differs: 1 and 2 are the dock, the rest undocked.
|
||
chat_window_looks: std::array::from_fn(chat_window::ChatWindowLook::stock),
|
||
chat_window_changes: HashSet::new(),
|
||
chat_colors: super::chat_types::seed(),
|
||
chat_colors_changed: false,
|
||
known_languages: Vec::new(),
|
||
language_table: Vec::new(),
|
||
zone_channel_catalog: Vec::new(),
|
||
channel_commands: Vec::new(),
|
||
guild_recruitment_mode: 1,
|
||
guild_recruitment_cascade: false,
|
||
guild_recruitment_changed: false,
|
||
emote_requests: Vec::new(),
|
||
roll_requests: Vec::new(),
|
||
uninvite_requests: Vec::new(),
|
||
console_lines: Vec::new(),
|
||
logging_chat: false,
|
||
logging_combat: false,
|
||
logging_changed: false,
|
||
user_placed_changed: false,
|
||
default_language: None,
|
||
duel_requests: Vec::new(),
|
||
follow_requests: Vec::new(),
|
||
camera_view_requests: Vec::new(),
|
||
session_requests: Vec::new(),
|
||
glue: Default::default(),
|
||
player_login_armed: false,
|
||
pvp_toggles: 0,
|
||
honor: None,
|
||
inspect_honor: None,
|
||
inspect_honor_requests: 0,
|
||
inspect_honor_pending: false,
|
||
helm_shown: true,
|
||
cloak_shown: true,
|
||
worn_display_toggles: Vec::new(),
|
||
action_bar_toggles: None,
|
||
action_bar_toggle_sends: Vec::new(),
|
||
sound_queue: Vec::new(),
|
||
music_queue: Vec::new(),
|
||
sound_suppression: 0,
|
||
cvars: HashMap::new(),
|
||
cvars_saved_base: HashMap::new(),
|
||
cvar_changes: Vec::new(),
|
||
cvar_registrations: Vec::new(),
|
||
cvars_warned: HashSet::new(),
|
||
cvars_read_only: HashSet::new(),
|
||
multisample_formats: Vec::new(),
|
||
screen_resolutions: Vec::new(),
|
||
current_resolution: None,
|
||
video_caps: super::cvars::VideoCaps::default(),
|
||
restart_gx_asks: 0,
|
||
saved_names: Vec::new(),
|
||
held_saved_files: Vec::new(),
|
||
keybinds: super::keybind::KeybindState::default(),
|
||
actions: HashMap::new(),
|
||
action_states: HashMap::new(),
|
||
bonus_bar_offset: 0,
|
||
action_sets: Vec::new(),
|
||
ui_errors: Vec::new(),
|
||
spellbook: spellbook::SpellBookState::default(),
|
||
pet_book: spellbook::PetBookState::default(),
|
||
macros: macros::MacroState::default(),
|
||
macros_dirty: false,
|
||
macros_generation: 0,
|
||
macro_bindings: HashMap::new(),
|
||
macro_icons: Vec::new(),
|
||
pet_spell_autocasts: Vec::new(),
|
||
casting: false,
|
||
spell_targeting: false,
|
||
spell_targetable_units: HashSet::new(),
|
||
talents: super::talent::TalentUiState::default(),
|
||
talent_learns: Vec::new(),
|
||
talent_wipe_confirms: 0,
|
||
talent_master_pending: false,
|
||
pet_unlearn_confirms: 0,
|
||
pet_untrainer_pending: false,
|
||
instance_boot_secs: 0,
|
||
area_spirit_healer_cached: false,
|
||
area_spirit_secs: 0,
|
||
area_spirit_accepts: 0,
|
||
battlefield_port_requests: Vec::new(),
|
||
battlefield_board: Default::default(),
|
||
battlefield_run_time_ms: 0,
|
||
battlefield_score_requests: 0,
|
||
battlefield_leave_requests: 0,
|
||
battlefield_list: Default::default(),
|
||
battlefield_selected: 0,
|
||
battlefield_slots: Default::default(),
|
||
battlefield_instance_expiration_ms: 0,
|
||
battlefield_join_requests: Vec::new(),
|
||
battlefield_list_requests: Vec::new(),
|
||
battlefield_positions: Vec::new(),
|
||
battlefield_flag: None,
|
||
battlefield_icon_scale: 1.0,
|
||
battlefield_position_requests: 0,
|
||
meeting_stone_cancels: 0,
|
||
meeting_stone_area: 0,
|
||
meeting_stone_status_text: None,
|
||
tutorial_bank: None,
|
||
tutorial_flag_requests: Vec::new(),
|
||
tutorial_clears: 0,
|
||
tutorial_resets: 0,
|
||
shapeshift_forms: Vec::new(),
|
||
pet_bar: super::pet::PetBarState::default(),
|
||
pet_autocast_toggles: Vec::new(),
|
||
pet_stop_attacks: 0,
|
||
player_control: true,
|
||
pet_set_actions: Vec::new(),
|
||
pet_abandons: 0,
|
||
pet_dismisses: 0,
|
||
pet_renames: Vec::new(),
|
||
containers: HashMap::new(),
|
||
container_cooldowns: HashMap::new(),
|
||
has_key: false,
|
||
cursor: None,
|
||
cursor_grid: false,
|
||
world_pick: cursor::WorldPick::default(),
|
||
container_moves: Vec::new(),
|
||
container_repairs: Vec::new(),
|
||
item_pick_armed: false,
|
||
item_picks: Vec::new(),
|
||
enchant_confirms: Vec::new(),
|
||
container_destroys: Vec::new(),
|
||
pending_wrap: None,
|
||
container_wraps: Vec::new(),
|
||
ui_cursor: None,
|
||
ui_cursor_dirty: false,
|
||
container_autoequips: Vec::new(),
|
||
bag_autostores: Vec::new(),
|
||
drag_registered: HashMap::new(),
|
||
drag: None,
|
||
moving: None,
|
||
layout_mode: 0,
|
||
slider_drag: None,
|
||
color_drag: None,
|
||
gossip: None,
|
||
gossip_selects: Vec::new(),
|
||
gossip_close: false,
|
||
gossip_quest_selects: Vec::new(),
|
||
merchant: None,
|
||
repair_all_cost: 0,
|
||
repair_costs: Default::default(),
|
||
merchant_buys: Vec::new(),
|
||
merchant_cursor_sells: Vec::new(),
|
||
merchant_slot_buys: Vec::new(),
|
||
merchant_close: false,
|
||
merchant_buybacks: Vec::new(),
|
||
repair_all: false,
|
||
repair_mode: false,
|
||
stable: Default::default(),
|
||
bank: None,
|
||
bank_purchase: false,
|
||
bank_close: false,
|
||
trainer: None,
|
||
trainer_buys: Vec::new(),
|
||
trainer_selection: None,
|
||
trainer_close: false,
|
||
trainer_filter: [true; 3],
|
||
trainer_collapsed: HashSet::new(),
|
||
taxi: None,
|
||
taxi_takes: Vec::new(),
|
||
taxi_close: false,
|
||
taxi_riding: false,
|
||
trade_skill: None,
|
||
trade_skill_dos: Vec::new(),
|
||
trade_skill_selection: 0,
|
||
trade_skill_close: false,
|
||
trade_skill_collapsed: HashSet::new(),
|
||
trade_skill_subclass_hidden: HashSet::new(),
|
||
trade_skill_invslot_mask: u32::MAX,
|
||
trade_skill_last_line: 0,
|
||
trade_skill_selected_spell: 0,
|
||
trade_skill_touched: false,
|
||
craft: None,
|
||
craft_dos: Vec::new(),
|
||
craft_selection: 0,
|
||
craft_close: false,
|
||
loot: None,
|
||
pending_equip_answers: Vec::new(),
|
||
bind_on_use_confirms: 0,
|
||
loot_picks: Vec::new(),
|
||
loot_confirms: Vec::new(),
|
||
loot_close: false,
|
||
loot_master_gives: Vec::new(),
|
||
loot_rolls: loot_roll::LootRollsState::default(),
|
||
loot_roll_votes: Vec::new(),
|
||
loot_roll_confirms: Vec::new(),
|
||
item_text: None,
|
||
item_text_close: false,
|
||
item_text_page_turns: Vec::new(),
|
||
mail: None,
|
||
mail_check_inbox: false,
|
||
mail_opens: Vec::new(),
|
||
mail_take_items: Vec::new(),
|
||
mail_take_money: Vec::new(),
|
||
mail_deletes: Vec::new(),
|
||
mail_returns: Vec::new(),
|
||
mail_take_texts: Vec::new(),
|
||
mail_close: false,
|
||
mail_send: None,
|
||
mail_send_money: 0,
|
||
mail_send_cod: 0,
|
||
mail_send_item: None,
|
||
mail_stationeries: Vec::new(),
|
||
mail_stationery: 0,
|
||
mail_packages: Vec::new(),
|
||
mail_package: 0,
|
||
has_new_mail: false,
|
||
auction: None,
|
||
auction_item_classes: Vec::new(),
|
||
auction_selected: [0; 3],
|
||
auction_can_query: true,
|
||
auction_query: None,
|
||
auction_owner_query: None,
|
||
auction_bidder_query: None,
|
||
auction_bids: Vec::new(),
|
||
auction_cancels: Vec::new(),
|
||
auction_start: None,
|
||
auction_sorts: Vec::new(),
|
||
auction_close: false,
|
||
auction_sell_item: None,
|
||
trade: None,
|
||
trade_initiates: Vec::new(),
|
||
trade_accept: false,
|
||
trade_unaccept: false,
|
||
trade_close: false,
|
||
trade_begin: false,
|
||
trade_cancel: false,
|
||
trade_set_money: None,
|
||
trade_set_items: Vec::new(),
|
||
trade_clear_items: Vec::new(),
|
||
quest: None,
|
||
quest_selects: Vec::new(),
|
||
quest_actions: Vec::new(),
|
||
death: death::DeathUiState::default(),
|
||
death_actions: Vec::new(),
|
||
quest_log: quest_log::QuestLogState::default(),
|
||
quest_log_selection: 0,
|
||
quest_log_abandon_mark: 0,
|
||
quest_log_abandons: Vec::new(),
|
||
quest_log_pushes: Vec::new(),
|
||
quest_confirms: 0,
|
||
item_templates: HashMap::new(),
|
||
item_sets: HashMap::new(),
|
||
item_set_asks: HashSet::new(),
|
||
random_properties: HashMap::new(),
|
||
item_stat_asks: HashSet::new(),
|
||
player_req: PlayerReqState::default(),
|
||
spell_tooltips: HashMap::new(),
|
||
spell_tooltip_asks: HashSet::new(),
|
||
pet_spell_tooltips: HashMap::new(),
|
||
pet_spell_tooltip_asks: HashSet::new(),
|
||
spell_tooltip_waits: HashMap::new(),
|
||
quest_log_collapses: Vec::new(),
|
||
quest_log_watched: Vec::new(),
|
||
server_unix_time: None,
|
||
worldmap: super::worldmap::WorldMapState::default(),
|
||
nameplates: super::nameplate::NamePlates::default(),
|
||
chat_bubbles: super::chat_bubble::ChatBubbles::default(),
|
||
worldstate: super::worldstate::WorldStateUiState::default(),
|
||
pending_events: Vec::new(),
|
||
verb_events: None,
|
||
cursor_pos: (0.0, 0.0),
|
||
minimap_ping_request: None,
|
||
minimap_ping: (0.0, 0.0),
|
||
chat_sends: Vec::new(),
|
||
addon_sends: Vec::new(),
|
||
played_time_asks: 0,
|
||
opening_cinematic_asks: 0,
|
||
screenshot_asks: 0,
|
||
realm_name: String::new(),
|
||
player_record: super::PlayerRecord::default(),
|
||
bind_location: String::new(),
|
||
gm_ticket_categories: Vec::new(),
|
||
gm_ticket_intents: Vec::new(),
|
||
stuck_casts: 0,
|
||
binder_confirms: 0,
|
||
binder_pending: false,
|
||
summon_confirm: super::summon::SummonConfirmUiState::default(),
|
||
summon_confirms: 0,
|
||
framerate: 0.0,
|
||
modifiers: (false, false, false),
|
||
money: 0,
|
||
net_latency_ms: 0,
|
||
player_xp: 0,
|
||
player_next_level_xp: 0,
|
||
combo_points: 0,
|
||
combo_target: 0,
|
||
rest_state: 2,
|
||
rest_pool: 0,
|
||
resting: false,
|
||
partial_play_time: false,
|
||
no_play_time: false,
|
||
billing_time_rested: 0,
|
||
in_cinematic: false,
|
||
exhaustion: [
|
||
(1, ("Rested".to_string(), 2.0)),
|
||
(2, ("Normal".to_string(), 1.0)),
|
||
(3, ("XXXTired".to_string(), 1.0)),
|
||
(4, ("XXXTired".to_string(), 0.5)),
|
||
(5, ("XXXExhausted".to_string(), 0.25)),
|
||
]
|
||
.into_iter()
|
||
.collect(),
|
||
player_combat_stats: None,
|
||
pet_combat_stats: None,
|
||
inventory_slots: Default::default(),
|
||
bank_bag_slots: Default::default(),
|
||
inventory_alerts: [0; 12],
|
||
inventory_repairs: Vec::new(),
|
||
weapon_enchants: [None; 2],
|
||
inspect: None,
|
||
inspect_notifies: Vec::new(),
|
||
inspect_clear: false,
|
||
dressup_intents: Vec::new(),
|
||
tabard_designs: HashMap::new(),
|
||
tabard_preview: None,
|
||
tabard_host: Default::default(),
|
||
tabard_intents: Vec::new(),
|
||
world_frame_made: false,
|
||
unit_reach: HashMap::new(),
|
||
chat_input: Vec::new(),
|
||
skills: skills::SkillsState::default(),
|
||
skills_groups: Vec::new(),
|
||
skills_collapsed: HashSet::new(),
|
||
skills_selected: None,
|
||
skill_abandons: Vec::new(),
|
||
reputation: reputation::ReputationState::default(),
|
||
reputation_groups: Vec::new(),
|
||
reputation_collapsed: HashSet::new(),
|
||
reputation_selected: None,
|
||
reputation_sends: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Mint (or fetch) the stable id of a frame handle.
|
||
pub(crate) fn frame_id(&mut self, h: FrameHandle) -> u32 {
|
||
if let Some(&id) = self.frame_to_id.get(&h) {
|
||
return id;
|
||
}
|
||
let id = self.next_id;
|
||
self.next_id += 1;
|
||
self.frame_to_id.insert(h, id);
|
||
self.id_to_frame.insert(id, h);
|
||
// Every creation path mints here, so a new frame always joins the graph. A region already
|
||
// a node on it gains its owner edge and the owner's id in its hash, so it re-derives too.
|
||
self.name_layout_node(id);
|
||
let owned: Vec<u32> = self
|
||
.arena
|
||
.frame(h)
|
||
.map(|f| {
|
||
f.regions
|
||
.iter()
|
||
.filter_map(|rh| self.region_to_id.get(rh).copied())
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
for rid in owned {
|
||
self.name_layout_node(rid);
|
||
}
|
||
id
|
||
}
|
||
|
||
/// Mint (or fetch) the stable id of a region handle.
|
||
pub(crate) fn region_id(&mut self, h: RegionHandle) -> u32 {
|
||
if let Some(&id) = self.region_to_id.get(&h) {
|
||
return id;
|
||
}
|
||
let id = self.next_id;
|
||
self.next_id += 1;
|
||
self.region_to_id.insert(h, id);
|
||
self.id_to_region.insert(id, h);
|
||
// Named, so an anchored region joins the graph; an unanchored one re-derives to nothing.
|
||
self.name_layout_node(id);
|
||
id
|
||
}
|
||
|
||
/// `CSimpleFrame::SetScale` (`0x76ac10`): store `h`'s own scale, cascade the effective scale
|
||
/// down its subtree, and re-lay out and re-measure on a change, since the owner's effective
|
||
/// scale is in every descendant FontString's measure key. Lua `SetScale` and the UI scale
|
||
/// ([`super::ui_scale`]) both come here, as both reach `0x76ac10` in the reference.
|
||
pub(crate) fn set_frame_scale(&mut self, h: FrameHandle, scale: f32) {
|
||
let changed = self.arena.frame(h).is_some_and(|f| f.scale != scale);
|
||
self.arena.set_scale(h, scale);
|
||
if changed {
|
||
// The effective scale cascades down the subtree, an input of each frame's node.
|
||
self.touch_layout_subtree(h);
|
||
self.touch_measure_all();
|
||
}
|
||
}
|
||
|
||
/// A layout write that names no node, for what moves every node at once (the screen, every
|
||
/// measure): the next resolve derives the graph in full. The resolve's tooltip pre-pass must not
|
||
/// call this, or the gate never closes; a missed call is caught by `WOW_LAYOUT_VERIFY`.
|
||
pub(crate) fn touch_layout(&mut self) {
|
||
self.give_up_naming();
|
||
self.bump_layout_epoch();
|
||
}
|
||
|
||
/// A write to region `rh`'s layout inputs or liveness: its anchors, size, measured extent,
|
||
/// art or owner. Its node re-derives at the next resolve, joining or leaving the graph as it
|
||
/// gained or lost its anchors. An id is minted only for an anchored region, the only kind that
|
||
/// can be a node; an anchorless one with no id has nothing in the graph to move. Naming
|
||
/// invalidates nothing: a write the reference invalidates for goes through
|
||
/// [`Self::invalidate_region`].
|
||
pub(crate) fn touch_layout_region(&mut self, rh: RegionHandle) {
|
||
match self.region_to_id.get(&rh) {
|
||
Some(&id) => self.name_layout_node(id),
|
||
None if self
|
||
.region_data
|
||
.get(&rh)
|
||
.is_some_and(|d| !d.anchors.is_empty()) =>
|
||
{
|
||
self.region_id(rh); // mints and names
|
||
}
|
||
None => self.bump_layout_epoch(),
|
||
}
|
||
}
|
||
|
||
/// A write to region `rh`'s text, which was non-empty iff `was_shown`: the sweep reads a
|
||
/// FontString's measure only while its text is non-empty
|
||
/// ([`super::layout::effective_measure`]), so a flip between empty and not is noted, and the
|
||
/// next resolve names it if it moved the node's hash. Checked there, not here, so a clear and
|
||
/// rebuild of the same text (the hover tooltip's, every frame) leaves the epoch alone. A
|
||
/// same-side write leaves the layout to the measure ledger.
|
||
pub(crate) fn touch_layout_text(&mut self, rh: RegionHandle, was_shown: bool) {
|
||
let shown = self
|
||
.region_data
|
||
.get(&rh)
|
||
.is_some_and(|d| d.text.as_deref().is_some_and(|t| !t.is_empty()));
|
||
if shown != was_shown {
|
||
self.text_flips.push(rh);
|
||
}
|
||
}
|
||
|
||
/// Text that emptied or filled on anchored region `rh`, which moves the measure its resolve
|
||
/// reads: a new string clears the valid bit and invalidates (`0x771ea4`), unless the node
|
||
/// still holds its converged hash, as when a clear and rebuild restored the same text.
|
||
pub(crate) fn touch_layout_content(&mut self, rh: RegionHandle) {
|
||
let Some(d) = self.region_data.get(&rh) else {
|
||
return;
|
||
};
|
||
if d.anchors.is_empty() {
|
||
return;
|
||
}
|
||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||
let owner = self
|
||
.arena
|
||
.region(rh)
|
||
.and_then(|r| self.frame_to_id.get(&r.owner).copied());
|
||
if self
|
||
.layout_scope
|
||
.holds(id, super::layout::region_node_hash(d, owner))
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
self.clear_region_valid(rh);
|
||
}
|
||
|
||
/// The same for frame `h`: its anchors, size, scale or clamp.
|
||
pub(crate) fn touch_layout_frame(&mut self, h: FrameHandle) {
|
||
match self.frame_to_id.get(&h) {
|
||
Some(&id) => self.name_layout_node(id),
|
||
None if self.arena.frame(h).is_some() => {
|
||
self.frame_id(h); // mints and names
|
||
}
|
||
None => self.bump_layout_epoch(),
|
||
}
|
||
}
|
||
|
||
/// A write the reference invalidates frame `h` for (`0x7680e0`): `SetPoint`, `SetWidth`,
|
||
/// `SetClampedToScreen` and the like, which pass `changed` for whether it moved an input. One
|
||
/// that moved nothing is skipped, as a re-resolve gives back the rect the frame holds, unless
|
||
/// the frame holds one a re-resolve would not give it ([`Self::frame_holds_stale`]): only an
|
||
/// invalidated node, or one whose target stored a new rect, takes a new rect.
|
||
pub(crate) fn invalidate_frame(&mut self, h: FrameHandle, changed: bool) {
|
||
if !changed && !self.frame_holds_stale(h) {
|
||
return;
|
||
}
|
||
self.touch_layout_frame(h);
|
||
if let Some(&id) = self.frame_to_id.get(&h) {
|
||
self.queue_invalidate(id);
|
||
}
|
||
}
|
||
|
||
/// [`Self::invalidate_frame`] for region `rh`.
|
||
pub(crate) fn invalidate_region(&mut self, rh: RegionHandle, changed: bool) {
|
||
if !changed && !self.region_holds_stale(rh) {
|
||
return;
|
||
}
|
||
// A region with no id was never anchored, so it holds no rect to lose.
|
||
self.touch_layout_region(rh);
|
||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||
self.queue_invalidate(id);
|
||
}
|
||
}
|
||
|
||
/// A write that clears region `rh`'s valid bit and invalidates it, as a FontString's new
|
||
/// string (`0x771ea4`), font (`0x7715e0`) or spacing (`0x771666`) does: the next resolve stores
|
||
/// its rect whole, and that store reaches what anchors to it (`0x768cf0`).
|
||
pub(crate) fn clear_region_valid(&mut self, rh: RegionHandle) {
|
||
// A region with no id was never anchored, so it holds no rect.
|
||
self.touch_layout_region(rh);
|
||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||
self.queue_force(id);
|
||
}
|
||
}
|
||
|
||
/// Invalidate node `id` (`0x7680e0(0)`): queued for the next resolve, or, while it is deferred,
|
||
/// held pending, its invalidate passing to what anchors to it once (`0x768169`–`0x76819c`).
|
||
pub(crate) fn queue_invalidate(&mut self, id: u32) {
|
||
if self.node_deferred(id) {
|
||
self.hold_invalidated(id, false);
|
||
} else {
|
||
self.layout_pending.push(id);
|
||
self.hold_set(id, HOLD_QUEUED);
|
||
}
|
||
}
|
||
|
||
/// [`Self::queue_invalidate`] for a write that also clears the node's resolved bit, whose
|
||
/// resolve then stores without the keep test ([`Self::layout_force`]).
|
||
pub(crate) fn queue_force(&mut self, id: u32) {
|
||
if self.node_deferred(id) {
|
||
self.hold_invalidated(id, true);
|
||
} else {
|
||
self.layout_force.push(id);
|
||
self.hold_set(id, HOLD_QUEUED_FORCE);
|
||
}
|
||
}
|
||
|
||
/// A deferred node's invalidate: it sets `0x4` and passes on (`0x768169`–`0x76819c`); one
|
||
/// already pending only writes the drain's retry counter, `+0x38 = 6` (`0x76814b`–`0x768166`).
|
||
pub(super) fn hold_invalidated(&mut self, id: u32, force: bool) {
|
||
if self.hold_flags(id) & HOLD_HELD == 0 {
|
||
self.layout_pass.push(id);
|
||
}
|
||
self.hold_set(id, HOLD_HELD | if force { HOLD_FORCE } else { 0 });
|
||
}
|
||
|
||
/// Node `id`'s `HOLD_*` bits.
|
||
#[inline]
|
||
pub(crate) fn hold_flags(&self, id: u32) -> u8 {
|
||
self.layout_hold.get(id as usize).copied().unwrap_or(0)
|
||
}
|
||
|
||
pub(super) fn hold_set(&mut self, id: u32, bits: u8) {
|
||
let i = id as usize;
|
||
if self.layout_hold.len() <= i {
|
||
self.layout_hold.resize(i + 1, 0);
|
||
}
|
||
self.layout_hold[i] |= bits;
|
||
}
|
||
|
||
/// Whether node `id`'s deferral bit is set.
|
||
pub(crate) fn node_deferred(&self, id: u32) -> bool {
|
||
node_deferred(&self.arena, &self.id_to_frame, &self.id_to_region, id)
|
||
}
|
||
|
||
/// The queued resolve of node `id`, just deferred, leaves the queue and waits with it, pending
|
||
/// (`0x768074`–`0x7680b3`).
|
||
fn hold_queued(&mut self, id: u32) {
|
||
let f = self.hold_flags(id);
|
||
if f & (HOLD_QUEUED | HOLD_QUEUED_FORCE) != 0 {
|
||
let force = if f & HOLD_QUEUED_FORCE != 0 {
|
||
HOLD_FORCE
|
||
} else {
|
||
0
|
||
};
|
||
self.hold_set(id, HOLD_HELD | force);
|
||
}
|
||
}
|
||
|
||
/// `0x768060(1)` on frame `h`'s layout object.
|
||
pub(crate) fn defer_frame(&mut self, h: FrameHandle) {
|
||
let Some(f) = self.arena.frame_mut(h) else {
|
||
return;
|
||
};
|
||
f.layout_deferred = true;
|
||
if let Some(&id) = self.frame_to_id.get(&h) {
|
||
self.hold_queued(id);
|
||
}
|
||
}
|
||
|
||
/// `0x768060(1)` on region `rh`'s layout object.
|
||
pub(crate) fn defer_region(&mut self, rh: RegionHandle) {
|
||
let Some(r) = self.arena.region_mut(rh) else {
|
||
return;
|
||
};
|
||
r.layout_deferred = true;
|
||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||
self.hold_queued(id);
|
||
}
|
||
}
|
||
|
||
/// `0x768060(0)` on frame `h`'s layout object: clear the bit, and a pending resolve runs now
|
||
/// (`0x7680c4` → `0x7680e0(1)`), here at the next resolve. Whether one is owed.
|
||
pub(crate) fn undefer_frame(&mut self, h: FrameHandle) -> bool {
|
||
let Some(f) = self.arena.frame_mut(h) else {
|
||
return false;
|
||
};
|
||
f.layout_deferred = false;
|
||
match self.frame_to_id.get(&h) {
|
||
Some(&id) => self.pull_pending(id),
|
||
None => false,
|
||
}
|
||
}
|
||
|
||
/// `0x768060(0)` on region `rh`'s layout object.
|
||
pub(crate) fn undefer_region(&mut self, rh: RegionHandle) -> bool {
|
||
let Some(r) = self.arena.region_mut(rh) else {
|
||
return false;
|
||
};
|
||
r.layout_deferred = false;
|
||
match self.region_to_id.get(&rh) {
|
||
Some(&id) => self.pull_pending(id),
|
||
None => false,
|
||
}
|
||
}
|
||
|
||
/// Node `id` resolves at the next resolve if it is pending, held or queued; whether it is.
|
||
pub(crate) fn pull_pending(&mut self, id: u32) -> bool {
|
||
let f = self.hold_flags(id);
|
||
if f & HOLD_HELD != 0 {
|
||
self.layout_pull.push(id);
|
||
self.name_layout_node(id);
|
||
}
|
||
f & (HOLD_HELD | HOLD_QUEUED | HOLD_QUEUED_FORCE) != 0
|
||
}
|
||
|
||
/// A getter's read of node `id` (`0x7a1a0b` → `0x7680e0(1)` while pending): a held node
|
||
/// resolves in the read's resolve, and its bit stays set.
|
||
pub(crate) fn pull_read(&mut self, id: u32) {
|
||
if self.hold_flags(id) & HOLD_HELD != 0 && self.node_deferred(id) {
|
||
self.layout_pull.push(id);
|
||
self.name_layout_node(id);
|
||
}
|
||
}
|
||
|
||
/// The hide transition's layout step (`0x76ad50`): out of its load window, the frame and its
|
||
/// title region defer (`0x76ad70`, `0x76ad84`), and each region it drew stops drawing and defers
|
||
/// (`0x76adbf` → `0x77fc82`).
|
||
pub(crate) fn layout_on_hide(&mut self, h: FrameHandle) {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return;
|
||
};
|
||
if f.load_window {
|
||
return;
|
||
}
|
||
let title = f.title_region;
|
||
let regions: Vec<RegionHandle> = f.regions.clone();
|
||
self.defer_frame(h);
|
||
if let Some(t) = title {
|
||
self.defer_region(t);
|
||
}
|
||
// The frame was visible until this transition, so each shown region drew.
|
||
for rh in regions {
|
||
if Some(rh) != title && self.region_shown_bit(rh) {
|
||
self.defer_region(rh);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The show transition's layout step for the frame (`0x76ae10`): out of its load window, the
|
||
/// frame and its title region undefer, resolving first (`0x76ae58`, `0x76ae6c`). Whether
|
||
/// either owes a resolve now, or an invalidate passed while deferred is yet to reach what it
|
||
/// may.
|
||
pub(crate) fn layout_on_show_frame(&mut self, h: FrameHandle) -> bool {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return false;
|
||
};
|
||
if f.load_window {
|
||
return false;
|
||
}
|
||
let title = f.title_region;
|
||
let mut owed = self.undefer_frame(h);
|
||
if let Some(t) = title {
|
||
owed |= self.undefer_region(t);
|
||
}
|
||
owed || !self.layout_pass.is_empty()
|
||
}
|
||
|
||
/// The show transition's region step, after the frame is marked visible (`0x76aeac` →
|
||
/// `0x77fcb0`): out of the frame's load window, each shown region starts drawing and
|
||
/// undefers (`0x77fce6`). Whether any owes a resolve now, as [`Self::layout_on_show_frame`].
|
||
pub(crate) fn layout_on_show_regions(&mut self, h: FrameHandle) -> bool {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return false;
|
||
};
|
||
if f.load_window {
|
||
return false;
|
||
}
|
||
let title = f.title_region;
|
||
let regions: Vec<RegionHandle> = f.regions.clone();
|
||
let mut owed = false;
|
||
for rh in regions {
|
||
if Some(rh) != title && self.region_shown_bit(rh) {
|
||
owed |= self.undefer_region(rh);
|
||
}
|
||
}
|
||
owed || !self.layout_pass.is_empty()
|
||
}
|
||
|
||
/// Both show steps at once, for a show with no handler to run between them.
|
||
pub(crate) fn layout_on_show(&mut self, h: FrameHandle) -> bool {
|
||
let frame = self.layout_on_show_frame(h);
|
||
self.layout_on_show_regions(h) || frame
|
||
}
|
||
|
||
/// Whether a resolve of `roots` now can fire `OnSizeChanged`: a root frame has the handler, or
|
||
/// a deferred, pending frame an anchor read on the way would resolve (`0x7a2fc0`) does. With no
|
||
/// such handler the resolve shows nothing in the binding, and the next resolve, a getter's or
|
||
/// the drain's, gives the same rects.
|
||
pub(crate) fn resolve_fires(&self, roots: &[u32]) -> bool {
|
||
let fires = |f: FrameHandle| {
|
||
self.scripts
|
||
.get(&f)
|
||
.is_some_and(|s| s.contains("OnSizeChanged"))
|
||
};
|
||
let anchors = |id: u32| -> Option<crate::layout::Anchors> {
|
||
match self.id_to_frame.get(&id) {
|
||
Some(h) => self.layout_inputs.get(h).map(|i| i.anchors),
|
||
None => self
|
||
.id_to_region
|
||
.get(&id)
|
||
.and_then(|rh| self.region_data.get(rh))
|
||
.map(|d| d.anchors),
|
||
}
|
||
};
|
||
let mut stack: Vec<u32> = Vec::new();
|
||
for &id in roots {
|
||
if self.id_to_frame.get(&id).is_some_and(|&h| fires(h)) {
|
||
return true;
|
||
}
|
||
stack.extend(
|
||
anchors(id)
|
||
.iter()
|
||
.flat_map(|a| a.iter().map(|a| a.relative_to)),
|
||
);
|
||
}
|
||
let mut seen: Vec<u32> = Vec::new();
|
||
while let Some(t) = stack.pop() {
|
||
if seen.contains(&t) || self.hold_flags(t) & HOLD_HELD == 0 || !self.node_deferred(t) {
|
||
continue;
|
||
}
|
||
seen.push(t);
|
||
if self.id_to_frame.get(&t).is_some_and(|&th| fires(th)) {
|
||
return true;
|
||
}
|
||
stack.extend(
|
||
anchors(t)
|
||
.iter()
|
||
.flat_map(|a| a.iter().map(|a| a.relative_to)),
|
||
);
|
||
}
|
||
false
|
||
}
|
||
|
||
/// The layout ids [`Self::layout_on_show_frame`] undeferred: the frame and its title region.
|
||
pub(crate) fn show_frame_roots(&self, h: FrameHandle) -> Vec<u32> {
|
||
let mut out: Vec<u32> = self.frame_to_id.get(&h).copied().into_iter().collect();
|
||
if let Some(t) = self.arena.frame(h).and_then(|f| f.title_region) {
|
||
out.extend(self.region_to_id.get(&t).copied());
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The layout ids [`Self::layout_on_show_regions`] undeferred: the shown regions.
|
||
pub(crate) fn show_region_roots(&self, h: FrameHandle) -> Vec<u32> {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return Vec::new();
|
||
};
|
||
f.regions
|
||
.iter()
|
||
.filter(|&&rh| Some(rh) != f.title_region && self.region_shown_bit(rh))
|
||
.filter_map(|rh| self.region_to_id.get(rh).copied())
|
||
.collect()
|
||
}
|
||
|
||
/// Whether node `id` is pending (`0x4`, [`Self::layout_hold`]): queued, or held under its
|
||
/// deferral.
|
||
pub(crate) fn node_pending(&self, id: u32) -> bool {
|
||
self.hold_flags(id) & (HOLD_HELD | HOLD_QUEUED | HOLD_QUEUED_FORCE) != 0
|
||
}
|
||
|
||
/// The arena's handler-free `Show`/`Hide` ([`WidgetArena::set_shown`]) with each transition's
|
||
/// layout step; a resolve the shows owe runs at the next resolve.
|
||
pub(crate) fn set_shown_silent(&mut self, h: FrameHandle, shown: bool) {
|
||
for f in self.arena.set_shown(h, shown) {
|
||
if shown {
|
||
self.layout_on_show(f);
|
||
} else {
|
||
self.layout_on_hide(f);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The pre-load hook's step (`0x769770`): the frame, its title region and every region defer
|
||
/// (`0x7697cc`, `0x7697e0`, `0x769807`).
|
||
pub(crate) fn layout_load_begin(&mut self, h: FrameHandle) {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return;
|
||
};
|
||
let regions: Vec<RegionHandle> = f.regions.clone();
|
||
self.defer_frame(h);
|
||
for rh in regions {
|
||
if self.arena.region(rh).is_some_and(|r| !r.detached) {
|
||
self.defer_region(rh);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The post-load hook's step (`0x76a2f0`): a visible frame undefers, with its title region and
|
||
/// each region it draws (`0x76a310`, `0x76a324`, `0x76a35b`). A hidden one stays deferred.
|
||
/// Whether any owes a resolve now, as [`Self::layout_on_show_frame`]; the caller runs it
|
||
/// before the frame's `<Frames>` are built.
|
||
pub(crate) fn layout_load_end(&mut self, h: FrameHandle) -> bool {
|
||
let Some(f) = self.arena.frame(h) else {
|
||
return false;
|
||
};
|
||
if !f.effective_visible {
|
||
return false;
|
||
}
|
||
let title = f.title_region;
|
||
let regions: Vec<RegionHandle> = f.regions.clone();
|
||
let mut owed = self.undefer_frame(h);
|
||
if let Some(t) = title {
|
||
owed |= self.undefer_region(t);
|
||
}
|
||
for rh in regions {
|
||
if Some(rh) != title && self.region_draws(rh) {
|
||
owed |= self.undefer_region(rh);
|
||
}
|
||
}
|
||
owed || !self.layout_pass.is_empty()
|
||
}
|
||
|
||
/// The layout ids [`Self::layout_load_end`] undeferred: the frame, its title region and the
|
||
/// regions it draws.
|
||
pub(crate) fn load_end_roots(&self, h: FrameHandle) -> Vec<u32> {
|
||
let mut out = self.show_frame_roots(h);
|
||
out.extend(self.show_region_roots(h));
|
||
out
|
||
}
|
||
|
||
/// The region's own shown bit (`+0xc4`): not hidden, and linked to its owner.
|
||
fn region_shown_bit(&self, rh: RegionHandle) -> bool {
|
||
self.arena.region(rh).is_some_and(|r| !r.detached)
|
||
&& self.region_data.get(&rh).is_none_or(|d| !d.hidden)
|
||
}
|
||
|
||
/// Whether the region draws (`+0xc8`): shown, on a visible owner.
|
||
pub(crate) fn region_draws(&self, rh: RegionHandle) -> bool {
|
||
self.region_shown_bit(rh)
|
||
&& self
|
||
.arena
|
||
.region(rh)
|
||
.and_then(|r| self.arena.frame(r.owner))
|
||
.is_some_and(|f| f.effective_visible)
|
||
}
|
||
|
||
/// Write region `rh`'s shown bit (`Show`/`Hide` and the engine's writes of `+0xc4`), with the
|
||
/// layout step of its start or stop drawing: out of the owner's load window it undefers
|
||
/// (`0x77fcb0` → `0x77fce6`) or defers (`0x77fc60` → `0x77fc82`).
|
||
pub(crate) fn set_region_hidden(&mut self, rh: RegionHandle, hidden: bool) {
|
||
let was = self.region_draws(rh);
|
||
self.region_data.entry(rh).or_default().hidden = hidden;
|
||
self.note_region_draw(rh, was);
|
||
}
|
||
|
||
/// The layout step of a write to region `rh`'s shown bit that `set_region_hidden` could not
|
||
/// wrap, given whether it drew before.
|
||
pub(crate) fn note_region_draw(&mut self, rh: RegionHandle, drew: bool) {
|
||
let now = self.region_draws(rh);
|
||
if drew == now || self.region_owner_loading(rh) {
|
||
return;
|
||
}
|
||
if now {
|
||
self.undefer_region(rh);
|
||
} else {
|
||
self.defer_region(rh);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn region_owner_loading(&self, rh: RegionHandle) -> bool {
|
||
self.arena
|
||
.region(rh)
|
||
.and_then(|r| self.arena.frame(r.owner))
|
||
.is_some_and(|f| f.load_window)
|
||
}
|
||
|
||
/// The layout step of region `rh`'s re-link (`0x77fd10`), given whether it drew under its old
|
||
/// owner, whether that owner was in its load window and whether the owner changed: the new
|
||
/// owner's bit is copied (`0x77fd1e`–`0x77fd28`), a stop on the old owner defers (`0x77fd47` →
|
||
/// `0x77fc60`), and a start on the new one undefers (`0x77fd8f` → `0x77fcb0`). A detach copies
|
||
/// nothing and only stops.
|
||
///
|
||
/// `shown` is the link's shown argument, written here; `None` keeps the bit, as
|
||
/// `Region:SetParent` passes the region's own (`0x7733d0`).
|
||
pub(crate) fn relink_region_layout(
|
||
&mut self,
|
||
rh: RegionHandle,
|
||
drew: bool,
|
||
old_loading: bool,
|
||
moved: bool,
|
||
shown: Option<bool>,
|
||
) {
|
||
let Some(r) = self.arena.region(rh) else {
|
||
return;
|
||
};
|
||
if r.detached {
|
||
if let Some(shown) = shown {
|
||
self.region_data.entry(rh).or_default().hidden = !shown;
|
||
}
|
||
if drew && !old_loading {
|
||
self.defer_region(rh);
|
||
}
|
||
return;
|
||
}
|
||
let owner_bit = self.arena.frame(r.owner).is_some_and(|f| f.layout_deferred);
|
||
if owner_bit {
|
||
self.defer_region(rh);
|
||
} else {
|
||
self.undefer_region(rh);
|
||
}
|
||
if moved && drew && !old_loading {
|
||
self.defer_region(rh);
|
||
}
|
||
if let Some(shown) = shown {
|
||
self.region_data.entry(rh).or_default().hidden = !shown;
|
||
}
|
||
let loading = self.region_owner_loading(rh);
|
||
if self.region_draws(rh) {
|
||
if !loading {
|
||
self.undefer_region(rh);
|
||
}
|
||
} else if !moved && drew && !loading {
|
||
self.defer_region(rh);
|
||
}
|
||
}
|
||
|
||
/// A write that moved region `rh`'s layout inputs and invalidated nothing (a re-link into
|
||
/// another frame, `0x77fd10`; a dying target's unhook, `0x767e70`): its node re-derives, and
|
||
/// it keeps its state until the resolve reaches it.
|
||
pub(crate) fn unsettle_region(&mut self, rh: RegionHandle) {
|
||
self.touch_layout_region(rh);
|
||
if let Some(&id) = self.region_to_id.get(&rh) {
|
||
self.layout_unsettled.insert(id);
|
||
}
|
||
}
|
||
|
||
/// [`Self::unsettle_region`] for frame `h`.
|
||
pub(crate) fn unsettle_frame(&mut self, h: FrameHandle) {
|
||
self.touch_layout_frame(h);
|
||
if let Some(&id) = self.frame_to_id.get(&h) {
|
||
self.layout_unsettled.insert(id);
|
||
}
|
||
}
|
||
|
||
/// New art on region `rh` (`SetTexture` `0x770200`, the colour form `0x770360`): it marks
|
||
/// only the draw dirty (`0x77fdf0`), so the layout is not touched. An anchored texture with an
|
||
/// axis sized 0 reads its art in its next reaching resolve, so until then it holds a state
|
||
/// that resolve need not give back.
|
||
pub(crate) fn note_art(&mut self, rh: RegionHandle) {
|
||
let Some(&id) = self.region_to_id.get(&rh) else {
|
||
return;
|
||
};
|
||
if self.region_data.get(&rh).is_some_and(|d| {
|
||
!d.anchors.is_empty() && d.size.is_none_or(|(w, h)| w == 0.0 || h == 0.0)
|
||
}) {
|
||
self.layout_unsettled.insert(id);
|
||
}
|
||
}
|
||
|
||
/// Whether frame `h` holds a state its inputs would not resolve to now: a write moved them
|
||
/// without invalidating it ([`Self::layout_unsettled`]), or it holds a rect with no anchors
|
||
/// or reading a target with no rect, which stands only because nothing invalidated it since.
|
||
pub(crate) fn frame_holds_stale(&self, h: FrameHandle) -> bool {
|
||
if self
|
||
.frame_to_id
|
||
.get(&h)
|
||
.is_some_and(|id| self.layout_unsettled.contains(id))
|
||
{
|
||
return true;
|
||
}
|
||
if !self.resolved.contains_key(&h) {
|
||
return false;
|
||
}
|
||
self.layout_inputs
|
||
.get(&h)
|
||
.is_none_or(|i| self.anchors_hold_stale(&i.anchors, false))
|
||
}
|
||
|
||
/// [`Self::frame_holds_stale`] for region `rh`.
|
||
pub(crate) fn region_holds_stale(&self, rh: RegionHandle) -> bool {
|
||
if self
|
||
.region_to_id
|
||
.get(&rh)
|
||
.is_some_and(|id| self.layout_unsettled.contains(id))
|
||
{
|
||
return true;
|
||
}
|
||
if !self.region_resolved.contains_key(&rh) {
|
||
return false;
|
||
}
|
||
self.region_data
|
||
.get(&rh)
|
||
.is_none_or(|d| self.anchors_hold_stale(&d.anchors, true))
|
||
}
|
||
|
||
/// No anchors, or one whose target has no rect; a region reads none at the screen root.
|
||
fn anchors_hold_stale(&self, anchors: &crate::layout::Anchors, region: bool) -> bool {
|
||
anchors.is_empty()
|
||
|| anchors.iter().any(|a| {
|
||
let t = a.relative_to;
|
||
if t == super::SCREEN {
|
||
return region;
|
||
}
|
||
match (self.id_to_frame.get(&t), self.id_to_region.get(&t)) {
|
||
(Some(f), _) => !self.resolved.contains_key(f),
|
||
(_, Some(r)) => !self.region_resolved.contains_key(r),
|
||
_ => true,
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Every frame under `root`, inclusive, for a write that moves the effective scale down a
|
||
/// subtree (`SetParent`, `SetScale`); anchor targets are ids fixed at `SetPoint`, so only the
|
||
/// scale moves. Addon pools reparent hundreds of frames a redraw, so this stays precise.
|
||
pub(crate) fn touch_layout_subtree(&mut self, root: FrameHandle) {
|
||
let mut subtree = vec![root];
|
||
let mut at = 0;
|
||
while at < subtree.len() {
|
||
let h = subtree[at];
|
||
at += 1;
|
||
if let Some(f) = self.arena.frame(h) {
|
||
subtree.extend(f.children.iter().copied());
|
||
}
|
||
}
|
||
for h in subtree {
|
||
self.touch_layout_frame(h);
|
||
}
|
||
}
|
||
|
||
/// Names `rh` on the measure ledger for a write that may move its measure key.
|
||
pub(crate) fn touch_measure(&mut self, rh: RegionHandle) {
|
||
if let Some(list) = &mut self.measure_dirty {
|
||
list.push(rh);
|
||
}
|
||
}
|
||
|
||
/// A measure-key write that names no region, such as a font object: the next sweep walks all.
|
||
pub(crate) fn touch_measure_all(&mut self) {
|
||
self.measure_dirty = None;
|
||
}
|
||
|
||
/// Names a frame without bumping the epoch, for the resolve's own tooltip pre-pass: a bump
|
||
/// inside a resolve would hold the gate open for good, yet an unnamed write could go stale.
|
||
pub(crate) fn note_layout_frame_write(&mut self, h: FrameHandle) {
|
||
if self.layout_touched.is_some() {
|
||
match self.frame_to_id.get(&h) {
|
||
Some(&id) => {
|
||
self.layout_touched
|
||
.as_mut()
|
||
.expect("checked above")
|
||
.push(id);
|
||
}
|
||
_ => self.give_up_naming(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `note_layout_frame_write` for a region.
|
||
pub(crate) fn note_layout_region_write(&mut self, rh: RegionHandle) {
|
||
if self.layout_touched.is_some() {
|
||
match self.region_to_id.get(&rh) {
|
||
Some(&id) => {
|
||
self.layout_touched
|
||
.as_mut()
|
||
.expect("checked above")
|
||
.push(id);
|
||
}
|
||
_ => self.give_up_naming(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Names layout id `id` as what this write moved, while naming is still on, whether or not
|
||
/// the cached graph holds it yet: the resolve re-derives it either way.
|
||
pub(crate) fn name_layout_node(&mut self, id: u32) {
|
||
// Already conservative: a precise touch cannot un-say an imprecise one.
|
||
if let Some(list) = &mut self.layout_touched {
|
||
list.push(id);
|
||
}
|
||
self.bump_layout_epoch();
|
||
}
|
||
|
||
/// Every write that gives up naming comes through here, so the next resolve derives in full.
|
||
/// `WOW_LAYOUT_DERIVE_TRACE=<secs>:<n>` backtraces the first `n` of these after `secs`.
|
||
fn give_up_naming(&mut self) {
|
||
if self.layout_touched.is_some() && derive_trace_armed() {
|
||
eprintln!(
|
||
"[layout-derive] the ledger gave up naming, at:\n{}",
|
||
std::backtrace::Backtrace::force_capture()
|
||
);
|
||
}
|
||
self.layout_touched = None;
|
||
}
|
||
|
||
/// Bumps tier 1's counter, reopening the gate; `layout_touched` decides how much it derives.
|
||
fn bump_layout_epoch(&mut self) {
|
||
self.layout_epoch = self.layout_epoch.wrapping_add(1);
|
||
// `WOW_LAYOUT_TOUCH_TRACE=<secs>:<n>`: backtraces the first `n` touches after `secs`.
|
||
use std::sync::atomic::{AtomicU32, Ordering};
|
||
use std::sync::OnceLock;
|
||
static SPEC: OnceLock<Option<(std::time::Instant, f64, AtomicU32)>> = OnceLock::new();
|
||
let spec = SPEC.get_or_init(|| {
|
||
let v = std::env::var("WOW_LAYOUT_TOUCH_TRACE").ok()?;
|
||
let (secs, n) = v.split_once(':')?;
|
||
Some((
|
||
std::time::Instant::now(),
|
||
secs.trim().parse().ok()?,
|
||
AtomicU32::new(n.trim().parse().ok()?),
|
||
))
|
||
});
|
||
if let Some((t0, delay, left)) = spec {
|
||
if t0.elapsed().as_secs_f64() >= *delay
|
||
&& left
|
||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_sub(1))
|
||
.is_ok()
|
||
{
|
||
eprintln!(
|
||
"[layout-touch] epoch={} at:\n{}",
|
||
self.layout_epoch,
|
||
std::backtrace::Backtrace::force_capture()
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `WOW_LAYOUT_DERIVE_TRACE=<secs>:<n>`: armed after `secs`, past the UI load, then `n` prints.
|
||
fn derive_trace_armed() -> bool {
|
||
use std::sync::atomic::{AtomicU32, Ordering};
|
||
use std::sync::OnceLock;
|
||
static SPEC: OnceLock<Option<(std::time::Instant, f64, AtomicU32)>> = OnceLock::new();
|
||
let Some((t0, delay, left)) = SPEC.get_or_init(|| {
|
||
let v = std::env::var("WOW_LAYOUT_DERIVE_TRACE").ok()?;
|
||
let (secs, n) = v.split_once(':')?;
|
||
Some((
|
||
std::time::Instant::now(),
|
||
secs.trim().parse().ok()?,
|
||
AtomicU32::new(n.trim().parse().ok()?),
|
||
))
|
||
}) else {
|
||
return false;
|
||
};
|
||
t0.elapsed().as_secs_f64() >= *delay
|
||
&& left
|
||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_sub(1))
|
||
.is_ok()
|
||
}
|