`GetSpellRecField` knew three names. Counting what the corpus actually asks it for: `name` (4 sites), `school` (3), `stackAmount` (2), `spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`, `manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten answered nil, and each was a feature quietly off rather than an error — DoiteAuras caps its stack display on `stackAmount` (DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a crowd-control effect off the two arrays (Utility.lua:7776-7794). The eighty field names are the DLL's own, read off its pool in Spell.dbc column order. The mapping to `SpellDisplay` lives in `ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM side would be free to drift — and `GetSpellRec(spellId)` is that same field list walked through that same per-field reader, so the two verbs cannot answer differently for one name. A test asserts exactly that, key by key, over one row. The array columns are a 1-based table of three, which is measured, not guessed: SuperCleveRoidMacros walks `effectMechanic` as `for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and `reagentCount` take the same shape, two and eight long. One new DBC column — `StackAmount`, 39, bracketed by two constants this tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one added: fifteen further names upstream answers are left nil rather than putting fifteen unused u32s on a struct the spell catalog holds one of per row. They are listed at `FIELDS` with that reason, so the gap is a decision rather than an omission. `GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the probe, so a VM with a book and no catalog — a capture, or the boot VM before the catalog loads — still answers a learnt spell's name and rank. Every other column is a DBC row and has nowhere else to come from, which is why the fallback is exactly three names wide and says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2462 lines
134 KiB
Rust
2462 lines
134 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, 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,
|
|
};
|
|
|
|
// ── 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 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>;
|
|
|
|
/// A spell's `Spell.dbc` display row, reduced to the three strings ClassicAPI's `C_Spell` getters
|
|
/// quote. Plain data: `benilla-ui` must not name `benilla_formats::SpellDisplay`, so the host maps
|
|
/// its DBC row onto this on the way out.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct SpellLookup {
|
|
/// `Spell.dbc` `Name` enUS.
|
|
pub name: String,
|
|
/// `SpellNameSubtext` enUS: "Rank 3", "Racial", or absent.
|
|
pub rank: Option<String>,
|
|
/// `SpellIcon.dbc`'s `TextureFilename`, extension-free, exactly as a spellbook slot carries it
|
|
/// (`Interface\Icons\Spell_Nature_Rejuvenation`).
|
|
pub icon: Option<String>,
|
|
/// `Spell.dbc`'s `Reagent[8]`/`ReagentCount[8]` pairs (item id, count), **empty rows dropped**
|
|
/// — so this is the reagent list, not a fixed-width array with holes, and a spell needing
|
|
/// nothing has an empty `Vec`. Backs `C_Spell.GetSpellReagents`.
|
|
///
|
|
/// Kept in DBC column order, which is the order the reference's own pre-send possession check
|
|
/// walks them in (`0x6e4000`), so "the first reagent" means the same thing here as there —
|
|
/// pfUI's action-bar reagent counter reads exactly `reagents[1].itemID`
|
|
/// (`modules/actionbar.lua:1553`).
|
|
pub reagents: Vec<(u32, u32)>,
|
|
/// `Spell.dbc`'s `Dispel` id — the dispel class an aura belongs to (Magic, Curse, Disease,
|
|
/// Poison…), `0` for none. Backs `C_Spell.GetSpellDispelType`.
|
|
///
|
|
/// The raw id, not the localized name `SpellDispelType.dbc` gives: the caller maps it itself
|
|
/// (pfUI keys a `dispelTypeMap` on the number, `libs/libdebuff.lua:379`), and a name would
|
|
/// make a localized realm answer something its table cannot match.
|
|
pub dispel: u32,
|
|
/// Whether this spell's aura is kept out of the aura UI: `ATTR_DO_NOT_DISPLAY`,
|
|
/// `ATTR_EX_NO_AURA_ICON`, or a tracking aura. Exactly the predicate `benilla-app`'s
|
|
/// `ui_aura::shown_in_aura_ui` applies to the buff and debuff lists, so this answers for the
|
|
/// same auras those omit. Backs nampower's `IsAuraHidden`.
|
|
pub hidden_aura: bool,
|
|
}
|
|
|
|
/// The host's answer for *any* spell id, learned or not: `Spell.dbc`'s whole display table, which
|
|
/// `benilla-ui` has no access to. The spellbook can only answer for the ~200 spells the player
|
|
/// knows, and ClassicAPI's getters are asked about arbitrary ids at addon file scope (pfUI's
|
|
/// `api/unitframes.lua:42` keys a table on `GetSpellTexture(774)` on every class). `None` falls
|
|
/// back to the spellbook.
|
|
pub type SpellProbe = Box<dyn Fn(u32) -> Option<SpellLookup>>;
|
|
|
|
/// The host's answer for an `ItemDisplayInfo` row id: its inventory icon as an extensionless
|
|
/// `Interface\Icons\…` path. Keyed on the **display** id, not the item id, because that is the
|
|
/// column an item template carries and the one the reference's own vendor-cursor path resolves
|
|
/// through (`benilla-app`'s `cursor.rs`). `None` where the row or its icon is absent.
|
|
pub type ItemIconProbe = Box<dyn Fn(u32) -> Option<String>>;
|
|
|
|
/// One `Spell.dbc` column as nampower's `GetSpellRec`/`GetSpellRecField` hands it to Lua.
|
|
///
|
|
/// The array variants are a **1-based Lua table of three**, which is not a guess:
|
|
/// `SuperCleveRoidMacros` walks `GetSpellRecField(spellID, "effectMechanic")` as
|
|
/// `for i = 1, 3 do local em = effectMechanics[i]` (`Utility.lua:7776-7794`), and does the same
|
|
/// with `effectApplyAuraName`.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum SpellRecValue {
|
|
Int(i64),
|
|
Num(f64),
|
|
Str(String),
|
|
Ints(Vec<i64>),
|
|
Nums(Vec<f64>),
|
|
}
|
|
|
|
/// One field of a spell's `Spell.dbc` row by nampower's own name for it, or `None` for a name this
|
|
/// client does not parse (and for an id no spell owns).
|
|
///
|
|
/// **The name-to-column mapping lives in `benilla-app`,** beside the `SpellDisplay` it reads: this
|
|
/// is a mapping from upstream's spelling to that struct's fields, so it belongs where that struct
|
|
/// is, and a copy here would be free to drift from it.
|
|
pub type SpellRecFieldProbe = Box<dyn Fn(u32, &str) -> Option<SpellRecValue>>;
|
|
|
|
/// The whole row as `(name, value)` pairs in `Spell.dbc` column order, for `GetSpellRec`. Built
|
|
/// by walking the same field list through the same per-field function the probe above uses, so
|
|
/// the two verbs cannot answer differently for one name.
|
|
pub type SpellRecordProbe = Box<dyn Fn(u32) -> Option<Vec<(&'static str, SpellRecValue)>>>;
|
|
|
|
/// nampower's `GetSpellModifiers(spellId, modifierType)`: the `(flat, percent)` pair the client's
|
|
/// own `GetSpellModifiers 0x6e6b30` folds out of its two talent-modifier tables, or `None` for
|
|
/// any of that reader's four false exits (the spell has no family, it is not the player's class
|
|
/// family, it carries `AttributesEx3` bit 29, or both sums came out zero).
|
|
///
|
|
/// **A probe, not a pushed snapshot, and reinstalled when the tables change.** The arithmetic has
|
|
/// four gates and a 64-bit fold, and it is already written once in `benilla-app`'s
|
|
/// `spell/mods.rs` — the only reader of the two `i32[64][29]` tables the wire fills. Pushing the
|
|
/// tables here and folding them again would be a second implementation of that reader, free to
|
|
/// drift from the one the power cost is computed with. So the closure calls the real one, and
|
|
/// `ui_script/lifecycle.rs` reinstalls it whenever the wire writes a cell.
|
|
pub type SpellModifierProbe = Box<dyn Fn(u32, u8) -> Option<(i32, i32)>>;
|
|
|
|
/// A spell id to its `Spell.dbc` `Mechanic` column and that mechanic's enUS name from
|
|
/// `SpellMechanic.dbc`, for ClassicAPI's `C_Spell.GetSpellMechanicByID`.
|
|
///
|
|
/// Its own probe rather than two more columns on [`SpellLookup`]: the mechanic is asked for by
|
|
/// exactly one consumer, the name needs a second DBC the spell catalog does not carry, and
|
|
/// widening `SpellLookup` would touch all eleven of its construction sites for a field none of
|
|
/// them has an answer for.
|
|
///
|
|
/// `None` is "no such spell"; `Some((0, _))` is a real spell with no mechanic — a distinction the
|
|
/// consumer states outright ("the ID is 0 for a known spell with no mechanic, and the whole call
|
|
/// is nil for an invalid spell ID", `SuperCleveRoidMacros/ClassicAPI.lua:252-255`).
|
|
pub type SpellMechanicProbe = Box<dyn Fn(u32) -> Option<SpellDbcExtras>>;
|
|
|
|
/// The `Spell.dbc` columns ClassicAPI exposes that a spellbook slot does not carry.
|
|
///
|
|
/// One probe for the set rather than one each: they come from the same row, the same two catalogs
|
|
/// and the same install site, and a caller asking for any of them has already paid the lookup.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct SpellDbcExtras {
|
|
/// `Mechanic` (the spell-level column), `0` for a spell with no mechanic.
|
|
pub mechanic: u32,
|
|
/// That mechanic's enUS `SpellMechanic.dbc` name, lower-case as the data holds it; `None` for
|
|
/// mechanic 0 and for an id with no row.
|
|
pub mechanic_name: Option<String>,
|
|
/// `EffectMechanic[3]`, the per-effect column, where [`Self::mechanic`] is the spell-level
|
|
/// one. Vanilla puts some mechanics on an effect instead of the spell — Rake's bleed is the
|
|
/// consumer's own example — so a caller that reads only the spell-level field misses them.
|
|
/// `0` in a slot means that effect carries no mechanic.
|
|
pub effect_mechanics: [u32; 3],
|
|
/// `manaCost` (column 32): the flat cost, in [`Self::power_type`]'s unit.
|
|
pub mana_cost: u32,
|
|
/// `powerType`: 0 mana, 1 rage, 3 energy — the unit [`Self::mana_cost`] is counted in.
|
|
pub power_type: u32,
|
|
/// `SpellIconID`, the `SpellIcon.dbc` row id — nampower's `GetSpellRecField(id,
|
|
/// "spellIconID")`, whose whole purpose is to be handed to `GetSpellIconTexture`. `0` for a
|
|
/// spell with no icon.
|
|
pub icon_id: u32,
|
|
/// nampower's `GetSpellDuration`: the spell's own aura length in ms, `0` where this client
|
|
/// cannot say — no `SpellDuration.dbc` row, the permanent sentinel, or a row that scales with
|
|
/// level or combo points. The same three cases `ui_cast_events::aura_duration_ms` answers 0
|
|
/// for, and for the same reason: a consumer treats 0 as absent and falls back, but a number it
|
|
/// trusts and draws as a timer.
|
|
pub duration_ms: u32,
|
|
}
|
|
|
|
/// `SpellIcon.dbc` by row id: nampower's `GetSpellIconTexture`. Installed by the app, which owns
|
|
/// the DBC; `None` for a row id the file has no entry for.
|
|
pub type SpellIconProbe = Box<dyn Fn(u32) -> Option<String> + Send + Sync>;
|
|
|
|
/// What a creature display id resolves to for a model pane.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct CreatureModelView {
|
|
/// The `CreatureModelData` path, as `SetModel` would be given it.
|
|
pub path: String,
|
|
/// The display's three creature skin variations, by M2 skin slot. A creature M2 names no
|
|
/// texture of its own for those slots, so without these the pane draws the model **white** —
|
|
/// which is exactly what the shop's mount and companion cards did.
|
|
pub skins: [Option<String>; 3],
|
|
}
|
|
|
|
/// A creature display id to its model and skins (`CreatureDisplayInfo` → `CreatureModelData`),
|
|
/// for `PlayerModel:SetUnit(<displayID>)`. Installed by the app, which owns the DBCs.
|
|
pub type CreatureModelProbe = Box<dyn Fn(u32) -> Option<CreatureModelView> + Send + Sync>;
|
|
|
|
/// One known spell's live usability, nampower's `IsSpellUsable`/`IsSpellInRange` answer
|
|
/// (`compat/nampower.rs`), pushed by `ui_action::state`'s feed.
|
|
///
|
|
/// Keyed by spell id and covering the player's own book alone, which is the contract upstream
|
|
/// states: "nil on error (spell not in spellbook)".
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct SpellUsable {
|
|
/// `IsSpellUsable`'s first return.
|
|
pub usable: bool,
|
|
/// Its second: the power gate alone failed, upstream's `oom`.
|
|
pub out_of_power: bool,
|
|
/// `IsSpellInRange` **against the current target**; `None` when the spell has no range at all
|
|
/// or there is no target to measure to, which is the one case that answers nil.
|
|
pub in_range: Option<bool>,
|
|
}
|
|
|
|
/// One `C_Timer.After`/`NewTimer`/`NewTicker` row (`compat/classic_api.rs`), drained by
|
|
/// [`super::UiScript::tick`] against `GetTime()`.
|
|
pub(crate) struct PendingTimer {
|
|
pub(crate) id: u64,
|
|
/// `GetTime()` seconds this fires at; re-armed by `interval` after firing, for a ticker.
|
|
pub(crate) fire_at: f64,
|
|
/// `NewTicker`'s repeat, `None` for `After`/`NewTimer` (fires once).
|
|
pub(crate) interval: Option<f64>,
|
|
/// `NewTicker("iterations")`'s remaining fire count, `None` for unlimited/non-repeating.
|
|
pub(crate) iterations_left: Option<u32>,
|
|
/// Set by the handle's `:Cancel()`; skipped and dropped on the next drain rather than removed
|
|
/// on the spot, so cancelling from inside another timer's own callback is safe.
|
|
pub(crate) cancelled: bool,
|
|
pub(crate) callback: mlua::RegistryKey,
|
|
/// Where this row's callback was *written*, `Interface\AddOns\pfUI\modules\minimap.lua:135`,
|
|
/// for the per-timer rows of `WOW_UI_TICK`'s report ([`super::tick_prof`]). `None` unless that
|
|
/// split is armed: it costs a `lua_getinfo` per arm, and `RunNextFrame` arms every frame.
|
|
///
|
|
/// The callback's definition site, not the `C_Timer.NewTicker` call's: a ticker armed as
|
|
/// `NewTicker(FALLBACK_INTERVAL, UpdateDisplay)` must name `UpdateDisplay`, which is the body
|
|
/// that costs, rather than the one line every such ticker shares.
|
|
pub(crate) site: Option<std::rc::Rc<str>>,
|
|
}
|
|
|
|
/// The server's wall clock as the login packet broke it down, plus how far the session has run.
|
|
/// Plain data: no `Instant`, so the VM holds nothing that needs ticking.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ServerClock {
|
|
/// Civil year, e.g. 2026.
|
|
pub year: i64,
|
|
/// 1-12.
|
|
pub month: u32,
|
|
/// 1-31.
|
|
pub day: u32,
|
|
pub hour: u32,
|
|
pub min: u32,
|
|
}
|
|
|
|
/// One `SetUnitBlip`/`SetObjectTypeBlip` call (`compat/vanilla_helpers.rs`), queued for
|
|
/// `crates/benilla-app/src/minimap/blips` to drain: a texture path, not yet a loaded asset, and a
|
|
/// scale (`None` leaves the baked-in size).
|
|
pub struct BlipOverrideRequest {
|
|
pub key: BlipOverrideKey,
|
|
pub texture: String,
|
|
pub scale: Option<f32>,
|
|
}
|
|
|
|
/// Which blip a [`BlipOverrideRequest`] retargets: a unit by GUID (`SetUnitBlip`, resolved from
|
|
/// its token at call time, same as every other GUID-keyed thing in `compat/`) or a landmark by its
|
|
/// `AreaPOI` icon id (`SetObjectTypeBlip`'s "type" — the only existing per-landmark key this
|
|
/// client already carries, `benilla-formats::AreaPoi::icon`).
|
|
pub enum BlipOverrideKey {
|
|
Unit(u64),
|
|
ObjectType(u32),
|
|
}
|
|
|
|
/// One playable race as `ChrRaces.dbc` carries it, fed once per session
|
|
/// (`compat/classic_api/creature_info.rs`). Plain data: this crate reads no DBC itself, so the app
|
|
/// hands it the two string columns already resolved.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct RaceInfo {
|
|
/// `ChrRaces` field 17, the localized display name (`"Night Elf"`, and `"High Elf"` on a realm
|
|
/// whose row 10 says so).
|
|
pub name: String,
|
|
/// `ChrRaces` field 15, the file token texture paths key on (`"NightElf"`, `"BloodElf"`).
|
|
/// Genuinely distinct from [`Self::name`], not the name with spaces stripped.
|
|
pub client_file_string: String,
|
|
/// `"Alliance"` or `"Horde"`; `None` where the app cannot place the race on a side.
|
|
pub faction_group: Option<String>,
|
|
}
|
|
|
|
/// One class as `ChrClasses.dbc` carries it, fed once per session
|
|
/// (`compat/classic_api/creature_info.rs`). Plain data for the same reason [`RaceInfo`] is: this
|
|
/// crate reads no DBC, so the app hands over the two string columns already resolved.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct ClassInfo {
|
|
/// `ChrClasses` field 5, the localized display name (`"Warrior"`).
|
|
pub name: String,
|
|
/// `ChrClasses` field 14, the uppercase file token FrameXML keys `RAID_CLASS_COLORS` and
|
|
/// `LOCALIZED_CLASS_NAMES_MALE` on (`"WARRIOR"`). A separate column from [`Self::name`], not
|
|
/// that name uppercased — the same independence [`RaceInfo::client_file_string`] documents.
|
|
pub class_file: String,
|
|
}
|
|
|
|
/// One remembered player, as [`Model::player_cache`] holds them. Every field
|
|
/// is a value a caller handed `RememberPlayer`; none is derived here.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct PlayerCacheEntry {
|
|
/// `OBJECT_FIELD_GUID` as the caller spelled it — the 16-hex-digit token
|
|
/// `script/unit/mod.rs`'s `guid_token` mints, not a number, since that is what `UnitGUID`
|
|
/// answers and what the caller passes straight through.
|
|
pub guid: String,
|
|
/// The name as given, for the display spelling; the map key is its lowercase form.
|
|
pub name: String,
|
|
/// `RememberPlayer`'s third argument, upstream's `classToken`.
|
|
pub class: Option<String>,
|
|
/// `RememberPlayer`'s fourth, which pfUI fills with `UnitRace(unit)`'s first (localized)
|
|
/// return.
|
|
pub race: Option<String>,
|
|
/// `RememberPlayer`'s fifth, `UnitSex(unit)`'s 2 male / 3 female.
|
|
pub sex: Option<u32>,
|
|
}
|
|
|
|
/// The self player's posture, pushed once per tick (`compat/classic_api/state.rs`). 1.12's own
|
|
/// Lua has no path to any of these at all (no mounted/swim/indoor getter exists on the client),
|
|
/// so there is no reference default to match — `Default` (everything `false`/`0`) is only ever
|
|
/// observed for the one frame before the feed system's first run.
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub struct SelfMovement {
|
|
/// `MOVEFLAG_SWIMMING`.
|
|
pub swimming: bool,
|
|
/// A live `UNIT_FIELD_MOUNTDISPLAYID`.
|
|
pub mounted: bool,
|
|
/// The area pass's own indoor bit (`benilla-app`'s `area.rs`), the same one `ZONE_CHANGED_INDOORS`
|
|
/// keys on.
|
|
pub indoors: bool,
|
|
/// `MOVEFLAG_FALLING`, set for the whole airborne arc.
|
|
pub falling: bool,
|
|
/// `MOVEFLAG_ROOT`, for nampower's `PlayerIsRooted()`. Not 1.12 data either: the client reads
|
|
/// its own movement word, and no stock binding exposes it.
|
|
pub rooted: bool,
|
|
/// Any of the six movement bits set — forward, backward, the two strafes, the two keyboard
|
|
/// turns — for nampower's `PlayerIsMoving()`. The turn keys count because they are movement
|
|
/// input to the same word the client tests, and a turn-in-place does interrupt a cast.
|
|
pub moving: bool,
|
|
/// `UNIT_FIELD_BYTES_2` byte 3, `0` not shapeshifted (`ShapeshiftForm.dbc` row otherwise).
|
|
pub shapeshift_form: u8,
|
|
/// `IsMouselooking()`: the camera rig's own mouse-look flag, the reference's `[cam+0x90] & 1`
|
|
/// (`CameraControl::mouselooking`) — a right-held turn or a both-button run, not a
|
|
/// left-button orbit. This one *is* 1.12 data; it is here rather than behind its own feed
|
|
/// because this snapshot is the one per-tick push of the self player's posture and the rig is
|
|
/// posture.
|
|
pub mouselooking: bool,
|
|
}
|
|
|
|
/// The player's own cast state, pushed once per tick (`benilla-app`'s `ui_self_cast.rs`) for
|
|
/// nampower's [`GetCurrentCastingInfo`](crate::script::compat) and `GetCastInfo` — the two
|
|
/// most-called nampower functions in grom's corpus.
|
|
///
|
|
/// **Why a snapshot and not a read of `unit_casts`.** The cast *window* (`UnitCastState`) is what
|
|
/// a bar draws: one row per caster, start and end, nothing about the caster's own machinery. These
|
|
/// five ids and four flags are the reference's own cast globals — the inflight slot `0xceca88`,
|
|
/// the cast with the bar `0xceac48`, the auto-repeat key `0xceac30`, the queued strike, the
|
|
/// melee toggle — which live in `benilla-app`'s resources and have no descriptor field at all.
|
|
/// `GetCastInfo` then folds the two: its timing columns come from the player's own window, its
|
|
/// identity and GCD columns from here, pushed in the same frame by a system ordered after the
|
|
/// window drain so the pair can never describe different casts.
|
|
///
|
|
/// `Default` (everything zero) is what a frame before the feed's first run reads, and it is also
|
|
/// exactly "not casting", which is the honest answer then.
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
|
pub struct SelfCast {
|
|
/// `GetCurrentCastingInfo`'s first return, which upstream spells `castId` and every consumer
|
|
/// reads as a **spell id**: `SuperCleveRoidMacros` assigns it to `CurrentSpell.castingSpellId`
|
|
/// (`Init.lua:89`) and hands it to `C_Spell.GetSpellName` (`Core.lua:5427-5433`). It is the
|
|
/// reference's inflight slot `0xceca88` — the last committed, unresolved cast of any class.
|
|
pub inflight_spell: u32,
|
|
/// `visId`: the cast that owns the bar, the reference's `0xceac48`. Read as a spell id too
|
|
/// (`Core.lua:5347`, the `GetCastInfo` fallback), and `Init.lua:99` stores it as
|
|
/// `visualSpellId`.
|
|
pub visual_spell: u32,
|
|
/// `autoId`: the live auto-repeat spell, the reference's `0xceac30`.
|
|
pub auto_repeat_spell: u32,
|
|
/// `casting`: a cast or auto-repeat is in flight. Deliberately **not** the same bool as
|
|
/// [`Model::casting`](crate::script) — that one answers `SpellStopCasting`'s question (what
|
|
/// Esc can stop, which includes an auto-repeat and excludes a channel), and upstream's pair
|
|
/// here is `casting` *and* `channeling` as two independent columns, which is why the consumer
|
|
/// tests `casting == 1 or channeling == 1`.
|
|
pub casting: bool,
|
|
/// `channeling`: a channel is running (`0xceac58`).
|
|
pub channeling: bool,
|
|
/// `onswing`: an on-next-swing strike is queued (Heroic Strike, Cleave). Three macro
|
|
/// conditionals read this column alone (`Conditionals.lua:7587`, `:7594`, `:9120`).
|
|
pub on_swing: bool,
|
|
/// `autoattack`: melee auto-attack is running. `Core.lua:94` reads this one with no existence
|
|
/// guard at all — `local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()` — so on a client
|
|
/// without the function that line raises.
|
|
pub auto_attack: bool,
|
|
/// `GetCastInfo`'s `castId`, which is a **different** number from
|
|
/// [`Self::inflight_spell`](Self) despite the shared upstream name: that table carries
|
|
/// `castId` and `spellId` as separate columns, so this one is the monotonic count of
|
|
/// committed casts (`PendingCast::arm`'s own counter), not a spell id.
|
|
pub cast_id: u32,
|
|
/// `GetCastInfo`'s `castType`, decided at the commit: `0` normal, `1` non-GCD, `2` on-swing,
|
|
/// `3` channel, `4` targeting, `5` targeting non-GCD. The channel value is applied by the
|
|
/// binding from the live window rather than stored here, because a cast's type is fixed at
|
|
/// send and whether it is *currently* channelling is not.
|
|
pub cast_type: u8,
|
|
/// `GetCastInfo`'s `guid`: the committed cast's target. Written as a GUID token by the
|
|
/// binding, `0` meaning no explicit target.
|
|
pub target_guid: u64,
|
|
/// The running global cooldown on the `GetTime()` clock — `GetCastInfo`'s `gcdEndS`, with
|
|
/// `gcdRemainingMs` derived from it. `0.0` for both when no GCD is running.
|
|
///
|
|
/// One number for what 1.12 keys per `startRecoveryCategory`, because that is the shape the
|
|
/// consumer wants: `API.GetGCDRemainingMs` divides it and the `[gcd]`/`[nogcd]` macro
|
|
/// conditionals compare it against a number of seconds. The app publishes the **longest**
|
|
/// running GCD leg, which is the one a player is actually waiting on.
|
|
pub gcd_start: f64,
|
|
pub gcd_end: f64,
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
/// 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 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.
|
|
/// ClassicAPI's `C_PlayerCache`, name (lowercased) to what a caller told
|
|
/// `RememberPlayer` about that player. Not fed by the app at all — see
|
|
/// `compat/classic_api/player_cache.rs`'s header on why the writer and the reader being the
|
|
/// same addon is what makes it answerable.
|
|
pub(crate) player_cache: HashMap<String, PlayerCacheEntry>,
|
|
/// `C_PlayerCache`'s two enable toggles, `[IsEnabled, IsScanEnabled]`, both on at boot.
|
|
/// Preference state with nothing downstream, as `bag_autostore` is.
|
|
pub(crate) player_cache_enabled: [bool; 2],
|
|
pub(crate) texture_probe: Option<TextureProbe>,
|
|
/// 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>,
|
|
/// `Spell.dbc` by id for `C_Spell`'s getters, past the player's own book; `None` restricts
|
|
/// them to the book, which is all a VM with no host can see.
|
|
pub(crate) spell_probe: Option<SpellProbe>,
|
|
pub(crate) spell_mechanic_probe: Option<SpellMechanicProbe>,
|
|
/// 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<std::collections::HashMap<String, crate::framexml::Element>>,
|
|
/// `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 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, each write claiming it left the
|
|
/// cached graph's shape alone, so a resolve seeds its dirty closure from them. `None` derives
|
|
/// the graph in full; `WOW_LAYOUT_VERIFY` checks every incremental pass against a full one.
|
|
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,
|
|
/// 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, so a rise means a write fell back to
|
|
/// `touch_layout`. The `WOW_LAYOUT_VERIFY` re-run's derivation is not counted.
|
|
pub(crate) layout_derives: u64,
|
|
/// Where the writes that force a whole-graph derivation came from: `file:line` of the Lua
|
|
/// binding or engine site, to a count. The derivation count on its own says a frame paid for
|
|
/// one and not which of the forty-odd `touch_layout` sites asked; this is that half, and it is
|
|
/// what a fix has to be aimed at. Populated only while [`Self::layout_derive_sites`] has been
|
|
/// asked for, so a session that does not look pays one branch.
|
|
pub(crate) layout_derive_sites: Option<std::collections::HashMap<(&'static str, u32), 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 frame, rasterized links `(y-up rect, link, full |H…|h markup)` for `OnHyperlinkClick`.
|
|
pub(crate) link_spans: HashMap<FrameHandle, Vec<(Rect, String, String)>>,
|
|
/// The hyperlink span the cursor is inside, `(frame id, link, markup)`, for the
|
|
/// `OnHyperlinkEnter`/`OnHyperlinkLeave` edges ([`super::pointer`]). The *markup* identifies
|
|
/// the span rather than the rect: a chat frame scrolls its lines under a still cursor, and
|
|
/// re-entering the same link at a new y is not a new hover.
|
|
pub(crate) hovered_link: Option<(u32, String, String)>,
|
|
/// Tab was pressed in the chat edit box (`BenillaChatTabPressed`), for the whisper cycle.
|
|
pub(crate) chat_tab: bool,
|
|
/// 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`).
|
|
pub(crate) backdrops: HashMap<FrameHandle, backdrop::Backdrop>,
|
|
/// `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>,
|
|
/// Anchored regions' owner-relative rects from the last resolve; anchor-less ones are absent.
|
|
pub(crate) region_resolved: 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 id, first wins: `SetPoint` anchors to a sibling region by name, after frames.
|
|
pub(crate) region_names: 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`; the tick sorts them by frame id.
|
|
pub(crate) on_update_frames: Vec<FrameHandle>,
|
|
/// 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>,
|
|
/// Edit boxes owed `OnTextChanged`: the reference's dirty bit (`[E+0x31c]` bit 0), set by
|
|
/// `SetText`/`Insert`, cleared only by `0x77d3e0`: one fire per drain, with the final text, as
|
|
/// `MoneyInputFrame_SetCopper` needs. A hidden box's fire waits for a show, key or mouse-down.
|
|
pub(crate) dirty_editboxes: Vec<FrameHandle>,
|
|
/// 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>>,
|
|
/// ClassicAPI's `SetAttribute`/`GetAttribute` (`compat/classic_api/frame.rs`): a plain
|
|
/// per-frame key/value store, which is all those two verbs fundamentally are. The *secure*
|
|
/// half of the modern secure-template system (restricted execution, the state and attribute
|
|
/// drivers, `RegisterUnitWatch`) is not built and this is not it — 1.12 has no such subsystem
|
|
/// to be faithful to. Keys are strings; values are whatever Lua put there, held as
|
|
/// [`ScriptValue`] so this crate keeps no Lua handle.
|
|
pub(crate) frame_attributes: HashMap<FrameHandle, HashMap<String, ScriptValue>>,
|
|
/// `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>,
|
|
|
|
/// The mouse-enabled frame under the cursor, for the next move's `OnLeave`/`OnEnter`.
|
|
pub(crate) mouseover: Option<FrameHandle>,
|
|
/// A frame under a still cursor hid or 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,
|
|
/// Per button, the frame its press landed on, for `OnClick`'s same-frame release test.
|
|
pub(crate) mouse_down_on: HashMap<String, 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)>,
|
|
|
|
/// 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>,
|
|
/// Things the host should log at `info!` because they are neither a fault nor noise — an
|
|
/// on-demand `LoadAddOn` is the first: the startup walk names every addon it loads, this path
|
|
/// named none, so a session log could not say which window was open when it stalled.
|
|
pub(crate) notices: Vec<String>,
|
|
/// The screen-root rect (`[bottom, left, top, right]`), the anchor base for top-level frames.
|
|
pub(crate) screen: Rect,
|
|
|
|
/// 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>,
|
|
/// Live cast/channel windows by caster GUID, behind ClassicAPI's
|
|
/// `C_Spell.UnitCastingInfo`/`UnitChannelInfo` — read it only through [`Self::unit_cast`],
|
|
/// where the token -> GUID fold lives. A `Vec`, not a map, for the reason
|
|
/// [`Self::unit_by_guid`] is a scan: at most the handful of units actually mid-cast, and the
|
|
/// app prunes each window the tick it ends.
|
|
pub(crate) unit_casts: Vec<super::unit::UnitCastState>,
|
|
/// Per unit token, auras in display order: the player's by insertion, the rest by aura slot.
|
|
pub(crate) auras: HashMap<String, Vec<AuraState>>,
|
|
/// 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>,
|
|
/// `TargetUnit`, `AssistUnit` and `TargetLastEnemy` calls in call order, one queue as the
|
|
/// reference routes all three through one helper; an unresolvable one is a no-op, as there.
|
|
pub(crate) selection_requests: Vec<super::SelectionRequest>,
|
|
/// `TargetNearestFriend` calls, `true` for the reverse cycle.
|
|
pub(crate) target_nearest_friend_requests: Vec<bool>,
|
|
/// `TargetByName(name, exactMatch)` calls, for the app's by-name resolver.
|
|
pub(crate) target_by_name_requests: Vec<(String, bool)>,
|
|
/// `ClearTarget()` fired with a live target, the last step of `ToggleGameMenu`'s ESC chain.
|
|
pub(crate) target_clear: bool,
|
|
/// `DropItemOnUnit` tokens (`0x48d960`), gated by the app; a refusal silently keeps the item.
|
|
pub(crate) drop_item_on_unit: Vec<String>,
|
|
/// `SpellTargetUnit` tokens: a unit frame binds the spell currently waiting on the cursor.
|
|
pub(crate) spell_target_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,
|
|
/// 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>,
|
|
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>,
|
|
/// `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 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,
|
|
pub(crate) action_uses: Vec<super::action::ActionUse>,
|
|
/// `(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,
|
|
/// Live usability per known spell, by spell id ([`SpellUsable`]).
|
|
pub(crate) spell_usable: std::collections::HashMap<u32, SpellUsable>,
|
|
/// `SpellIcon.dbc` lookup ([`SpellIconProbe`]).
|
|
pub(crate) spell_icon_probe: Option<SpellIconProbe>,
|
|
pub(crate) item_icon_probe: Option<ItemIconProbe>,
|
|
pub(crate) spell_modifier_probe: Option<SpellModifierProbe>,
|
|
pub(crate) spell_rec_field_probe: Option<SpellRecFieldProbe>,
|
|
pub(crate) spell_record_probe: Option<SpellRecordProbe>,
|
|
/// Creature display id to model path ([`CreatureModelProbe`]).
|
|
pub(crate) creature_model_probe: Option<CreatureModelProbe>,
|
|
/// 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,
|
|
/// The macro icon paths from `SpellIcon.dbc`, behind `GetMacroIconInfo`.
|
|
pub(crate) macro_icons: Vec<String>,
|
|
/// The modern enumeration's two buckets, behind `GetMacroIcons`/`GetMacroItemIcons` and so
|
|
/// behind `IconDataProviderMixin`: `(ABILITY_*`/`SPELL_*`, `INV_*)` as **uppercase basenames**
|
|
/// without the `Interface\Icons\` prefix or an extension, which is the shape upstream's own
|
|
/// four mutators push and the mixin re-prefixes. The spell bucket is the same files
|
|
/// `macro_icons` above holds, differently spelled; the item bucket is the ~2.4k `INV_*` icons
|
|
/// 1.12's own chooser filter throws away.
|
|
pub(crate) macro_icon_buckets: (Vec<String>, Vec<String>),
|
|
/// 1-based macro index to the spell id that macro casts, the reference's own `[rec+0x564]`,
|
|
/// behind ClassicAPI's `GetMacroSpell`. Derived app-side and fed in
|
|
/// ([`UiScript::set_macro_bound_spells`](crate::script::UiScript::set_macro_bound_spells)),
|
|
/// which is what `script/macros.rs`'s header already said this fact would need: the slash
|
|
/// alias table and the player's book, neither of which this crate owns. Only macros that bind
|
|
/// a *resolved* spell have a row; "casts nothing" and "casts something unknown" are both
|
|
/// absence, because `GetMacroSpell` has one way to say either.
|
|
pub(crate) macro_bound_spells: std::collections::HashMap<u32, u32>,
|
|
/// Spell ids `CastSpell` queued.
|
|
pub(crate) spell_casts: Vec<u32>,
|
|
/// SuperWoW: `CastSpellByName(name, "CLICK")` ids, cast at the `"mouseover"` unit instead of
|
|
/// the current selection (`CastSpellByName`'s doc, `script/spellbook.rs`); a separate queue
|
|
/// so the stock `spell_casts` path (current-selection target) is untouched.
|
|
pub(crate) mouseover_spell_casts: Vec<u32>,
|
|
/// `CastSpell(id, "pet")` ids, sent as `CMSG_PET_ACTION` with a type-1 word (`0x4b34ce`).
|
|
pub(crate) pet_spell_casts: Vec<u32>,
|
|
/// `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,
|
|
/// `SpellStopCasting()` fired while casting: the ESC cancel.
|
|
pub(crate) spell_stop: 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,
|
|
/// Tokens for which `SpellCanTargetUnit`'s armed unit word clears fully, used by stock unit
|
|
/// frames before they call `SpellTargetUnit`.
|
|
pub(crate) spell_targetable_units: HashSet<String>,
|
|
/// `SpellStopTargeting()` fired while targeting: the ESC targeting cancel.
|
|
pub(crate) spell_stop_targeting: bool,
|
|
|
|
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.
|
|
pub(crate) battlefield_slots: Vec<super::battlefield_queue::BattlefieldQueueSlot>,
|
|
/// `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>,
|
|
/// Form spell ids `CastShapeshiftForm` queued.
|
|
pub(crate) shapeshift_casts: Vec<u32>,
|
|
|
|
/// 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 slots `CastPetAction` queued.
|
|
pub(crate) pet_actions_pressed: Vec<u32>,
|
|
/// 1-based slot indices `TogglePetAutocast` queued.
|
|
pub(crate) pet_autocast_toggles: Vec<u32>,
|
|
/// `PetStopAttack()` calls.
|
|
pub(crate) pet_stop_attacks: u32,
|
|
/// `PetAttack`, `PetFollow`, `PetWait` and mode orders, as the packed word of their bar slot.
|
|
pub(crate) pet_orders: Vec<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), and the queued `UseContainerItem` calls.
|
|
pub(crate) containers: HashMap<i64, container::ContainerState>,
|
|
pub(crate) container_uses: Vec<(i64, u32)>,
|
|
/// 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,
|
|
/// ClassicAPI's bag-sort preference toggles (`compat/classic_api/container.rs`): 1.12 has no
|
|
/// bag-sort feature at all, so these are stored preferences with nothing downstream reading
|
|
/// them yet — a real `SortBags`/`SortBankBags` would need a sort algorithm this client doesn't
|
|
/// have, not just a flag.
|
|
pub(crate) bag_sort_right_to_left: bool,
|
|
pub(crate) backpack_autosort_disabled: bool,
|
|
pub(crate) bank_autosort_disabled: bool,
|
|
/// What the cursor carries; nothing moves locally, the server's updates settle the bags.
|
|
pub(crate) cursor: Option<cursor::CursorPayload>,
|
|
/// The action bar's drop grid: shown on the cursor's empty-to-held edge for any payload but a
|
|
/// pet action or a vendor row (mode 5; the grab setter `0x4950f0` shows it for mode 7 only).
|
|
pub(crate) cursor_grid_shown: bool,
|
|
/// The pet bar's grid, lit only by a pet-action pickup (`0x494f28`); never both grids at once.
|
|
pub(crate) pet_grid_shown: 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 `StartMoving()` in flight, the client's `root+0xcfc` slot, apart from `drag` as in
|
|
/// the reference: a move outlives the button, since mouse-up's auto-stop skips a Lua drag.
|
|
pub(crate) moving: Option<super::object::FrameMove>,
|
|
/// The `StartSizing` in flight, cleared by `StopMovingOrSizing` (`0x776990`).
|
|
pub(crate) sizing: Option<super::object::FrameSizing>,
|
|
/// 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, which the app pushes every frame a vendor is open.
|
|
pub(crate) repair_all_cost: u32,
|
|
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)>,
|
|
/// `(bag, slot)` of each grey stack `C_MerchantFrame.SellAllJunkItems` asked to sell. Its
|
|
/// own queue rather than sharing the cursor one above, whose name is a statement about where
|
|
/// the item came from; the app drains both into the same `CMSG_SELL_ITEM` loop.
|
|
pub(crate) merchant_junk_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 whole `TaxiNodes.dbc` table for ClassicAPI's `C_TaxiMap.GetTaxiNodesForMap`, resolved
|
|
/// to zones and map percentages app-side. Nothing to do with [`Self::taxi`]: that is the open
|
|
/// window's *known* nodes and is cleared when it closes, this is static file data that stands
|
|
/// for the session (`compat::classic_api::taxi_map`).
|
|
pub(crate) taxi_catalog: Vec<taxi::TaxiMapNode>,
|
|
|
|
/// 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,
|
|
|
|
/// 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 (the C `CLootButton`,
|
|
/// here `BenillaTakeLootSlot`); `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,
|
|
/// `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>,
|
|
/// `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],
|
|
/// `UseInventoryItem` slot ids, sent as `CMSG_USE_ITEM` on the equipped item.
|
|
pub(crate) inventory_uses: Vec<u32>,
|
|
/// 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.
|
|
/// Queued dressing-room intents, each with the pane it was called on: an addon may own
|
|
/// many `DressUpModel` panes, and each keeps its own substitutions.
|
|
pub(crate) dressup_intents: Vec<(crate::widget::FrameHandle, 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 lowercased live-unit token, reach for `CanInspect`/`CheckInteractDistance`; absent: nil.
|
|
pub(crate) unit_reach: HashMap<String, super::UnitReach>,
|
|
/// VanillaHelpers' `SetUnitBlip`/`SetObjectTypeBlip` (`compat/vanilla_helpers.rs`), queued and
|
|
/// drained by `crates/benilla-app/src/minimap/blips`'s own override resource — not a 1.12
|
|
/// verb, so no reference shape to match.
|
|
pub(crate) blip_overrides: Vec<BlipOverrideRequest>,
|
|
|
|
/// 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 the chat box submitted (`SubmitChatInput`), for the app's slash-command parser.
|
|
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,
|
|
/// The world-state readout's rows, gated and resolved by the app.
|
|
pub(crate) worldstate: super::worldstate::WorldStateUiState,
|
|
/// Events bindings raise. Deviation: fired at the next tick, not synchronously as in the
|
|
/// reference, because a binding runs inside Lua and cannot re-enter handler dispatch.
|
|
pub(crate) pending_events: Vec<(String, Vec<ScriptValue>)>,
|
|
/// The last cursor position in UI space (logical px, y-up), behind `GetCursorPosition()`.
|
|
pub(crate) cursor_pos: (f32, f32),
|
|
|
|
/// ClassicAPI's `C_Timer` (`compat/classic_api.rs`): not a 1.12 verb at all. Drained by
|
|
/// `UiScript::tick` against [`UiScript::now`], the same `GetTime()` clock every cooldown and
|
|
/// aura timer already reads, so a timer survives exactly as long as anything else does (a
|
|
/// `ReloadUI`/relog rebuilds the VM and drops every pending one, same as the reference's own
|
|
/// `C_Timer` handles never surviving one).
|
|
/// SuperWoW's `SetMouseoverUnit(unit)` (`compat/superwow.rs`): a GUID the `"mouseover"` token
|
|
/// resolves to instead of whatever the cursor is over, until `SetMouseoverUnit()` clears it.
|
|
/// Stored as a GUID rather than a snapshot so the override stays live — it re-resolves through
|
|
/// the current frame's unit set every read, instead of freezing one tick's health and position.
|
|
/// Seconds to add to a UTC unix timestamp to get the **player's own wall clock**, pushed by
|
|
/// `benilla-app` (which owns the OS call). `0` until pushed, which is UTC — the behaviour this
|
|
/// client had everywhere before, so a VM with no host still renders what it always did.
|
|
///
|
|
/// `date()` applies it, as Lua's `os.date` applies the zone. `C_DateAndTime::
|
|
/// GetServerTimeLocal` subtracts it, so `date(fmt, GetServerEpoch())` still prints the
|
|
/// SERVER's wall clock: the two compose, and neither is correct without the other.
|
|
pub(crate) local_utc_offset_secs: i32,
|
|
pub(crate) mouseover_override: Option<u64>,
|
|
/// Every `(lowercased query, exactMatch)` a script has passed to `TargetByName`, so the app
|
|
/// can hold each one's resolved unit ready *before* the next call ([`Self::named_targets`]).
|
|
///
|
|
/// Capped at [`BY_NAME_ASKED_CAP`]: an addon's watch list is a fixed set, but nothing stops
|
|
/// one feeding this a new string every frame, and each entry costs the app a candidate rank
|
|
/// per streamed unit per frame. Past the cap a query simply keeps the one-frame-late path it
|
|
/// has always had.
|
|
pub(crate) by_name_asked: std::collections::BTreeSet<(String, bool)>,
|
|
/// Those queries resolved against the streamed units, pushed once per frame by the app
|
|
/// (`benilla-app`'s `target::by_name::feed_named_targets`) and **not** written here.
|
|
pub(crate) named_targets: HashMap<(String, bool), UnitState>,
|
|
/// The unit a `TargetByName` selected during this Lua frame: `"target"`'s answer until the
|
|
/// app's own push replaces it, cleared at the top of every tick.
|
|
///
|
|
/// The reference selects inside the call — `TargetByName 0x489d60` runs the resolver and
|
|
/// `CGGameUI::Target` before it returns — so an addon can read `UnitName("target")` on the
|
|
/// very next line and see what it just picked. Here the selection is a request the app drains
|
|
/// later in the frame, and the token snapshots were pushed *before* Lua ran, so without this
|
|
/// that read answers the previous target. One frame is the whole window: unitscan probes for
|
|
/// a rare by targeting its name, reading the name back, and calling `TargetLastTarget` to put
|
|
/// the old target back only `if prevTarget ~= foundTarget` (`unitscan.lua:112-130`). A frame
|
|
/// late, the probing frame sees no change and the frame that *does* see the mob has already
|
|
/// re-read `prevTarget` as the mob — so the restore never fires and the scan keeps your
|
|
/// target. That is the "unitscan steals my target" report, and it is this field's whole job.
|
|
pub(crate) target_override: Option<UnitState>,
|
|
/// ClassicAPI's `FocusUnit(unit)`/`ClearFocus()` (`compat/classic_api/focus.rs`): the focus
|
|
/// unit's GUID, the persistent secondary selection 1.12 has no verb and no token for. Stored
|
|
/// as a GUID for the same reason [`Self::mouseover_override`] is — the `"focus"` token must
|
|
/// answer the unit's *live* health, power and auras, not a frozen copy of the tick it was
|
|
/// assigned on. The app reads it back (`benilla-app`'s `ui_unit.rs`) to push the `"focus"` and
|
|
/// `"focustarget"` token snapshots and fire their `UNIT_*` edges, which is the only way either
|
|
/// token resolves once no other token names the unit. `None` is no focus, which a fresh VM (a
|
|
/// `/reload`, a relog) restores — the reference's own selection survives neither.
|
|
pub(crate) focus_guid: Option<u64>,
|
|
/// SuperWoW's `Clickthrough([0|1])`: click-through on lootless corpses. Stored and reported
|
|
/// faithfully; **nothing consumes it yet** — the world-click corpse path has not been wired to
|
|
/// read it, so an addon's toggle round-trips correctly but does not change picking. Tracked in
|
|
/// `docs/ADDON_COMPAT.md`.
|
|
pub(crate) clickthrough_corpses: bool,
|
|
/// Playable races by id, from the player's own `ChrRaces.dbc`, fed once at startup
|
|
/// (`benilla-app`'s `ui_chr_races::feed_race_info`). Empty until then, and on a run with no
|
|
/// install — `C_CreatureInfo.GetRaceInfo` answers nil from an empty table, which is also what
|
|
/// it answers past the last race, so a caller walking ids until nil terminates either way.
|
|
pub(crate) race_info: std::collections::BTreeMap<u32, RaceInfo>,
|
|
/// Classes by id, from the player's own `ChrClasses.dbc`, fed once at startup
|
|
/// (`benilla-app`'s `ui_chr_classes::feed_class_info`). Empty until then and on a run with no
|
|
/// install, which `C_CreatureInfo.GetClassInfo` answers as nil — the same "no such class"
|
|
/// answer it gives for 1.12's unused ids 6 and 10, so a caller walking ids cannot tell a
|
|
/// chainless run from the gaps the real table has anyway.
|
|
pub(crate) class_info: std::collections::BTreeMap<u32, ClassInfo>,
|
|
/// The server's own broken-down wall clock, fed from `SMSG_LOGIN_SETTIMESPEED`'s packed date
|
|
/// (`benilla-app`'s `minimap::feed_server_clock`), for ClassicAPI's `C_DateAndTime`. `None`
|
|
/// before login, which that namespace reports as nil — the shape pfUI documents and relies on.
|
|
pub(crate) server_clock: Option<ServerClock>,
|
|
pub(crate) timers: Vec<PendingTimer>,
|
|
/// Ids `:Cancel()` could not reach, because the drain had moved their rows out of
|
|
/// [`Self::timers`] with `mem::take` — which is every cancel taken from inside a `C_Timer`
|
|
/// callback, the commonest shape there is (`ticker:Cancel()` on the fire that finished the
|
|
/// job). Without this the cancel silently did nothing and a self-stopping ticker ran for the
|
|
/// session: `ItemMixin:ContinueOnItemLoad`'s poller re-ran its payload **every tick**, which on
|
|
/// pfQuest's hearthstone check (`database.lua:2242`) meant a whole `BuildNameIndex` rebuild per
|
|
/// frame. Read and cleared by `drain_timers` at the end of the pass it was filled in; ids are
|
|
/// monotonic and never reused, so a stale entry cannot hit the wrong row.
|
|
pub(crate) timers_cancelled: Vec<u64>,
|
|
/// The next [`PendingTimer::id`], so `:Cancel()` can find its row after others fire and are
|
|
/// removed.
|
|
pub(crate) next_timer_id: u64,
|
|
/// The self player's movement/posture snapshot (`compat/classic_api/state.rs`'s
|
|
/// `IsSwimming`/`IsMounted`/`IsIndoors`/`IsOutdoors`/`GetShapeshiftFormID`, SuperWoW's own
|
|
/// lowercase-initial siblings pending): not 1.12 verbs, so fed only for `"player"`, not the
|
|
/// generic per-token reach the rest of `UnitState` has.
|
|
pub(crate) self_movement: SelfMovement,
|
|
/// The player's own cast machinery, pushed once per tick beside it.
|
|
pub(crate) self_cast: SelfCast,
|
|
|
|
/// A `Minimap:PingLocation(x, y)`, drained the same frame: centre-relative offsets in UI 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,
|
|
/// `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; an
|
|
/// error raised by the handler itself goes to `errors` directly, which stops the recursion.
|
|
pub(crate) fn record_script_error(&mut self, msg: String) {
|
|
self.diagnostics
|
|
.record(super::diagnostics::DiagnosticKind::Error, &msg);
|
|
self.pending_error_dispatch.push(msg.clone());
|
|
self.errors.push(msg);
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// [`Self::record_warning`] for a warning a single session repeats: the retained log takes
|
|
/// every occurrence (its row's `count` is the real total), the host's terminal takes only the
|
|
/// first. For a diagnostic about a call site the player cannot fix, where the 1st line is the
|
|
/// whole value and the 283rd is noise — one real session logged 283 copies of the `SetOwner`
|
|
/// unknown-anchor warning from one line of TurtleWoW's own FrameXML.
|
|
///
|
|
/// Dedup is the retained log's own row identity (`DiagnosticLog::record`), so the caller's
|
|
/// dedup key is whatever it folds into the message, and the fold is exact: two call sites, two
|
|
/// first lines. Past [`super::diagnostics::DIAGNOSTIC_LOG_CAP`] distinct messages an evicted
|
|
/// row can print once more; that is the cap's price everywhere and not worth a second table.
|
|
pub(crate) fn record_warning_once(&mut self, msg: impl Into<String>) {
|
|
let msg = msg.into();
|
|
if 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);
|
|
}
|
|
|
|
/// A model pane's subject: the guid its `SetUnit` pinned ([`crate::widget::ModelState::unit_guid`]),
|
|
/// else its token re-read, for a pane seated before its unit streamed in.
|
|
///
|
|
/// One reader for the paint gate and the extract, so the two cannot disagree about whether the
|
|
/// pane has anything to draw. `Some(0)` is a token naming a unit with no guid, which only a
|
|
/// harness feed produces; the app resolves it to no entity, as it always has.
|
|
pub(crate) fn unit_pane_subject(&self, m: &crate::widget::ModelState) -> Option<u64> {
|
|
m.unit_guid.or_else(|| {
|
|
m.unit
|
|
.as_deref()
|
|
.and_then(|token| self.unit(token))
|
|
.map(|u| u.guid)
|
|
})
|
|
}
|
|
|
|
/// A unit token's state, 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 unit(&self, token: &str) -> Option<&UnitState> {
|
|
let lower = token.to_ascii_lowercase();
|
|
// SuperWoW's `SetMouseoverUnit`: an explicit override wins over whatever the cursor is
|
|
// over, which is the whole point — an addon sets it from a unit frame's OnEnter so a
|
|
// `mouseover` macro works off the frame rather than the 3-D world. Checked before the
|
|
// token map, since the app overwrites the real `"mouseover"` snapshot every frame.
|
|
if lower == "mouseover" {
|
|
if let Some(guid) = self.mouseover_override {
|
|
return self.unit_by_guid(guid);
|
|
}
|
|
}
|
|
// A `TargetByName` that resolved during this very tick, for the reason above: the
|
|
// reference's selection is synchronous and ours is a queued request, so the call's own
|
|
// read has to see it. Dropped at the top of the next tick, so it never outlives the Lua
|
|
// frame that made it.
|
|
if lower == "target" {
|
|
if let Some(state) = &self.target_override {
|
|
return Some(state);
|
|
}
|
|
}
|
|
// ClassicAPI's focus (`compat/classic_api/focus.rs`), both its tokens. Checked before the
|
|
// token map for the reason `"mouseover"` is: the app pushes both snapshots off
|
|
// [`Self::focus_guid`], so a `FocusUnit`/`ClearFocus` run *this* frame has to win over
|
|
// whatever the last push left there. With no focus set both tokens name nothing at once,
|
|
// so a cleared focus does not keep answering for a frame. With one set, `"focus"` answers
|
|
// from any token already naming that unit (the assigning frame, before the feed catches
|
|
// up); otherwise both fall through to the map, which is the steady state — the app's push
|
|
// is then the only resolver, and for `"focustarget"` always is, since the focus's
|
|
// `UNIT_FIELD_TARGET` is a hop this crate cannot make.
|
|
if lower == "focus" || lower == "focustarget" {
|
|
let guid = self.focus_guid?;
|
|
if lower == "focus" {
|
|
if let Some(u) = self.unit_by_guid(guid) {
|
|
return Some(u);
|
|
}
|
|
}
|
|
}
|
|
if let Some(u) = self.units_by_lower.get(&lower) {
|
|
return Some(u);
|
|
}
|
|
// SuperWoW: a raw GUID names whichever currently-reachable unit carries it, and
|
|
// `"<base>owner"` the owner (`UNIT_FIELD_SUMMONEDBY`, else charmer, else creator) of
|
|
// whatever `<base>` itself names. Both fall back to the live reach, so a unit no longer
|
|
// named by any token (target moved on, mouseover left) is not findable this way either —
|
|
// a narrower reach than SuperWoW's own (any loaded object), not yet built.
|
|
if let Some(guid) = super::unit::token_guid(&lower) {
|
|
return self.unit_by_guid(guid);
|
|
}
|
|
if let Some(base) = lower.strip_suffix("owner").filter(|b| !b.is_empty()) {
|
|
let owner = self.units_by_lower.get(base)?.owner;
|
|
return (owner != 0).then(|| self.unit_by_guid(owner)).flatten();
|
|
}
|
|
// SuperWoW: `"mark1"`..`"mark8"` name whichever currently-reachable unit carries that
|
|
// raid-target icon.
|
|
if let Some(mark) = lower
|
|
.strip_prefix("mark")
|
|
.and_then(|n| n.parse::<u8>().ok())
|
|
.filter(|n| (1..=8).contains(n))
|
|
{
|
|
return self.units_by_lower.values().find(|u| u.raid_target == mark);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Whichever currently-reachable unit's snapshot carries this GUID, for [`Self::unit`]'s
|
|
/// SuperWoW fallbacks.
|
|
///
|
|
/// A linear scan over `units_by_lower`, which is the live *token* set, never a server's whole
|
|
/// object list — but no longer "a handful" either: 40 plates and each one's target push 80
|
|
/// `nameplate<N>` keys on top of the stock singles and the party/raid families, so a crowded
|
|
/// pull holds ~90. Measured against that set
|
|
/// (`script::tests::unit_token_bench`): a token hit through `Self::unit` is 30 ns and a
|
|
/// raw-GUID hit through this scan 90 ns, so the scan costs ~60 ns on the paths that reach it
|
|
/// — the raw-GUID and `mark<n>` spellings, and `"mouseover"`/`"focus"` while an override is
|
|
/// set. A GUID index would buy that 60 ns back and nothing else: no shipped addon asks a unit
|
|
/// verb by raw GUID per frame (pfUI compares cached GUID *strings*, `modules/nameplates.lua`),
|
|
/// and the whole verb is 220 ns of which 44 ns is the Rust/Lua boundary alone. Left a scan
|
|
/// until something measured wants it.
|
|
fn unit_by_guid(&self, guid: u64) -> Option<&UnitState> {
|
|
self.units_by_lower.values().find(|u| u.guid == guid)
|
|
}
|
|
|
|
/// The live cast or channel window of whatever unit `token` names, for ClassicAPI's
|
|
/// `C_Spell.UnitCastingInfo`/`UnitChannelInfo` (`docs/ADDON_COMPAT.md`).
|
|
///
|
|
/// The whole fold is here, in one place: resolve the token to a snapshot ([`Self::unit`],
|
|
/// which already understands every token spelling this client answers — the stock ones, a raw
|
|
/// SuperWoW GUID string, `"mark<n>"`, `"<base>owner"`, ClassicAPI's `"focus"`), take its GUID,
|
|
/// then find that GUID's window. `want_channel` picks between the two verbs, which are
|
|
/// disjoint by construction: a channel never answers `UnitCastingInfo` and a cast never
|
|
/// answers `UnitChannelInfo`, as upstream's pair behaves and as every consumer's
|
|
/// `if not name then ...` fallback assumes.
|
|
///
|
|
/// A unit the client holds no snapshot for answers `None` even if it is casting: the GUID is
|
|
/// the window's key, but a token is the only way in, and with nothing naming the unit there is
|
|
/// no token to ask with. Nothing is invented for one.
|
|
pub(crate) fn unit_cast(
|
|
&self,
|
|
token: &str,
|
|
want_channel: bool,
|
|
) -> Option<&super::unit::UnitCastState> {
|
|
let guid = self.unit(token).map(|u| u.guid).filter(|g| *g != 0)?;
|
|
self.unit_casts
|
|
.iter()
|
|
.find(|c| c.guid == guid && c.channel == want_channel)
|
|
}
|
|
|
|
/// ClassicAPI's `UnitTokenFromGUID`: the token key whose snapshot carries this GUID, the
|
|
/// other half of [`Self::unit_by_guid`]. Same reach: only a currently-named token, never a
|
|
/// server's whole object list.
|
|
///
|
|
/// **One token, and that is all this verb promises.** `UnitTokenFromGUID` returns a single
|
|
/// string, so a unit answering to three names has to be collapsed to one; which one is
|
|
/// whichever the map walk reaches first. [`Self::tokens_for_guid`] is the honest plural, and
|
|
/// exists because the `UNIT_SPELLCAST_*` events genuinely need *all* of them.
|
|
pub(crate) fn token_for_guid(&self, guid: u64) -> Option<&str> {
|
|
self.units_by_lower
|
|
.iter()
|
|
.find(|(_, u)| u.guid == guid)
|
|
.map(|(k, _)| k.as_str())
|
|
}
|
|
|
|
/// **Every** token this client currently answers for `guid` — the fan-out a per-unit event has
|
|
/// to fire under, since one unit routinely answers to several names at once (`"target"` and
|
|
/// `"nameplate3"` and `"focus"` can all be the same mob, and each drives a different frame).
|
|
///
|
|
/// The set is the live token keys, i.e. exactly what the app's feeds have pushed: `"player"`,
|
|
/// `"target"`, `"targettarget"`, `"focus"`/`"focustarget"`, `"mouseover"`, `"npc"`, the
|
|
/// party/raid/pet families, and the `nameplate<N>` pair. Deliberately **not** the derived
|
|
/// spellings [`Self::unit`] also resolves — a raw 16-hex GUID, `"mark<n>"`, `"<base>owner"`.
|
|
/// Those are ways a *caller* can spell a unit it already has in hand, not names the client
|
|
/// publishes: nothing registers a handler keyed on one, and fanning an event out across
|
|
/// `mark1..8` plus an `owner` form per base would multiply every cast edge for no reader.
|
|
///
|
|
/// Sorted, so a caller's fan-out is deterministic and a test can assert the list. Nothing
|
|
/// depends on the *order*: every consumer in the corpus keys on its own token
|
|
/// (`pfUI/modules/castbar.lua:355` `arg1 ~= unit`,
|
|
/// `modules/nameplates.lua:651` `strfind(arg1, "^nameplate")`), never on which came first.
|
|
///
|
|
/// A 0 GUID answers nothing: it names no unit, and a window keyed on it could otherwise be
|
|
/// matched by any snapshot whose GUID has not resolved yet.
|
|
pub(crate) fn tokens_for_guid(&self, guid: u64) -> Vec<&str> {
|
|
if guid == 0 {
|
|
return Vec::new();
|
|
}
|
|
let mut out: Vec<&str> = self
|
|
.units_by_lower
|
|
.iter()
|
|
.filter(|(_, u)| u.guid == guid)
|
|
.map(|(k, _)| k.as_str())
|
|
.collect();
|
|
out.sort_unstable();
|
|
out
|
|
}
|
|
|
|
pub(crate) fn new() -> Model {
|
|
Model {
|
|
addons: Vec::new(),
|
|
addon_index: Vec::new(),
|
|
addon_info_hidden: None,
|
|
addons_root: None,
|
|
addons_chain_reader: None,
|
|
measurer: None,
|
|
texture_probe: None,
|
|
spell_probe: None,
|
|
item_icon_probe: None,
|
|
spell_modifier_probe: None,
|
|
spell_rec_field_probe: None,
|
|
spell_record_probe: None,
|
|
spell_mechanic_probe: None,
|
|
player_cache: HashMap::new(),
|
|
player_cache_enabled: [true, true],
|
|
texture_size_probe: None,
|
|
font_probe: None,
|
|
addons_saved_account: None,
|
|
addons_saved_character: None,
|
|
framexml_templates: Default::default(),
|
|
framexml_debug: 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_derive_sites: None,
|
|
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_spans: HashMap::new(),
|
|
hovered_link: None,
|
|
chat_tab: false,
|
|
region_data: HashMap::new(),
|
|
backdrops: HashMap::new(),
|
|
simple_html: simplehtml::SimpleHtmlStates::new(),
|
|
font_objects_by_lower: HashMap::new(),
|
|
region_resolved: 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_names: HashMap::new(),
|
|
scripts: HashMap::new(),
|
|
on_update_frames: Vec::new(),
|
|
on_size_changed_frames: Vec::new(),
|
|
on_update_model_frames: Vec::new(),
|
|
model_facts: HashMap::new(),
|
|
model_facts_wanted: Vec::new(),
|
|
dirty_editboxes: Vec::new(),
|
|
event_to_frames: HashMap::new(),
|
|
frame_events: HashMap::new(),
|
|
frame_attributes: HashMap::new(),
|
|
all_event_frames: Vec::new(),
|
|
focused_editbox: None,
|
|
mouseover: None,
|
|
hover_repick: false,
|
|
mouse_down_on: HashMap::new(),
|
|
mouse_capture: None,
|
|
last_click: HashMap::new(),
|
|
pending_size_changed: Vec::new(),
|
|
errors: Vec::new(),
|
|
pending_error_dispatch: Vec::new(),
|
|
diagnostics: Default::default(),
|
|
warnings: Vec::new(),
|
|
notices: 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),
|
|
units_by_lower: HashMap::new(),
|
|
unit_casts: Vec::new(),
|
|
auras: HashMap::new(),
|
|
cancel_aura_requests: Vec::new(),
|
|
tracking: None,
|
|
selection_requests: Vec::new(),
|
|
target_nearest_friend_requests: Vec::new(),
|
|
target_by_name_requests: Vec::new(),
|
|
drop_item_on_unit: Vec::new(),
|
|
spell_target_unit: Vec::new(),
|
|
target_clear: false,
|
|
joined_channels: Vec::new(),
|
|
party: party::PartyState::default(),
|
|
party_requests: Vec::new(),
|
|
ready_check: party::ReadyCheckState::default(),
|
|
saved_instances: Vec::new(),
|
|
raid_selection: 0,
|
|
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(),
|
|
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(),
|
|
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(),
|
|
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_uses: Vec::new(),
|
|
action_sets: Vec::new(),
|
|
ui_errors: Vec::new(),
|
|
spellbook: spellbook::SpellBookState::default(),
|
|
spell_usable: std::collections::HashMap::new(),
|
|
spell_icon_probe: None,
|
|
creature_model_probe: None,
|
|
pet_book: spellbook::PetBookState::default(),
|
|
macros: macros::MacroState::default(),
|
|
macros_dirty: false,
|
|
macros_generation: 0,
|
|
macro_icons: Vec::new(),
|
|
macro_icon_buckets: (Vec::new(), Vec::new()),
|
|
macro_bound_spells: std::collections::HashMap::new(),
|
|
spell_casts: Vec::new(),
|
|
mouseover_spell_casts: Vec::new(),
|
|
pet_spell_casts: Vec::new(),
|
|
pet_spell_autocasts: Vec::new(),
|
|
casting: false,
|
|
spell_stop: false,
|
|
spell_targeting: false,
|
|
spell_targetable_units: HashSet::new(),
|
|
spell_stop_targeting: false,
|
|
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: Vec::new(),
|
|
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(),
|
|
shapeshift_casts: Vec::new(),
|
|
pet_bar: super::pet::PetBarState::default(),
|
|
pet_actions_pressed: Vec::new(),
|
|
pet_autocast_toggles: Vec::new(),
|
|
pet_stop_attacks: 0,
|
|
pet_orders: Vec::new(),
|
|
player_control: true,
|
|
pet_set_actions: Vec::new(),
|
|
pet_abandons: 0,
|
|
pet_dismisses: 0,
|
|
pet_renames: Vec::new(),
|
|
containers: HashMap::new(),
|
|
container_uses: Vec::new(),
|
|
container_cooldowns: HashMap::new(),
|
|
has_key: false,
|
|
bag_sort_right_to_left: false,
|
|
backpack_autosort_disabled: false,
|
|
bank_autosort_disabled: false,
|
|
cursor: None,
|
|
cursor_grid_shown: false,
|
|
pet_grid_shown: 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,
|
|
sizing: None,
|
|
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,
|
|
merchant_buys: Vec::new(),
|
|
merchant_cursor_sells: Vec::new(),
|
|
merchant_junk_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,
|
|
taxi_catalog: Vec::new(),
|
|
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,
|
|
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,
|
|
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(),
|
|
quest_log_collapses: Vec::new(),
|
|
quest_log_watched: Vec::new(),
|
|
server_unix_time: None,
|
|
worldmap: super::worldmap::WorldMapState::default(),
|
|
nameplates: super::nameplate::NamePlates::default(),
|
|
worldstate: super::worldstate::WorldStateUiState::default(),
|
|
pending_events: Vec::new(),
|
|
cursor_pos: (0.0, 0.0),
|
|
local_utc_offset_secs: 0,
|
|
mouseover_override: None,
|
|
by_name_asked: std::collections::BTreeSet::new(),
|
|
named_targets: HashMap::new(),
|
|
target_override: None,
|
|
focus_guid: None,
|
|
clickthrough_corpses: false,
|
|
race_info: std::collections::BTreeMap::new(),
|
|
class_info: std::collections::BTreeMap::new(),
|
|
server_clock: None,
|
|
timers: Vec::new(),
|
|
timers_cancelled: Vec::new(),
|
|
next_timer_id: 0,
|
|
self_movement: SelfMovement::default(),
|
|
self_cast: SelfCast::default(),
|
|
minimap_ping_request: None,
|
|
minimap_ping: (0.0, 0.0),
|
|
chat_sends: Vec::new(),
|
|
addon_sends: Vec::new(),
|
|
played_time_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_uses: Vec::new(),
|
|
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(),
|
|
blip_overrides: Vec::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 opens the layout gate.
|
|
self.touch_layout();
|
|
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);
|
|
// A new region can join the resolve's external set, so it opens the gate too.
|
|
self.touch_layout();
|
|
id
|
|
}
|
|
|
|
/// A layout write that names no node. The resolve's tooltip pre-pass must not call this, or
|
|
/// the gate never closes; a missed call is caught by `WOW_LAYOUT_VERIFY`.
|
|
#[track_caller]
|
|
pub(crate) fn touch_layout(&mut self) {
|
|
// Naming nothing, the next resolve derives the graph in full: slow, never wrong.
|
|
self.give_up_naming();
|
|
self.bump_layout_epoch();
|
|
}
|
|
|
|
/// A write that moved one region's offsets, size or measured extent, not its targets or
|
|
/// liveness; a region missing from the cached graph falls back to `touch_layout`.
|
|
pub(crate) fn touch_layout_region(&mut self, rh: RegionHandle) {
|
|
match self.region_to_id.get(&rh) {
|
|
Some(&id) => self.touch_layout_node(id),
|
|
None => self.touch_layout(),
|
|
}
|
|
}
|
|
|
|
/// The same for a frame: an anchor offset or size, nothing that moves an edge or the roster.
|
|
pub(crate) fn touch_layout_frame(&mut self, h: FrameHandle) {
|
|
match self.frame_to_id.get(&h) {
|
|
Some(&id) => self.touch_layout_node(id),
|
|
None => self.touch_layout(),
|
|
}
|
|
}
|
|
|
|
/// A write that moved the **effective scale** of `root`'s subtree and nothing else: a
|
|
/// `SetParent`, or a `SetScale` on `root` itself. Anchor targets are ids fixed at `SetPoint`,
|
|
/// so no edge moves and the roster is unchanged — only each node's scale input.
|
|
///
|
|
/// Both callers are hot and for the same reason: addon pools reparent hundreds of frames a
|
|
/// redraw, and pfUI's action-button press animation walks a button overlay's scale a step
|
|
/// every frame for the ~60 frames its fade lasts. Giving up naming for either one costs a
|
|
/// whole-graph derivation per call — 232 of the 463 in one measured session, and the four
|
|
/// costliest five-second windows of that run were almost nothing else. See the `[scale]` row
|
|
/// in `benilla-app`'s `tick_bench`.
|
|
pub(crate) fn touch_layout_subtree_scale(&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());
|
|
}
|
|
}
|
|
// Write the new scale here: only a full derivation syncs it, and a named node skips that.
|
|
let Model {
|
|
arena,
|
|
layout_inputs,
|
|
measure_dirty,
|
|
..
|
|
} = self;
|
|
for &h in &subtree {
|
|
if let Some(f) = arena.frame(h) {
|
|
layout_inputs.entry(h).or_default().scale = f.effective_scale;
|
|
// The owner's effective scale is in every one of its FontStrings' measure keys
|
|
// (`layout::stale_measure_key`), and it is the *only* thing outside a region's own
|
|
// data that key reads — so the subtree's regions are exactly the ones that can go
|
|
// stale, and the ledger does not have to be dropped for them. A `None` ledger
|
|
// makes the next `fontstrings_needing_measure` walk all 43,000 regions of a real
|
|
// interface to issue, typically, no requests at all. The walk is kept honest by
|
|
// the verify build, which runs it beside the drain and panics on any stale region
|
|
// the ledger missed.
|
|
if let Some(list) = measure_dirty.as_mut() {
|
|
list.extend(f.regions.iter().copied());
|
|
}
|
|
}
|
|
}
|
|
for h in subtree {
|
|
self.touch_layout_frame(h);
|
|
}
|
|
}
|
|
|
|
/// A write changed frame `h`'s anchor targets; `old` and `new` are its full target lists, so
|
|
/// the cached edges are re-pointed. Falls back to `touch_layout` when the scope refuses.
|
|
pub(crate) fn touch_layout_retarget_frame(&mut self, h: FrameHandle, old: &[u32], new: &[u32]) {
|
|
match self.frame_to_id.get(&h).copied() {
|
|
// Already conservative: the next resolve rebuilds every edge, so patching is wasted.
|
|
Some(id)
|
|
if self.layout_touched.is_some() && self.layout_scope.retarget(id, old, new) =>
|
|
{
|
|
self.touch_layout_node(id);
|
|
}
|
|
_ => self.touch_layout(),
|
|
}
|
|
}
|
|
|
|
/// `touch_layout_retarget_frame` for a region.
|
|
pub(crate) fn touch_layout_retarget_region(
|
|
&mut self,
|
|
rh: RegionHandle,
|
|
old: &[u32],
|
|
new: &[u32],
|
|
) {
|
|
match self.region_to_id.get(&rh).copied() {
|
|
Some(id)
|
|
if self.layout_touched.is_some() && self.layout_scope.retarget(id, old, new) =>
|
|
{
|
|
self.touch_layout_node(id);
|
|
}
|
|
_ => self.touch_layout(),
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
#[track_caller]
|
|
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) if self.layout_scope.has_node(id) => {
|
|
self.layout_touched
|
|
.as_mut()
|
|
.expect("checked above")
|
|
.push(id);
|
|
}
|
|
_ => self.give_up_naming(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `note_layout_frame_write` for a region.
|
|
#[track_caller]
|
|
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) if self.layout_scope.has_node(id) => {
|
|
self.layout_touched
|
|
.as_mut()
|
|
.expect("checked above")
|
|
.push(id);
|
|
}
|
|
_ => self.give_up_naming(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Names `id` as what this write moved, if the cached graph has it and naming is still on.
|
|
#[track_caller]
|
|
fn touch_layout_node(&mut self, id: u32) {
|
|
let in_graph = self.layout_scope.has_node(id);
|
|
match &mut self.layout_touched {
|
|
// Already conservative: a precise touch cannot un-say an imprecise one.
|
|
None => {}
|
|
Some(_) if !in_graph => self.give_up_naming(),
|
|
Some(list) => 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`.
|
|
#[track_caller]
|
|
fn give_up_naming(&mut self) {
|
|
if self.layout_touched.is_some() {
|
|
if derive_trace_armed() {
|
|
eprintln!(
|
|
"[layout-derive] the ledger gave up naming, at:\n{}",
|
|
std::backtrace::Backtrace::force_capture()
|
|
);
|
|
}
|
|
// `#[track_caller]` all the way down this chain, so the location is the write site —
|
|
// one of the forty-odd `touch_layout` callers — and not this function. A backtrace
|
|
// says the same thing and costs a thousand times as much, which is why the trace above
|
|
// has to be rationed to `n` prints and this does not.
|
|
if let Some(sites) = self.layout_derive_sites.as_mut() {
|
|
// `(&'static str, u32)`, not a formatted `String`: `Location::caller()` hands back
|
|
// a `&'static Location`, so the file is already static and the key costs nothing.
|
|
// This path runs thousands of times a session; formatting it would be the
|
|
// instrument's own cost showing up in the measurement.
|
|
let at = std::panic::Location::caller();
|
|
*sites.entry((at.file(), at.line())).or_insert(0) += 1;
|
|
}
|
|
}
|
|
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()
|
|
}
|