diff --git a/crates/benilla-adt/src/combined_alpha.rs b/crates/benilla-adt/src/combined_alpha.rs index fa3e39e0..57ec6866 100644 --- a/crates/benilla-adt/src/combined_alpha.rs +++ b/crates/benilla-adt/src/combined_alpha.rs @@ -187,8 +187,8 @@ mod tests { } } - /// The reference packs a 4-bit layer weight into an RGBA4444 texel (wow-re `terrain.md`, - /// the `alpha_texel_*` packers), and a 4-bit unorm channel reads as `n / 15` — so a fully + /// The reference packs a 4-bit layer weight into an RGBA4444 texel (the 64×64 and 32×32 + /// packers `0x6b03d0`/`0x6b08d0`), and a 4-bit unorm channel reads as `n / 15` — so a fully /// painted nibble is full coverage. `n × 16` stopped at 240: every "fully painted" road or rock /// let 6% of the layer beneath show through. #[test] diff --git a/crates/benilla-adt/src/lib.rs b/crates/benilla-adt/src/lib.rs index ebee4e03..78f91f67 100644 --- a/crates/benilla-adt/src/lib.rs +++ b/crates/benilla-adt/src/lib.rs @@ -125,10 +125,9 @@ impl LiquidVertex { /// The authored `(s, t)` texture-coordinate pair — two little-endian `u16`s over the same 4 /// union bytes. **Magma blocks only**: the reference's lava vert-fill is single-stage and reads /// its `tc0` straight from here, where water/ocean instead write a UV into the depth-ramp - /// texture (VERIFIED bit-exact `WoW.exe 0x68d890` `liquid_render_verts`, emulation-diffed in - /// wow-re `crates/terrain`: `u = (s as i32 as f64 · 3/256) as f32`, same for `t`). The field is - /// authored world-continuous — a chunk's east edge repeats its neighbour's west edge exactly — - /// so lava tiles seamlessly across MCNK borders. + /// texture (bit-exact `WoW.exe 0x68d890`: `u = (s as i32 as f64 · 3/256) as f32`, same for + /// `t`). The field is authored world-continuous — a chunk's east edge repeats its neighbour's + /// west edge exactly — so lava tiles seamlessly across MCNK borders. pub fn texcoords(&self) -> [u16; 2] { [ u16::from_le_bytes([self.union_data[0], self.union_data[1]]), @@ -140,16 +139,16 @@ impl LiquidVertex { /// One MCLQ liquid **block** — a 9×9 absolute-height grid + an 8×8 cell-flag grid, `0x324` bytes. /// /// An MCNK carries **one block per set liquid header bit** (bits 2–5), packed back to back and with -/// absent bits consuming nothing (VERIFIED wow-re `system/terrain/scratch/adt-format.md`, the cursor -/// walk at `0x6af7a3`–`0x6af7cb`). Most liquid chunks set exactly one bit; **28 shipped Azeroth -/// chunks set two** — a river *and* the sea, at a river mouth — and carry two blocks at different -/// heights (measured: `sizeMCLQ` 1616 = 8 + 2·`0x324`, block 0 the stream at z≈5.0, block 1 the -/// ocean at z=0). Reading only the first is how the sea goes missing there. +/// absent bits consuming nothing (the cursor walk at `0x6af7a3`–`0x6af7cb`). Most liquid chunks set +/// exactly one bit; **28 shipped Azeroth chunks set two** — a river *and* the sea, at a river mouth +/// — and carry two blocks at different heights (measured: `sizeMCLQ` 1616 = 8 + 2·`0x324`, block 0 +/// the stream at z≈5.0, block 1 the ocean at z=0). Reading only the first is how the sea goes +/// missing there. /// /// **No liquid type here on purpose.** The header-bit→type ordering (bit2 river / bit3 ocean / -/// bit4 magma / bit5 slime) is *INFERRED* upstream — never byte-proven — while the per-cell flag low -/// nibble IS the type, VERIFIED at both consumers (`0x6ba970` `and al,0xf`, `0x68d9b0`). So this -/// reader takes only the block *count* from the flags and leaves the type to whoever reads +/// bit4 magma / bit5 slime) is *inferred* — never byte-proven — while the per-cell flag low nibble +/// IS the type, as both consumers read it (`0x6ba970` `and al,0xf`, `0x68d9b0`). So this reader +/// takes only the block *count* from the flags and leaves the type to whoever reads /// [`Self::tile_flags`]. (The old `LiquidType::from_mcnk_flags` priority guess labelled all 28 /// two-bit chunks "ocean" while handing back the river block's bytes.) #[derive(Debug, Clone)] @@ -167,9 +166,8 @@ pub struct MclqChunk { /// MCNK header flag **bit 1** — the chunk a mover may not enter (the "impassable" band that walls /// Searing Gorge and the other mountain rims off from their neighbours). /// -/// The *producer* is VERIFIED per-instruction in wow-re (`system/terrain/scratch/adt-format.md`, -/// the MCNK header walk: `hdr+0x00` bit1 → `chunk+0xc |= 0x40`); the *name* is wow-re's INFERRED -/// label for that bit. Consuming it is decision 1266 / report B129. +/// The *producer* is the MCNK header walk (`0x6af5f0`: `hdr+0x00` bit1 → `chunk+0xc |= 0x40`); +/// the *name* is an inferred label for that bit. Consuming it is decision 1266 / report B129. pub const MCNK_IMPASSABLE: u32 = 0x2; /// The fields of the 128-byte MCNK header the renderer reads. `pred_tex`/`no_effect_doodad`/ @@ -559,10 +557,10 @@ const MCLQ_BLOCK: usize = 0x324; /// read: the bit→type ordering is INFERRED, the cell nibble is VERIFIED (see [`MclqChunk`]). const MCNK_LIQUID_BITS: u32 = 0x3c; -/// Read the packed MCLQ blocks — one per set liquid header bit, back to back (VERIFIED wow-re -/// `adt-format.md`). Stops early on a block the payload can't cover, so a truncated tail degrades to -/// the blocks that *are* whole rather than to nothing. A chunk with an MCLQ sub-chunk but no liquid -/// bit set still yields one block, matching the pre-multi-block reader. +/// Read the packed MCLQ blocks — one per set liquid header bit, back to back (the cursor walk +/// `0x6af7a3`–`0x6af7cb`). Stops early on a block the payload can't cover, so a truncated tail +/// degrades to the blocks that *are* whole rather than to nothing. A chunk with an MCLQ sub-chunk +/// but no liquid bit set still yields one block, matching the pre-multi-block reader. fn read_mclq_blocks(sub: &[u8], mcnk_flags: u32) -> Vec { let want = (mcnk_flags & MCNK_LIQUID_BITS).count_ones().max(1) as usize; let mut out = Vec::with_capacity(want); diff --git a/crates/benilla-app/examples/addon_harness.rs b/crates/benilla-app/examples/addon_harness.rs index a6d253eb..1d75164c 100644 --- a/crates/benilla-app/examples/addon_harness.rs +++ b/crates/benilla-app/examples/addon_harness.rs @@ -92,7 +92,7 @@ fn render_frames(frames: &[String]) -> String { /// diff interesting, and the extra-names side the one nothing else in this harness can see — every /// other ranking here is demand-driven, so it can only ever report what an addon *asked* for. /// -/// The reference side is `reference/1.12-globals.tsv`, itself generated from wow-re's live capture. +/// The reference side is `reference/1.12-globals.tsv`, generated from a live reference capture. /// `lod` rows are the twelve LoadOnDemand `Blizzard_*` addons' names, unioned in by 1200 because a /// live dump misses them unless the player opened those windows; they are counted separately rather /// than silently folded in, since "absent from us" means something different for a window nobody @@ -736,9 +736,8 @@ fn main() { // **WHOSE package is incomplete** — printed immediately before the ranked blockers, because it // is the line that stops a session hunting for a client bug that is not one. A `.toc` entry // whose file the addon does not ship is the ADDON's defect, and the reference client's - // behaviour there is what ours already does: log `Couldn't open %s` and carry on - // (wow-re `ui/scratch/xml-toc-path-resolution.md` §4). Nothing is subtracted from the headline - // — 1213 — so both readings stay available. + // behaviour there is what ours already does: log `Couldn't open %s` and carry on (`0x6edaa0`). + // Nothing is subtracted from the headline — 1213 — so both readings stay available. let own: Vec<&addon_harness::AddonReport> = reports .iter() .filter(|r| !r.absent_own_files.is_empty()) diff --git a/crates/benilla-app/src/addon_harness/mod.rs b/crates/benilla-app/src/addon_harness/mod.rs index 1ba460f3..58ae5d98 100644 --- a/crates/benilla-app/src/addon_harness/mod.rs +++ b/crates/benilla-app/src/addon_harness/mod.rs @@ -115,10 +115,9 @@ pub struct AddonReport { /// Not "did every entry resolve" — that is [`Self::absent_own_files`] and /// [`Self::absent_foreign_files`], and it is a different question because the reference /// answers it differently: a manifest line naming no file is `"Couldn't open %s"` in - /// FrameXML.log and the walk carries straight on (wow-re - /// `ui/scratch/xml-toc-path-resolution.md` §4, VERIFIED). An addon shipping an incomplete zip - /// *works* on a real client, so counting it here made the survey say the opposite of the truth - /// about 24 addons — FuBar itself among them, on two locale files it does not ship. + /// FrameXML.log and the walk carries straight on (`0x6edaa0`). An addon shipping an incomplete + /// zip *works* on a real client, so counting it here made the survey say the opposite of the + /// truth about 24 addons — FuBar itself among them, on two locale files it does not ship. /// /// **[`Self::errors`] is unchanged and still carries those rows verbatim** (1213), so every /// number in every past record is still readable off it; what changed, deliberately and with @@ -144,11 +143,11 @@ pub struct AddonReport { /// /// A different question from [`Self::absent_own_files`] and kept apart from it: this addon's /// package is fine, it wants a neighbour. The corpus's two are Auctioneer and BeanCounter - /// reaching `..\Blizzard_AuctionUI\Blizzard_AuctionUITemplates.xml` — which wow-re records as - /// **RESOLVING** in the real client (`ui/scratch/xml-toc-path-resolution.md` §5 case 2, by - /// name), because there the file layer can see `Blizzard_AuctionUI` inside `patch.MPQ`. Ours - /// reads the AddOns directory only, so it misses. That one IS ours, and the split is what - /// makes it visible instead of averaging with the row above. + /// reaching `..\Blizzard_AuctionUI\Blizzard_AuctionUITemplates.xml` — which the real client + /// **RESOLVES** (`0x6ede10` collapses the `..`), because there the file layer can see + /// `Blizzard_AuctionUI` inside `patch.MPQ`. Ours reads the AddOns directory only, so it + /// misses. That one IS ours, and the split is what makes it visible instead of averaging + /// with the row above. pub absent_foreign_files: Vec, /// Names it calls that the VM does not have — see the module doc on what this is worth. pub missing_globals: Vec, @@ -2084,11 +2083,10 @@ struct FileLoad { /// /// It is `false` for an addon whose only failures are absent files, because on the reference /// that addon *loads*: `0x6edaa0` logs `"Couldn't open %s"` and returns null, and the walk - /// carries on to the next manifest entry with nothing raised (wow-re - /// `ui/scratch/xml-toc-path-resolution.md` §4, VERIFIED — the same rule 2107 unified for the - /// two loaders that reach it from Lua). Until this existed the survey scored 24 corpus addons - /// as not surviving a session start on nothing but a locale file their own zip omits, FuBar - /// and 22 of its plugins among them. + /// carries on to the next manifest entry with nothing raised (the same rule 2107 unified for + /// the two loaders that reach it from Lua). Until this existed the survey scored 24 corpus + /// addons as not surviving a session start on nothing but a locale file their own zip omits, + /// FuBar and 22 of its plugins among them. raised: bool, } @@ -2107,10 +2105,10 @@ fn load_addon_files(script: &UiScript, root: &Path, name: &str, toc: &Toc) -> Fi let Some(bytes) = read_under(root, &path) else { errors.push(format!("{file}: not found")); // WHOSE package is incomplete. `join_ref` has already collapsed the `..`s the way the - // client does (wow-re `ui/scratch/xml-toc-path-resolution.md` §2), so the resolved - // path is what the real file layer would be handed — and an entry that still points - // inside the addon's own folder is the addon shipping a manifest it does not satisfy, - // while one pointing out of it wants a neighbour that is not installed. + // client does (`0x6ede10`), so the resolved path is what the real file layer would be + // handed — and an entry that still points inside the addon's own folder is the addon + // shipping a manifest it does not satisfy, while one pointing out of it wants a + // neighbour that is not installed. let own = path .strip_prefix(base.as_str()) .is_some_and(|rest| rest.starts_with('/')); @@ -4135,12 +4133,12 @@ mod dependency_tests { /// are this, all one family: `DPSMate_CureDisease.toc` names eight files and the folder /// holds six, because the three `*Received*` ones were split into a sibling addon and the /// manifest was never updated. Our loader is behaving correctly (the reference logs - /// `Couldn't open %s` and carries on — wow-re `ui/scratch/xml-toc-path-resolution.md` §4). + /// `Couldn't open %s` at `0x6edaa0` and carries on). /// - **`foreign`** — the entry escapes the addon's folder with `..`, which the client supports /// and `join_ref` reproduces, into a folder that is not installed. The corpus's two are - /// Auctioneer and BeanCounter reaching `..\Blizzard_AuctionUI\...`, which wow-re records as - /// **RESOLVING** in the real client (§5 case 2, by name) because its file layer can see that - /// folder inside `patch.MPQ`. That one IS ours. + /// Auctioneer and BeanCounter reaching `..\Blizzard_AuctionUI\...`, which the real client + /// **RESOLVES** (`0x6ede10` collapses the `..`) because its file layer can see that folder + /// inside `patch.MPQ`. That one IS ours. /// /// The assertion that matters is that the two never merge: a single "files not found" count /// would average a broken package with a real client gap and read as one number. @@ -4276,9 +4274,9 @@ mod dependency_tests { // **NOTHING is subtracted from `errors`** (1213) — both rows are still there, verbatim, so // every figure any past record quoted is still readable off this list. What changed in // 2155 is which of them `loaded` counts: on the reference these addons LOAD, because - // `0x6edaa0` logs `Couldn't open %s` and the walk carries on with nothing raised - // (`ui/scratch/xml-toc-path-resolution.md` §4). Both halves are asserted together, - // because either one alone is a rule that has already been got wrong in both directions. + // `0x6edaa0` logs `Couldn't open %s` and the walk carries on with nothing raised. Both + // halves are asserted together, because either one alone is a rule that has already been + // got wrong in both directions. for r in [short, wants] { assert!( r.loaded, diff --git a/crates/benilla-app/src/area.rs b/crates/benilla-app/src/area.rs index 756e323b..f599708b 100644 --- a/crates/benilla-app/src/area.rs +++ b/crates/benilla-app/src/area.rs @@ -3,8 +3,8 @@ //! `GetMinimapZoneText`/`GetZonePVPInfo` (decision 0287; corrected to the bytes by its fold-back //! record). //! -//! Byte-verified model (wow-re ui `zonetext-pvpinfo.md`; `0x494780` is the one updater all three -//! zone events fire from, on the per-update area resolve): +//! The model (`0x494780` is the one updater all three zone events fire from, on the per-update +//! area resolve): //! //! - The client caches `(zoneId, zoneText, subzoneText)` and compares per update: **zone id //! changed → `ZONE_CHANGED_NEW_AREA` fires alone**; else **either text changed → @@ -23,10 +23,10 @@ //! ownerless zone** (nil is structural failure only); `factionName` = FactionGroup.dbc's //! localized Name for the zone's mask bit; pvpType is never "arena"; realm type never enters. //! - **The indoor bit** is the player's faces-only down-ray verdict -//! ([`benilla_world::wmo_portal::CurrentAreaInterior`] — wow-re `zonetext-indoor-bit.md`, the CGLight -//! node's `+0x90` bit 0): indoors ⇔ the nearest surface below is a WMO face whose group lacks -//! MOGP `0x8` EXTERIOR. The abbey yard is terrain-below ⇒ outdoors; the flip is the doorway. -//! - **The indoor naming** (`0x67e670` (d), byte-pinned by (d-ii)): while indoors and the hit +//! ([`benilla_world::wmo_portal::CurrentAreaInterior`] — the CGLight node's `+0x90` bit 0, set +//! by `0x6a87f0`): indoors ⇔ the nearest surface below is a WMO face whose group lacks MOGP +//! `0x8` EXTERIOR. The abbey yard is terrain-below ⇒ outdoors; the flip is the doorway. +//! - **The indoor naming** (`0x67e670`): while indoors and the hit //! GROUP is unchanged, the WHOLE updater is skipped (the dedup `[0x868608]`/`[0x86860c]`). On //! an indoor change: **query A** (the whole-WMO −1 row, exact key, `0x69d830`) overrides the //! ZONE slot — only when its resolved name is non-empty AND differs from the current subzone @@ -43,9 +43,7 @@ //! half is modelled where the map data lives, in //! [`crate::ui_world_map::world_enter_selection`] — the gate there is the same precondition //! (`0x67e510` never reaches this updater with a zero zone id), read off our own area feed rather -//! than off the cache below. wow-re carved it as -//! `system/ui/scratch/worldmap-selection-autosync.md`, correcting this note's own earlier source, -//! which had dismissed the call as unrelated. +//! than off the cache below. //! //! The area *authority* stays `terrain_stream::CurrentArea` (decision 0232 — the MCNK `areaId` //! with the WMO-interior override, now off the same faces-only claim). Host globals are written @@ -110,9 +108,8 @@ struct ZoneCache { minimap_text: String, /// The previous update's indoor bit (`[0x868608]`). indoor: bool, - /// The previous hit group's identity (`[0x86860c]` — the client stores the group index, - /// (d-ii); we scope the MOGP uniqueID by WMO): same group while indoors ⇒ the whole updater - /// is skipped. + /// The previous hit group's identity (`[0x86860c]` — the client stores the group index; we + /// scope the MOGP uniqueID by WMO): same group while indoors ⇒ the whole updater is skipped. wmo_id: u32, wmo_group: u32, } @@ -181,13 +178,13 @@ fn feed_zone_events( leaf_row.name.clone() }; - // The indoor naming (`0x67e670` (d)/(d-ii), module doc): dedup-skip while the hit group + // The indoor naming (`0x67e670`, module doc): dedup-skip while the hit group // holds; else the default-row name (query A) may override the zone slot and the group-row // name (query B) re-populates the subzone — both EXACT-key lookups (no name-set retry). let mut wmo_group = 0u32; if let Some((k, group, default)) = world.area_interior_rows() { // The dedup key: the hit GROUP's identity (the client's `[groupRec+0x7c]` group index, - // nonzero-gated — (d-ii)). We key the MOGP uniqueID scoped by WMO id: it changes exactly + // nonzero-gated). We key the MOGP uniqueID scoped by WMO id: it changes exactly // when the hit group does, and — a named divergence — never aliases across two adjacent // buildings the way the client's raw per-WMO index can. wmo_group = k.group_area_id; @@ -400,7 +397,7 @@ mod tests { ); } - /// The indoor naming law (`0x67e670` (d)) against the REAL abbey/inn data shapes — the + /// The indoor naming law (`0x67e670`) against the REAL abbey/inn data shapes — the /// override-skip (whole-WMO name == yard subzone) and the override-fire (inn) branches, /// run through the same WmoAreaCatalog the live feed reads. Skips without client data. #[test] @@ -419,7 +416,7 @@ mod tests { group.area_table_id, 24, "the leaf stays Northshire Abbey (24)" ); - // The dedup rides the hit-group identity ((d-ii): the client's group index; our MOGP + // The dedup rides the hit-group identity (the client's group index; our MOGP // uniqueID) — distinct per room, so a room hop re-fires and standing still skips. let library = cat.group_row(59, 1, 1943).expect("library row"); assert_ne!(group.id, library.id, "distinct rows exist per room"); diff --git a/crates/benilla-app/src/area_trigger.rs b/crates/benilla-app/src/area_trigger.rs index 4e3f74f7..1ef12615 100644 --- a/crates/benilla-app/src/area_trigger.rs +++ b/crates/benilla-app/src/area_trigger.rs @@ -13,8 +13,7 @@ //! //! ## The law, from the reference //! -//! **VERIFIED** against `0x5e2110` (the per-frame check) and `0x5e22d0` (containment), wow-5875-re -//! `object-layer/scratch/w2b1-decomp.c`; the geometry itself is +//! From `0x5e2110` (the per-frame check) and `0x5e22d0` (containment); the geometry itself is //! [`AreaTriggerRow::contains`](benilla_formats::AreaTriggerRow::contains). //! //! - The map's rows are the only candidates (`0x5e2080` narrows the map-sorted table to a diff --git a/crates/benilla-app/src/aura_visual.rs b/crates/benilla-app/src/aura_visual.rs index 9c2184c9..7f4c01f2 100644 --- a/crates/benilla-app/src/aura_visual.rs +++ b/crates/benilla-app/src/aura_visual.rs @@ -9,7 +9,7 @@ //! kit carries no effect models and no anim at all — its entire visual is one CharProc — so the //! effects-only watcher resolved the kit, found an empty slot list, and dropped it on the floor. //! -//! ## The mechanism (VERIFIED — wow-re `ghost-death-visuals.md` §2, `state-kit-aura-lifecycle.md`) +//! ## The mechanism //! //! `UNIT_FIELD_AURA` slot change → the aura watcher `0x604d00` → `0x6123f0` → **`0x5ff350` //! PlayAuraStateVisual**: the slot's spell → `Spell.dbc` → `SpellVisual` → **field 4 = the state @@ -21,10 +21,9 @@ //! - **proc 14 = translucency** (`0x60d972`): the param becomes a node keyed by spell id //! (`node+0x18`, value at `node+0x78`), linked at the HEAD of the unit's list `unit+0xb50`. The //! unit's effective alpha is then recomputed by `0x60d180` as **`baseAlpha × the head node's -//! factor`** — at most one node term, never a product over the chain (wow-re -//! `base-render-alpha.md` §4, correcting this note's earlier `Π` gloss) — `baseAlpha` from the +//! factor`** — at most one node term, never a product over the chain — `baseAlpha` from the //! vtbl+0x6c getter `0x60d2d0` (`CreatureDisplayInfo+0x14 × 1/255`, the SAME slot in the unit -//! and player vtables: "players 1.0" was the data talking, not a type fork — §6) — and handed +//! and player vtables: a player's 1.0 is the data talking, not a type fork) — and handed //! to **`0x614f80` StartAlphaFade(target, 1000 ms)**, the same ramp block (`+0xec..0x100`) and //! the same `clamp01(t)³` ease (`0x614a90`) our appear-fade already rides //! ([`benilla_world::model_fade::fade_alpha`]). Per frame `model+0x180 = master × fade` @@ -41,10 +40,10 @@ //! //! The vis-flag is a decoy and must not be used here: `UNIT_FIELD_BYTES_1` byte 3's CREEP (stealth) //! and GHOST bits drive **no** body render at all — an exhaustive census of every `+0x213` reference -//! found three sites, all nameplate/marker suppression (wow-re `ghost-death-visuals.md` §1). The two -//! co-travel only because the server sets the flag and the aura from the same spell. Keying the body -//! off the flag would be the wrong mechanism that happens to look right on stealth and then fails on -//! every other member of the family. +//! found three sites, all nameplate/marker suppression (`0x5ff80d`, `0x607101`, `0x60f62e`). The +//! two co-travel only because the server sets the flag and the aura from the same spell. Keying +//! the body off the flag would be the wrong mechanism that happens to look right on stealth and +//! then fails on every other member of the family. //! //! ## The shipped data (read from the real 5875 `SpellVisualKit.dbc` this session — //! `benilla-extract charprocs` censuses it) @@ -96,7 +95,7 @@ //! //! Types 2, 7 and 13 also appear on state kits (Berserk/Bloodlust's 2, the Sap/Feign-Death 7, the //! "Glowy (Red)" 13) and have **no verified mechanism** — they fall through [`node_for`]'s single `_` -//! arm, which is where each lands as one match arm the day its RE returns. +//! arm, which is where each lands as one match arm the day its mechanism is known. use benilla_protocol::EntityKind; use bevy::mesh::MeshTag; @@ -154,10 +153,9 @@ pub(crate) struct AuraNodes { /// driver's own again. rate: Vec<(u32, f32)>, /// `baseAlpha`: the display row's `CreatureModelAlpha / 255` — for EVERY unit, players - /// included; the getter `0x60d2d0` has no type fork, a normal character's row just says 255 - /// (`base-render-alpha.md` §6, correcting the "players 1.0" gloss). Owned by - /// [`refresh_base_alpha`], which re-resolves it on every display-id change (a shapeshift - /// swaps it live) and ramps to the new value. + /// included; the getter `0x60d2d0` has no type fork, a normal character's row just says 255. + /// Owned by [`refresh_base_alpha`], which re-resolves it on every display-id change (a + /// shapeshift swaps it live) and ramps to the new value. base: f32, /// Where the ramp is now — what the parts actually render at. current: f32, @@ -188,8 +186,8 @@ impl AuraNodes { /// The reference's `0x60d180`: `baseAlpha × the HEAD alpha node's factor` — at most ONE node /// term, skipped entirely when the list is empty (`0x60d195 je`), never a product over the - /// chain (wow-re `base-render-alpha.md` §4, correcting `ghost-death-visuals.md`'s `Π` gloss). - /// Nodes are linked at the head as they install, so the newest one is the term that counts. + /// chain. Nodes are linked at the head as they install, so the newest one is the term that + /// counts. fn target(&self) -> f32 { let head = self.alpha.first().map_or(1.0, |(_, f)| *f); (self.base * head).clamp(0.0, 1.0) @@ -365,7 +363,7 @@ pub(crate) struct AnimRateFreeze; /// inherit the freeze and outlive it. /// /// **Reasserted every frame, and that is the faithful shape, not a belt-and-braces clamp.** The -/// reference's rate lives on the bone (`+0xb0`, the factor `timebase.md` §2's clock multiplies its +/// reference's rate lives on the bone (`+0xb0`, the factor the clock at `0x71458e` multiplies its /// window by) and **arming an animation does not touch it**: op4 `0x7121a0`'s success leg writes the /// track index and the cursors and leaves `+0xb0` alone (only its disarm leg and `SetBoneAnimSpeed /// 0x712910` write it). So "rate 0 until the aura is reaped" is literally the reference's state @@ -493,7 +491,7 @@ pub(crate) fn node_for(proc: benilla_formats::CharProc) -> Option { /// /// `base_alpha` here only SEEDS a node set created by an aura edge — its steady-state owner is /// [`refresh_base_alpha`], which re-resolves it on every display change. The getter is the same -/// for players and creatures (the display row's `CreatureModelAlpha`; `base-render-alpha.md` §6). +/// for players and creatures (the display row's `CreatureModelAlpha`, read by `0x60d2d0`). /// A unit whose display is unknown falls back to `1.0` — opaque, i.e. the aura's factor alone, /// which is the safe direction (the alternative would hide a unit whose row simply failed to /// load). @@ -566,9 +564,8 @@ fn reap(n: &mut AuraNodes, spell_id: u32, now: f32) { /// Install one spell's nodes, replacing any it already had (an aura re-applied by a second caster /// holds a second slot but installs one node set — the reference's own per-spell-id keying). -/// New nodes link **at the head** (`base-render-alpha.md` §4: the newest node is the one the -/// recompute reads), which is why both lists insert at the front here and both readers take -/// `.first()`. +/// New nodes link **at the head** (the newest node is the one the recompute `0x60d180` reads), +/// which is why both lists insert at the front here and both readers take `.first()`. fn install(n: &mut AuraNodes, spell_id: u32, procs: &[AuraNode], now: f32) { n.alpha.retain(|(s, _)| *s != spell_id); n.tint.retain(|(s, _)| *s != spell_id); @@ -616,10 +613,9 @@ fn trace_edge(what: &str, entity: Entity, spell_id: u32, n: &AuraNodes) { /// `baseAlpha` for a unit — the getter `0x60d2d0`: its CURRENT display row's /// `CreatureModelAlpha / 255`, for **every** unit kind. There is no player override — the same -/// vtable slot in both classes, byte-identical (`base-render-alpha.md` §6); an unshifted -/// character reads 1.0 only because its row (49, 50, …) says 255, and a shaman wearing Ghost -/// Wolf's display 4613 reads 102/255 = 0.4 through exactly this path. No row → 1.0 (the -/// `0x60d2e4` NULL fallback). +/// vtable slot in both classes, byte-identical; an unshifted character reads 1.0 only because its +/// row (49, 50, …) says 255, and a shaman wearing Ghost Wolf's display 4613 reads 102/255 = 0.4 +/// through exactly this path. No row → 1.0 (the `0x60d2e4` NULL fallback). fn base_alpha( entity: Entity, units: &Query<&crate::net::NetEntity>, @@ -646,7 +642,7 @@ fn display_base_alpha( } /// Re-resolve every unit's base alpha when its display changes — the reference's DISPLAYID -/// watcher path (`base-render-alpha.md` §5: `0x604990 → 0x60abe0 → 0x60afb0` caches the row → +/// watcher path (`0x604990 → 0x60abe0 → 0x60afb0` caches the row → /// `0x60ad9f → 0x60d180 → StartAlphaFade(target, 1000 ms)`), self-gated on a real record change /// (`0x60ae10`). This is what makes a unit with `CreatureModelAlpha < 255` translucent with **no /// aura in play** — the shifted shaman's ghost look — and what ramps it back to opaque when the diff --git a/crates/benilla-app/src/aura_visual/tests.rs b/crates/benilla-app/src/aura_visual/tests.rs index 6512b1a0..faca4011 100644 --- a/crates/benilla-app/src/aura_visual/tests.rs +++ b/crates/benilla-app/src/aura_visual/tests.rs @@ -66,8 +66,8 @@ fn the_dispatch_names_only_the_verified_procs() { Some(AuraNode::Alpha(0.0)), "kit 5129's 0.0 is a real value, not an absent one" ); - // 9222653 = 0x8CB9FD — the ghost's pale blue-white, byte-cited in wow-re as 0xFF8CB9FD after the - // client ORs the opaque byte on. + // 9222653 = 0x8CB9FD — the ghost's pale blue-white, 0xFF8CB9FD once the client ORs the opaque + // byte on (`0x60d8cc`). assert_eq!( node_for(proc(char_proc_type::TINT, 9_222_653.0)), Some(AuraNode::Tint([0x8C, 0xB9, 0xFD])), @@ -99,10 +99,9 @@ fn the_dispatch_names_only_the_verified_procs() { // ── The target and the ramp ────────────────────────────────────────────────────────────────────── /// `0x60d180`: the target is `baseAlpha × the HEAD node's factor` — at most one node term, the -/// newest (nodes link at the list head as they install), never a product over the chain (wow-re -/// `base-render-alpha.md` §4, correcting the earlier `Π` reading). Stacking stealth (0.3) then the -/// ghost aura (0.5) reads 0.5 — whichever landed last — and dropping the head hands the term to -/// the next node down. +/// newest (nodes link at the list head as they install), never a product over the chain. +/// Stacking stealth (0.3) then the ghost aura (0.5) reads 0.5 — whichever landed last — and +/// dropping the head hands the term to the next node down. #[test] fn the_target_is_base_times_the_newest_node() { let mut n = AuraNodes::new(1.0); @@ -135,7 +134,7 @@ fn the_target_is_base_times_the_newest_node() { /// The display-swap leg ([`refresh_base_alpha`]'s retarget): a base change rides the same 1000 ms /// cubic ramp an aura node does — Ghost Wolf's 102/255 eases in from opaque, and swapping home -/// eases back — never snaps (`base-render-alpha.md` §5: the fade is part of the mechanism). +/// eases back — never snaps (the fade, `0x614f80`, is part of the mechanism). #[test] fn a_base_change_rides_the_same_ramp() { let wolf = 102.0 / 255.0; diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index 7d1e96f9..ef50cf06 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -585,8 +585,7 @@ fn latch_and_dispatch( // rising edge of `capture.typing`, on a misreading of `0x514490` as "the reference's chat-focus // handler": its sole caller `0x493058` hangs off the CSimpleTop root's WM_ACTIVATE callback // slot (`[root+0x1134]`, event category 2, payload 0 = deactivate), so it is the **OS - // window-deactivate** handler, not a UI-focus one (wow-re `loading-screen-input-law.md`; the - // conflated phrasing was `rf79-autorun-cancel-set.md`'s "Chat EditBox / window focus" row). + // window-deactivate** handler, not a UI-focus one. // In the reference a focused box merely turns the movement handlers into no-ops and the // direction bits are *frozen, not cleared* — so holding W and pressing ENTER keeps you // running, and the world map eating the `M` that closes it keeps you running too. Both @@ -726,7 +725,7 @@ fn latch_and_dispatch( // ── Wheel ── **a notch is a press AND its release, back to back.** The reference builds one // chord and hands it to `CBindings::ExecuteBinding` twice — `isDown=1` at `0x483d6f`, then - // `isDown=0` at `0x483d82` (wow-re `system/ui/ui.md` §3, VERIFIED) — so a `runOnUp` command + // `isDown=0` at `0x483d82` — so a `runOnUp` command // runs both halves in the same frame and a plain one runs its single half (the up leg is the // `RunCommand 0x4b7b50` no-op: `UP + !runOnUp` returns without running anything). Before this // the notch was a press with no release, which quietly made every press+release command a @@ -931,9 +930,10 @@ fn release( /// **Restoring it is not tidiness.** `keystate` is absent from the 1.12.1 client's in-world `_G` /// (`reference/1.12-globals.tsv`) even though its own `Bindings.xml` bodies read it as a bare /// global — which is only possible if the reference sets it transiently around the call, exactly -/// as it does `this`/`event`/`arg1` (`invoke_with_globals`, RF-0025). Leaving it set would hand -/// every addon a global the reference does not have, and an addon that feature-detects it would -/// take a path we cannot honour — decision 1189's "a superset is not free", one call deeper. +/// as it does `this`/`event`/`arg1` (`invoke_with_globals`; `0x703f50` → `0x704f10`). Leaving it +/// set would hand every addon a global the reference does not have, and an addon that +/// feature-detects it would take a path we cannot honour — decision 1189's +/// "a superset is not free", one call deeper. /// /// Save-and-restore rather than set-and-delete, because these bodies nest: a binding whose Lua /// fires another binding must not clear the outer one's `keystate` on the way out. @@ -1159,10 +1159,9 @@ mod tests { /// strata walk carries the key down to `CGWorldFrame`, and `ExecuteBinding` runs `TURNLEFT`. /// The reference's own chat box ships the flag, so this is the default experience. /// - /// benilla read the flag as "consumed but inert, unless Ctrl" until the §5 - /// (`ignorearrows-alt-arrow-gate.md`) corrected both halves — the modifier is ALT, and the key - /// is not consumed at all. Under the old reading, holding LEFT with the chat box open did - /// nothing whatever. + /// benilla read the flag as "consumed but inert, unless Ctrl" until the bytes corrected both + /// halves — the modifier is ALT, and the key is not consumed at all. Under the old reading, + /// holding LEFT with the chat box open did nothing whatever. #[test] fn a_flagged_editbox_lets_the_arrow_keys_through_to_their_bindings() { let mut app = harness(); diff --git a/crates/benilla-app/src/bindings/chord.rs b/crates/benilla-app/src/bindings/chord.rs index e67a43c7..7687d86b 100644 --- a/crates/benilla-app/src/bindings/chord.rs +++ b/crates/benilla-app/src/bindings/chord.rs @@ -176,8 +176,8 @@ pub(crate) fn key_token(k: KeyCode) -> Option<&'static str> { // **On a Mac keyboard the print-screen key IS F13 — read out of the Mac binary now, not // inferred.** The 1.12.1 Mac slice's key table `0x5bf320` maps Mac virtual keycode `0x69` // (F13) to `0x212`, the same `PRINTSCREEN` the Windows table reaches from `VK_SNAPSHOT`. - // (This arm shipped as an inference off `KEY_PRINTSCREEN_MAC = "F13"`; wow-re's - // `keycode-origin-law.md` settled it at the bytes.) macOS has no PrintScreen keycode at + // (This arm shipped as an inference off `KEY_PRINTSCREEN_MAC = "F13"`; the Mac table + // settled it at the bytes.) macOS has no PrintScreen keycode at // all, so without this the byte-real `PRINTSCREEN SCREENSHOT` default is a dead key on // every Mac — not fidelity, a broken key. #[cfg(target_os = "macos")] @@ -252,10 +252,10 @@ pub(crate) fn key_token(k: KeyCode) -> Option<&'static str> { NumpadEqual => "NUMPADEQUALS", NumLock => "NUMLOCK", PrintScreen => "PRINTSCREEN", - // ScrollLock and Pause are deliberately absent: the reference's namer calls their key - // codes (`0x210`/`0x211`) `UNKNOWN`, so they are unbindable there (wow-re - // `keybinding-dispatch-law.md` §2.3), and `IsValidBindingKeyString` would refuse the - // names anyway — arm 4's 26-name table holds neither. + // ScrollLock and Pause are deliberately absent: the reference's namer (`0x4b66b0`) calls + // their key codes (`0x210`/`0x211`) `UNKNOWN`, so they are unbindable there, and + // `IsValidBindingKeyString` (`0x4b7890`) would refuse the names anyway — its 26-name table + // holds neither. CapsLock => "CAPSLOCK", Minus => "-", Equal => "=", diff --git a/crates/benilla-app/src/bindings/commands.rs b/crates/benilla-app/src/bindings/commands.rs index 0f059bb0..b1441b8b 100644 --- a/crates/benilla-app/src/bindings/commands.rs +++ b/crates/benilla-app/src/bindings/commands.rs @@ -1149,10 +1149,10 @@ const TABLE: &[Spec] = &[ // shot's confirmation, then ask the engine. // // `PRINTSCREEN` comes from the SHIPPED default, not a player's cache: `DefaultBindings.wtf` - // lives inside `patch.MPQ` and its line 128 is `bind PRINTSCREEN SCREENSHOT` (wow-re's - // dispatch on this feature; the install's account-ONE `bindings-cache.wtf` agrees). `Edge`, - // not `EdgeUpDown`, is also byte-real: the `` carries no `runOnUp`, and the - // reference's dispatcher returns on key-up unless that flag is set (`0x4b7bea`). + // lives inside `patch.MPQ` and its line 128 is `bind PRINTSCREEN SCREENSHOT` (the install's + // account-ONE `bindings-cache.wtf` agrees). `Edge`, not `EdgeUpDown`, is also byte-real: the + // `` carries no `runOnUp`, and the reference's dispatcher returns on key-up unless + // that flag is set (`0x4b7bea`). // // On a Mac keyboard the token arrives as F13, which is the reference's own Mac mapping rather // than an accommodation (`KEY_PRINTSCREEN_MAC = "F13"`); `super::chord` does the translation. diff --git a/crates/benilla-app/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs index d9b01c8a..25da649c 100644 --- a/crates/benilla-app/src/blob_shadow.rs +++ b/crates/benilla-app/src/blob_shadow.rs @@ -2,8 +2,7 @@ //! creature), the reference's per-frame shadow pass rebuilt on the shared surface-decal projector //! ([`benilla_world::decal`]), drawn on the shared effect stream (0733). //! -//! **The byte-verified mechanism** (wow-re `unit-blob-shadow.md`, a §5 cross-check; the "cloud -//! shadow" label on `0x6d7920` was corrected — it IS the unit shadow draw): +//! **The mechanism** (`0x6d7920` IS the unit shadow draw): //! - **Draw path**: a per-frame pass over registered model nodes (`0x683dd0`, list `[0xc7cb10]`) //! → gate `0x6d78f0` (model streamed, master toggle) → `0x6d7920` → the **same decal chain the //! selection ring uses** (`0x6d7330 → 0x6d6fa0 → 0x6d7480`), collector flags `0x2f0122` = the @@ -11,15 +10,12 @@ //! [`GroundDecalSurface`] set yet — the shadow lands on terrain + WMO faces only). //! - **Frame slot**: PHASE 1, among the opaque drains — `0x6812c5 call 0x683dd0`, fifth of the //! row `0x6812b1`–`0x6812ca` inside `0x681070`, which the driver `0x483460` calls at -//! `0x48361d` (wow-re `water-frame-straddle.md` §1 + `unit-blob-shadow.md` Q1). `0x683dd0` is -//! the **M2 node drain**, and the same loop body ticks each node's object first -//! (`0x48160c call [obj vt+0x38]` → the selection ring) and draws its shadow second -//! (`0x683ec3`) — so per unit the additive ring goes down and this modulate darkens it, never -//! the other way round (wow-re `decal-frame-slot.md`). That second step is **not** a gate: -//! both exits of the tick `0x481540` return 1, so `0x683ea5`'s `je` is dead — an earlier -//! reading of it as "the callback suppresses a hidden object's shadow" was wrong at the bytes -//! and is corrected in wow-re (2026-09-06). What the tick DOES do is write the alpha this draw -//! is about to read, 30 bytes later — which is the whole of §"Appearance" below. So the shadow +//! `0x48361d`. `0x683dd0` is the **M2 node drain**, and the same loop body ticks each node's +//! object first (`0x48160c call [obj vt+0x38]` → the selection ring) and draws its shadow second +//! (`0x683ec3`) — so per unit the additive ring goes down and this modulate darkens it, never the +//! other way round. That second step is **not** a gate: both exits of the tick `0x481540` return +//! 1, so `0x683ea5`'s `je` is dead. What the tick DOES do is write the alpha this draw is about +//! to read, 30 bytes later — which is the whole of §"Appearance" below. So the shadow //! lands after terrain and WMO and **before** everything else: the footprint decals //! (`0x483654`), the M2 opaque pass (`0x4836a6`), the water surfaces (phase 3, drawn *between* //! the two M2 transparent passes) and both of those passes. Every transparent in the world @@ -29,17 +25,17 @@ //! - **Texture**: `Textures\ShadowBlob.blp` — a 32×32 grayscale radial blob (flat gray-160 core, //! linear rim to white) under a binary alpha disc. The reference multitextures a procedural 64×8 //! trapezoid ramp on a second stage (`0x6d81a0`/`0x6d82d0`, blend-mode-selected); its combine -//! wiring is an open RE item (apitrace) — here the ramp is the vertex-alpha vertical fade below. +//! wiring is open (an apitrace question) — here the ramp is the vertex-alpha vertical fade below. //! - **Box law** (`0x711a20` + the `0x6d7920` corner build): a sequence CAaBox, clamped INTO ±5 //! per axis (a cap, never a floor), scaled by the world matrix, yaw-rotated with the unit's //! facing then **axis-aligned-bounded**. Vertical about the model origin: `+1.0·(zExt/2)` up, //! `−(5/3)·(zExt/2)` down. A degenerate horizontal box is the reference's no-op exit (no //! shadow). **No** `OBJECT_FIELD_SCALE_X` re-read (the transform scale already carries it), no //! ring-style `sqrt` compression, no floor. **WHICH sequence — settled at bytes + pixels** -//! (decision 0316; wow-re `27406d9b`, Q3-ORACLE): the draw re-reads +//! (decision 0316): the draw re-reads //! `playableAnimationLookup[0]` every frame — **slot 0 = Stand for characters, from the file //! image, so the value never changes** (not the playing sequence: the director's gait-stable -//! observation falsified that first reading, and the trace oracle confirmed — 1,682 measured +//! observation falsified that first reading, and reference captures confirmed — 1,682 measured //! draws, six bit-stable box sizes, HumanMale 0.9134 × 1.0805 yd permanently, Walk/Run extents //! never appear). Full extents, no missing half/scale factor — the standing size IS the law. //! - **Appearance**: multiplicative darken — `GL_DST_COLOR/GL_ZERO` with the fade riding the @@ -51,7 +47,7 @@ //! touched at exactly three `mov ecx,ebx; call` sites (box `0x711a20`, matrix `0x710600`, //! alpha `0x710ca0`), which makes that list the complete field census. //! -//! **What rides in, at the bytes** (wow-re, 2026-09-06): the 2 s appear ramp and the despawn +//! **What rides in, at the bytes** (2026-09-06): the 2 s appear ramp and the despawn //! ramp (`obj+0xf4`, `0x613b1e`'s `0x7d0`), the self first-person fade, **and the CharProc-14 //! aura transition** — stealth's `0.3`, ghost/invisibility's `0.5`. The aura is not a second //! channel: `0x60d180` drives the SAME `StartAlphaFade` (`0x614f80`) the appear fade uses, and @@ -70,7 +66,7 @@ //! - **Gating**: the reference's `shadowLOD` cvar {0,1} is the master toggle (default on) — we are //! always-on; `shadowBias` (default 0.1) is its depth-bias knob — [`SHADOW_DEPTH_BIAS`] plays //! that role here. No dead/mount/kind test exists on the draw path, and **which** objects -//! register is now settled (wow-re, 2026-09-06 — the old `HANDOFF(-> object-layer)` is closed): +//! register is now settled (2026-09-06): //! `[node+0x90]` bit `0x400` is `NOT(arg bit1)` (`0x670e94`), and `0x613e10` takes that arg off //! `OBJECT_FIELD_TYPE` — `0xb` GAMEOBJECT, `2` DYNAMICOBJECT, `0` otherwise. **GameObjects and //! DynamicObjects never cast a blob shadow; units, players and corpses always do.** Which is @@ -103,7 +99,7 @@ use benilla_world::particles::buffer::{begin_effect_frame, EffectVertex}; use benilla_world::schedule::WorldStage; use benilla_world::view::WorldCamera; -/// The reference's shadow disc (`Textures\ShadowBlob.blp`, wow-re unit-blob-shadow RE): grayscale +/// The reference's shadow disc (`Textures\ShadowBlob.blp`, created by `0x6d8070`): grayscale /// radial blob (gray-160 core → white rim) under a binary alpha disc, multiplied onto the ground. const SHADOW_TEXTURE: &str = "mpq://textures/shadowblob.blp"; /// The byte clamp on the animation box: each corner component is clamped INTO ±5 yd pre-scale @@ -172,8 +168,8 @@ fn setup_shadow_assets(mut commands: Commands, asset_server: Res) { /// Keep one shadow record per eligible unit: spawn for new Player/Unit entities whose model has /// built (an animated model — [`ModelAnimations`] arrives with it), despawn orphans (owner -/// destroyed / streamed out). The registration *policy* is the open RE item; this is the v1 set -/// (see module docs). +/// destroyed / streamed out). The registration *policy* is still open; this is the v1 set (see +/// module docs). #[allow(clippy::type_complexity)] // the filtered spawn-gate query, commented inline fn sync_shadows( mut commands: Commands, @@ -253,7 +249,7 @@ fn update_shadows( // mount's rendered scale while a mount model is attached (the mount IS the footprint on the // ground; the rider's box would undersize it). The mount-vs-body source of the client's own // shadow box is untraced — this is the named approximation of decision 0441's P2, carried - // until a wow-re shadow-consumer trace pins it. + // until that source is known. mount_anims: Query<(&NetEntity, &ModelAnimations), With>, mut shadows: Query<(&BlobShadow, &mut ShadowKey, &mut ShadowVerts)>, // Once-a-second census at debug level (`RUST_LOG=benilla_app::blob_shadow=debug` — the lib @@ -299,7 +295,7 @@ fn update_shadows( Some((mnet, manims)) => (manims, mnet.scale), None => (anims, 1.0), }; - // The byte+pixel law (0316, wow-re 27406d9b): the box is playableAnimationLookup[0]'s + // The byte+pixel law (0316): the box is playableAnimationLookup[0]'s // sequence — Stand, permanently (the reference re-reads it per frame from the file image; // the value can't change). resolve(0) walks the same baked table, so Stand-less models // land on their substitute exactly like the binary's row-0 fast path. @@ -463,8 +459,9 @@ fn update_shadows( .map(|v| format!("({:.3},{:.3})", v.uv[0], v.uv[1])) .collect(); debug!("self shadow uvs: {}", uvs.join(" ")); - // And the box/rect numbers: the oracle says HumanFemale's footprint is 0.77x0.74 yd - // nearly centred; a bigger or offset rect indicts the box math, not the projector. + // And the box/rect numbers: measured on the reference client, HumanFemale's + // footprint is 0.77x0.74 yd nearly centred; a bigger or offset rect indicts the box + // math, not the projector. debug!( "self shadow box: bmin {:?} bmax {:?} rect x [{:.3}, {:.3}] z [{:.3}, {:.3}] \ (extent {:.3}x{:.3}, centre offset ({:.3}, {:.3}))", @@ -568,8 +565,8 @@ fn key_changed(a: &ShadowKey, b: &ShadowKey) -> bool { || a.surfaces != b.surfaces } -/// The reference's trapezoid alpha ramp (`0x6d81a0`/`0x6d82d0`, diffed bit-exact in wow-re: -/// `x = 12·u` — rise `x<2 → x/2`, flat `2≤x<10 → 1`, fall `x≥10 → (12−x)/2`, clamped at 0). +/// The reference's trapezoid alpha ramp (`0x6d81a0`/`0x6d82d0`: `x = 12·u` — rise `x<2 → x/2`, +/// flat `2≤x<10 → 1`, fall `x≥10 → (12−x)/2`, clamped at 0). fn shadow_ramp(u: f32) -> f32 { let x = 12.0 * u.clamp(0.0, 1.0); if x < 2.0 { diff --git a/crates/benilla-app/src/bowstring.rs b/crates/benilla-app/src/bowstring.rs index 617a477d..ff3d0467 100644 --- a/crates/benilla-app/src/bowstring.rs +++ b/crates/benilla-app/src/bowstring.rs @@ -1,14 +1,13 @@ -//! The engine-drawn **bowstring** (wow-re `nocked-ammo-cancel.md` §G2, byte-verified): bow M2s -//! carry NO string geometry — the real client registers a bow-only per-frame draw callback -//! (`0x611ff0`) that spans the bow's `$WTT`/`$WTB` limb-tip event markers with a **2-segment -//! line list**, middle vertex at the character's HandArrow attach while the nock latch -//! (`[+0xd58] & 0x4000`) holds, else the tip midpoint — so the string tracks the draw hand while -//! nocked and relaxes to a straight chord at rest. +//! The engine-drawn **bowstring**: bow M2s carry NO string geometry — the real client registers a +//! bow-only per-frame draw callback (`0x611ff0`) that spans the bow's `$WTT`/`$WTB` limb-tip event +//! markers with a **2-segment line list**, middle vertex at the character's HandArrow attach while +//! the nock latch (`[+0xd58] & 0x4000`) holds, else the tip midpoint — so the string tracks the +//! draw hand while nocked and relaxes to a straight chord at rest. //! //! Benilla's transcription draws the two segments through Bevy's gizmo lines — the same //! immediate-mode screen-space-width primitive class as the client's GX lines. Named -//! deviation (decision record): the string color/width are INFERRED (the callback emits a -//! packed vertex color the round didn't decode — a dark cord is used). +//! deviation (decision record): the string color/width are inferred (the callback emits a +//! packed vertex color that is not decoded — a dark cord is used). //! //! **The tips are POSED** (decision 2281). The bow prop animates — `$BWP` arms BowPull(160) on the //! prop's own model and `$BWR` returns it to Stand(0) — so the `$WTT`/`$WTB` markers ride limb @@ -21,7 +20,7 @@ use bevy::prelude::*; use crate::creature_anim::NockLatch; use crate::entities::BoneAttach; -/// The HandArrow attach id (35 — wow-re §E2/G2): the string's middle control point while nocked, +/// The HandArrow attach id (35 — `0x6121b8`): the string's middle control point while nocked, /// the same point the nocked-arrow model rides. const HAND_ARROW: u16 = 0x23; @@ -55,7 +54,7 @@ fn draw_bowstrings( joints: Query<&GlobalTransform>, mut gizmos: Gizmos, ) { - // A dark waxed-cord tone; the ref's packed vertex color is not decoded (INFERRED, §G2). + // A dark waxed-cord tone, inferred: the ref's packed vertex color is not decoded. const STRING_COLOR: Color = Color::srgb(0.12, 0.10, 0.08); for (bs, prop, vis, flex) in &bows { if !vis.get() { diff --git a/crates/benilla-app/src/camera_shake.rs b/crates/benilla-app/src/camera_shake.rs index 46040f84..b5f56132 100644 --- a/crates/benilla-app/src/camera_shake.rs +++ b/crates/benilla-app/src/camera_shake.rs @@ -3,8 +3,8 @@ //! //! **Four producers, two id spaces, one evaluator.** Every producer ends at the same spawner — //! `AddShake(id, worldPos)` (`0x511d40`), whose five call sites are the whole population (proven by -//! xref sweep, wow-re `spell/scratch/camera-shake-producers.md` §Q4: no address-dword reference to -//! it anywhere, and no Lua, console, cinematic or quake path). +//! xref sweep: no address-dword reference to it anywhere, and no Lua, console, cinematic or quake +//! path). //! //! The **creature** pair names a `CameraShakes.dbc` preset **directly**: //! @@ -33,7 +33,7 @@ //! thumping-giant list; 49 carry one of the two, and 58 of the 1772 kits carry a shake group. See //! `benilla-extract shakecensus`, which maps all four producers onto the 24 presets. //! -//! ## The law (wow-re `ui/scratch/camera-shake-law.md`, §5-verified) +//! ## The law //! //! **Emission — the footstep**, gated in this order (`0x5fbf70`): not hovering · `BYTES_1` byte 3 //! bit `0x2` clear (our stealth bit) · not a player-ghost · **`|camera − footplant|² ≤ 2500` @@ -171,7 +171,7 @@ impl CameraShakes { /// Retire what has expired and compose this frame's offset. /// /// `suspended` is the reference's skip — **two** gates that both jump the same block - /// (`0x50ea87` / `0x50ea8b` → `0x50eb01`, wow-re `camera-shake-law.md` §6): the followed unit + /// (`0x50ea87` / `0x50ea8b` → `0x50eb01`): the followed unit /// is **swimming** (`[[unit+0x118]+0x40] & 0x200000`), or it is riding a **flying server /// spline** (`MI = [[unit+0x118]+0xa4]` non-null, not done, Flying bit set) — a taxi flight. /// The accumulator is zeroed *before* both gates, so a suspended frame yields the offset zero @@ -221,7 +221,7 @@ impl LiveShake { /// This record's `(compare key, signed offset)` at `eye`, or `None` when out of cull range. /// /// The compare key is the **distance-attenuated amplitude** — a per-record quantity that does - /// not swing with the sine — per wow-re's reading of the accumulator's stored slot. + /// not swing with the sine — the stored slot the reference compares at `0x5118a5`. fn sample(&self, eye: Vec3, now: f32) -> Option<(f32, f32)> { let d2 = eye.distance_squared(self.pos); if d2 > CULL_DISTANCE_SQ { @@ -451,14 +451,14 @@ type FollowedUnit = ( /// **Swimming** is its move flags (`[[unit+0x118]+0x40] & 0x200000`). /// /// **The spline gate** is read through to the LIVE path, exactly as the anim selector's `unify` -/// does ([`crate::creature_anim`], RF-0057 `0x5fd19c`) — the reference re-reads +/// does ([`crate::creature_anim`], `0x5fd19c`) — the reference re-reads /// `[[unit+0x118]+0xa4]` every frame rather than trusting a stamped flag, and our stored /// [`MovementState::flying`] is the selector's own derived value, left `false` on the controller's /// component. The [`Spline`]'s *presence* is the reference's "descriptor non-null **and** not done" /// (`sample_splines` removes the component the frame the path completes — the `& 0x4` DONE term), /// and `!grounded` is spline flag `0x200`. /// -/// **`0x200` is the shared fly-*or*-swim spline bit, not "taxi"** (wow-re `swim-transition.md`) — +/// **`0x200` is the shared fly-*or*-swim spline bit, not "taxi"** (`0x616cb0`) — /// so this arm covers a swimming server path as well as a flight, which is the same reason it /// sits beside a swim gate at all. Naming it "the taxi arm" (as 1540 did, and as this was first /// built) over-specifies what the bit means. A **ground** spline — Charge, knockback, a fleeing diff --git a/crates/benilla-app/src/capture/depth_probe.rs b/crates/benilla-app/src/capture/depth_probe.rs index d79689ac..3f06b9d1 100644 --- a/crates/benilla-app/src/capture/depth_probe.rs +++ b/crates/benilla-app/src/capture/depth_probe.rs @@ -200,7 +200,8 @@ struct QuadProbe { corners: [Vec2; 4], /// The NDC depth the corners carry. Read from the projected corners, not assumed constant — /// the *spread* is reported, because a billboard that is not a constant-depth plane is a - /// different bug wearing the same symptom (wow-re `part-flush-emitter-depth.md` §4). + /// different bug wearing the same symptom (`0x7b2a50`'s plain billboard gives all four + /// corners one depth). dquad: f32, dspread: f32, /// The quad centre's distance to the camera plane, yards — the unit the burial is reported in. diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs index a930049d..7b9cd584 100644 --- a/crates/benilla-app/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -771,8 +771,8 @@ pub(super) fn seed_ui_fixture( ..Default::default() }, ); - // The §22 SET block (real Defias Leather shape, 5 members, one equipped): gold - // "(1/5)" header + spacer, cream/gray member ladder, green (2)-bonus vs gray + // The item-SET block (`0x52b650`; real Defias Leather shape, 5 members, one equipped): + // gold "(1/5)" header + spacer, cream/gray member ladder, green (2)-bonus vs gray // (4)-bonus — the whole block's visual regression instrument. let mut inv: benilla_ui::script::InventorySlots = Default::default(); inv[4] = Some(benilla_ui::script::InvSlotView { diff --git a/crates/benilla-app/src/capture/fxview.rs b/crates/benilla-app/src/capture/fxview.rs index 2d6d59cb..e02de46b 100644 --- a/crates/benilla-app/src/capture/fxview.rs +++ b/crates/benilla-app/src/capture/fxview.rs @@ -97,8 +97,8 @@ pub(crate) fn drive_fx_view( .id(); } // `WOW_FX_GO`: the GAMEOBJECT lane. A placed trap/door/chest reaches the screen through - // `crate::go_anim`'s §243 state machine, never through the effect pool, so this is the - // only lane that reproduces one. The descriptor carries the three fields the machine + // `crate::go_anim`'s state machine (`0x5f3cb0`), never through the effect pool, so this is + // the only lane that reproduces one. The descriptor carries the three fields the machine // reads — display, TYPE_ID (the `go_animates` gate) and STATE (the substate) — and // nothing else, so an unset knob renders exactly what an omitted wire field renders. // Always seated on the terrain: a GO stands on the ground. diff --git a/crates/benilla-app/src/capture/mod.rs b/crates/benilla-app/src/capture/mod.rs index 891a1d8b..51e87b91 100644 --- a/crates/benilla-app/src/capture/mod.rs +++ b/crates/benilla-app/src/capture/mod.rs @@ -226,12 +226,12 @@ fn glue_screen() -> Option { /// /// **`WOW_FX_GO=` swaps the subject onto the GAMEOBJECT path** — the /// third lane, and the one a placed trap/door/chest actually takes: the wire component set a -/// streamed GO gets, so `crate::go_anim`'s §243 state machine — not the effect pool — chooses the -/// sequence. `WOW_FX_GO_STATE` (`GAMEOBJECT_STATE`, default 1 READY) and `WOW_FX_GO_TYPE` -/// (`GAMEOBJECT_TYPE_ID`, default 6 TRAP) select the substate; `benilla-extract goanimscan` -/// *predicts* what that resolves to, and this *shows* it. The lane matters because an absent -/// `GAMEOBJECT_STATE` reads as the wire default `0` = ACTIVE, which on a model with no `Opened` -/// sequence lands somewhere else entirely: +/// streamed GO gets, so `crate::go_anim`'s state machine (`0x5f3cb0`) — not the effect pool — +/// chooses the sequence. `WOW_FX_GO_STATE` (`GAMEOBJECT_STATE`, default 1 READY) and +/// `WOW_FX_GO_TYPE` (`GAMEOBJECT_TYPE_ID`, default 6 TRAP) select the substate; +/// `benilla-extract goanimscan` *predicts* what that resolves to, and this *shows* it. The lane +/// matters because an absent `GAMEOBJECT_STATE` reads as the wire default `0` = ACTIVE, which on a +/// model with no `Opened` sequence lands somewhere else entirely: /// /// ```text /// WOW_DATA= WOW_CAPTURE=fxview WOW_FX_GO=3073 WOW_FX_GO_STATE=1 WOW_FX_AGE=4 \ diff --git a/crates/benilla-app/src/capture/probe_bg.rs b/crates/benilla-app/src/capture/probe_bg.rs index 5629b993..42caa922 100644 --- a/crates/benilla-app/src/capture/probe_bg.rs +++ b/crates/benilla-app/src/capture/probe_bg.rs @@ -262,7 +262,7 @@ const GHOST_SAMPLES: u32 = 11; const FLAG_SAMPLES: u32 = 5; /// `SPIRITGUIDE` — `UNIT_NPC_FLAGS` bit 6, the flag the reference's area-spirit-healer acquire -/// scan keys on (wow-re `interact-dead-fork-and-npc-service-ladder.md` §C row 6). +/// scan `0x4924c0` keys on. const NPC_FLAG_SPIRITGUIDE: u32 = 1 << 6; /// The **event tap** — a Lua frame the probe installs on entry that records every battleground diff --git a/crates/benilla-app/src/capture/probe_binder.rs b/crates/benilla-app/src/capture/probe_binder.rs index 5afe9319..1f4ab690 100644 --- a/crates/benilla-app/src/capture/probe_binder.rs +++ b/crates/benilla-app/src/capture/probe_binder.rs @@ -224,7 +224,7 @@ fn last_event(script: &UiScript) -> String { /// The `CHAT_MSG_SYSTEM` lines seen since the hook went in, newest last — step 6's second /// observation. `SMSG_PLAYERBOUND` prints `ERR_DEATHBIND_SUCCESS_S` here (`0x5e3d3f` → -/// `DisplayError(0x138)`, chat type 238 = CHAT_MSG_SYSTEM; wow-re, decision 1335). +/// `DisplayError(0x138)`, chat type 238 = CHAT_MSG_SYSTEM; decision 1335). fn system_lines(script: &UiScript) -> Vec { script .eval::>("return ProbeBinderSystemLines or {}") diff --git a/crates/benilla-app/src/capture/probe_book.rs b/crates/benilla-app/src/capture/probe_book.rs index f125bc10..ad525eb3 100644 --- a/crates/benilla-app/src/capture/probe_book.rs +++ b/crates/benilla-app/src/capture/probe_book.rs @@ -131,7 +131,7 @@ fn reader_state(script: &UiScript) -> (bool, i64) { /// Every string the UI currently draws — the render list is the honest place to ask what the page /// looks like, and the only place since `ItemTextPageText` became a `SimpleHTML` (5875's has no -/// `GetText`; wow-re `simplehtml-markup-engine.md` §5.1). +/// `GetText` in its method table `0x87ba80`). fn drawn_strings(script: &UiScript) -> Vec { use benilla_ui::script::QuadContent; script diff --git a/crates/benilla-app/src/capture/probe_charter.rs b/crates/benilla-app/src/capture/probe_charter.rs index 3aeca3c1..99d83ec9 100644 --- a/crates/benilla-app/src/capture/probe_charter.rs +++ b/crates/benilla-app/src/capture/probe_charter.rs @@ -85,10 +85,9 @@ //! account to sign, and #5 (`ERR_GUILD_FOUNDER_S` on a successful turn-in) needs nine of them — //! neither is reachable from one client, so the whole sign/offer/turn-in half of the family //! (`SignPetition`, `OfferPetition`, `TurnInGuildCharter`, `MSG_PETITION_DECLINE`) still has no -//! live coverage after this probe. #3 (the two closes send nothing) is *superseded*: the wow-re -//! carve that landed alongside this file finds that closing a charter you do NOT own can send -//! `MSG_PETITION_DECLINE`, so this probe deliberately asserts nothing about either close rather -//! than pinning a claim that is being rewritten. +//! live coverage after this probe. #3 (the two closes send nothing) is *superseded*: closing a +//! charter you do NOT own can send `MSG_PETITION_DECLINE` (`0x4f3f60`), so this probe deliberately +//! asserts nothing about either close rather than pinning a claim that is being rewritten. //! //! ## The run recipe //! diff --git a/crates/benilla-app/src/capture/probe_clam.rs b/crates/benilla-app/src/capture/probe_clam.rs index f24774f6..a2216f03 100644 --- a/crates/benilla-app/src/capture/probe_clam.rs +++ b/crates/benilla-app/src/capture/probe_clam.rs @@ -4,7 +4,7 @@ //! The director, 2026-08-22: *"Clams don't open anymore. I right click them and they turn gray //! correctly but no loot window opens ever."* Both halves of that sentence are readings this probe //! takes separately, because they come from two different pre-send writes in the same emitter -//! (`0x5edc80`, wow-re `loot-anim-leg.md` §5 / `inventory-change-failure-display.md` §8): +//! (`0x5edc80`): //! //! - the **grey lock** ([`PendingItemOps`], `0x4953e0` at `0x5edcd9`) — the half that kept working; //! - the **loot latch** ([`LootLatch`], `0x5edcc0`) — the half decision 1477 left unmodelled, on diff --git a/crates/benilla-app/src/capture/probe_goquest.rs b/crates/benilla-app/src/capture/probe_goquest.rs index ff1a5dcd..891fead3 100644 --- a/crates/benilla-app/src/capture/probe_goquest.rs +++ b/crates/benilla-app/src/capture/probe_goquest.rs @@ -90,7 +90,7 @@ const NPC_FLAG_QUESTGIVER: u32 = 0x2; /// `CHAT`, which would need `level > QuestLevel + Quests.LowLevelHideDiff`). const LOW_LEVEL: u32 = 2; const HIGH_LEVEL: u32 = 8; -/// `DialogStatus` ids the verdict is written in (wow-re `questgiver-marker.md` §Q1's own map). +/// `DialogStatus` ids the verdict is written in (the keys of the status map `0x80c454`). const STATUS_UNAVAILABLE: u32 = 1; const STATUS_AVAILABLE: u32 = 5; @@ -319,9 +319,9 @@ fn goquest_probe( Phase::Level { to, since } => { if level == to { // Force the sweep this window depends on. A GameObject GUID reaches the wire ONLY - // from a sweep (wow-re §W14.8), and the level change above is one — but it is a - // no-op when the character already stood at this level, so the epoch bump makes - // the window's sweep unconditional. + // from a sweep (`0x5eb159`, `0x5eb456`), and the level change above is one — but it + // is a no-op when the character already stood at this level, so the epoch bump + // makes the window's sweep unconditional. quest.bump_reask(); info!("PROBE_GOQUEST: at level {to}, swept — reading the window"); probe.phase = Phase::Read { diff --git a/crates/benilla-app/src/capture/probe_mail.rs b/crates/benilla-app/src/capture/probe_mail.rs index ca3085f5..67e6b96e 100644 --- a/crates/benilla-app/src/capture/probe_mail.rs +++ b/crates/benilla-app/src/capture/probe_mail.rs @@ -358,7 +358,7 @@ fn mail_probe( let Some(idx) = index_of(&mail, letter_id) else { return; // shouldn't happen the same frame we just found it — re-poll }; - // CheckInbox() called twice, idempotently (wow-re §5's 60s client-side throttle, + // CheckInbox() called twice, idempotently (`0x4aeab0`'s 60s client-side throttle, // decision 0548 §2/0544) — proves a rapid re-call is a no-op, not a packet storm. if let Err(e) = script.run(&format!("CheckInbox() CheckInbox() GetInboxText({idx})")) { error!("PROBE_MAIL: FAIL (b) — GetInboxText({idx}) errored: {e}"); diff --git a/crates/benilla-app/src/capture/probe_model_camera.rs b/crates/benilla-app/src/capture/probe_model_camera.rs index d0569db5..011b8dce 100644 --- a/crates/benilla-app/src/capture/probe_model_camera.rs +++ b/crates/benilla-app/src/capture/probe_model_camera.rs @@ -4,8 +4,8 @@ //! //! ## What it asks, and why it is a probe rather than a test //! -//! The unit tests in `crate::ui_models` pin the leg's arithmetic — the client's diagonal-FOV -//! matrix against wow-re's worked numbers, and the three cancellations. What they cannot reach is +//! The unit tests in `crate::ui_models` pin the leg's arithmetic — the client's diagonal-FOV matrix +//! (`0x5c3cc0`) against its worked numbers, and the three cancellations. What they cannot reach is //! the half that only exists at run time: does the engine resolve the camera at all, does the //! renderer find the record, does a camera get spawned, aimed, and pointed at a real cell of a //! real atlas. So this probe drives the **whole live pipeline** from Lua and reads the result off @@ -19,11 +19,10 @@ //! - `scale` — `SetModelScale(3)` must not move a pixel. The authored eye is carried through the //! model's root transform, so the camera scales with the model. //! - `position` — `SetPosition(0.4, −0.3, 0.9)` must not move a pixel, for the same reason. -//! - `facing` — `SetFacing(1.0)` must not move a pixel **either**. That one is the finding this -//! work commissioned (wow-re `modelframe-facing-cancels.md`): the up vector `0x7ac640` builds is -//! model-space `+Z` at `roll = 0`, which is the axis the facing turns about, so eye, target, -//! geometry and up all turn together. `modelframe-render-law.md` §2's "only `SetFacing` shows" -//! is scoped to ``'s frozen camera. +//! - `facing` — `SetFacing(1.0)` must not move a pixel **either**: the up vector `0x7ac640` builds +//! is model-space `+Z` at `roll = 0`, which is the axis the facing turns about, so eye, target, +//! geometry and up all turn together. The rule that only `SetFacing` shows is scoped to +//! ``'s frozen camera (`0x7acf10`). //! - `ortho` — the control that must NOT hold: `SetCamera(9)` is past the file's camera count, so //! the widget installs the NULL camera and falls to the orthographic leg, where a facing is a //! plain roll in the screen plane. A probe whose "identical" legs all passed because nothing was @@ -56,8 +55,8 @@ use crate::ui_models::{TilePerspectiveCamera, TileRoot, UiModelTiles}; /// index 1 type 1 at eye `(5.5556, 0, 1.8056)` fov `0.67620`), so it exercises the raw-index /// selection AND has an index past the count for the orthographic control. const FILE: &str = r"Creature\\Wolf\\Wolf.mdx"; -/// The pane's size in FrameXML units — the pet pane's own `318×224`, whose aspect is wow-re's -/// worked example (`camera-law.md` §12.1: `θ = 0.287938 · fov`). +/// The pane's size in FrameXML units — the pet pane's own `318×224`, whose aspect is a worked +/// example of the projection `0x5c3cc0` (`θ = 0.287938 · fov`). const PANE_W: f32 = 318.0; const PANE_H: f32 = 224.0; /// Frames to let a change settle before the numbers are read: the extract republishes the request diff --git a/crates/benilla-app/src/capture/probe_pitch.rs b/crates/benilla-app/src/capture/probe_pitch.rs index 8957be1a..b2698619 100644 --- a/crates/benilla-app/src/capture/probe_pitch.rs +++ b/crates/benilla-app/src/capture/probe_pitch.rs @@ -6,17 +6,17 @@ //! `player::controller`, gated on `mouselook` (a held right button or MOVEANDSTEER) *and* a moving //! OS cursor inside the viewport. An unfocused probe window has neither. So the whole observed-swim //! lane — the wire's pitch tail, the dead-reckon's pitched travel basis, and the body-pitch render -//! law (decision 0464 TU-A) — was **unreachable from any script**: a benilla client could be made -//! to swim, but only ever level. The tilt shipped in July 2026 with no test, no trace field and no -//! way to drive it, which is why "do other swimmers tilt?" had no answer short of the director's -//! eye, and why a two-client probe of it could not be written at all. +//! law (`0x60a110`, decision 0464) — was **unreachable from any script**: a benilla client could be +//! made to swim, but only ever level. The tilt shipped in July 2026 with no test, no trace field +//! and no way to drive it, which is why "do other swimmers tilt?" had no answer short of the +//! director's eye, and why a two-client probe of it could not be written at all. //! //! Format: `WOW_PROBE_PITCH="@[:][;…]"` — `deg` is nose-**up** positive, //! matching `Player::mover_pitch` and the wire's own sign. `"-30@22;30@40"` aims 30° down at //! twenty-two seconds and 30° up at forty; `"-45@20:9"` starts level-ish and sweeps at 9°/s from //! twenty seconds on, which is what a slow real dive looks like on the wire. The last entry whose //! start has passed wins, and it is re-asserted **every frame** — the reference holds an unsteered -//! pitch rather than levelling it (TU-B(c)), so a one-shot write would be indistinguishable from a +//! pitch rather than levelling it, so a one-shot write would be indistinguishable from a //! held one; re-asserting is what makes the two separable when only one of them is under test. //! //! It writes [`Player::aim_pitch`] — the field the reference's `SetPitch` (`0x7c6f70`) writes, diff --git a/crates/benilla-app/src/capture/probe_service.rs b/crates/benilla-app/src/capture/probe_service.rs index e351507a..c73fd120 100644 --- a/crates/benilla-app/src/capture/probe_service.rs +++ b/crates/benilla-app/src/capture/probe_service.rs @@ -3,8 +3,7 @@ //! window instead of a gossip menu?". //! //! 1861 replaced a cursor-kind dispatch with the reference's own first-match-wins walk over -//! `UNIT_NPC_FLAGS` ([`crate::target::click::service_arm`]; wow-re -//! `object-layer/scratch/interact-dead-fork-and-npc-service-ladder.md` §C). **Bit 0 (GOSSIP) is +//! `UNIT_NPC_FLAGS` ([`crate::target::click::service_arm`]; `0x5f0130`). **Bit 0 (GOSSIP) is //! tested first**, so the flag — not the profession — decides: a trainer or questgiver that also //! carries GOSSIP still opens a gossip menu, and only a flagless one opens its own window. That //! precedence is the whole question, and it is invisible to a unit test: the bits come off the diff --git a/crates/benilla-app/src/capture/probe_taxi.rs b/crates/benilla-app/src/capture/probe_taxi.rs index 881d5318..8b97d5c4 100644 --- a/crates/benilla-app/src/capture/probe_taxi.rs +++ b/crates/benilla-app/src/capture/probe_taxi.rs @@ -94,7 +94,7 @@ enum Phase { started_at: f64, last_report: f64, /// Latched true the first in-flight frame the anim pair reads right — rider base - /// Mount(91), mount child base Fly(135) (RF-0057 `0x5fd19c` + the 0441 mount pin). + /// Mount(91), mount child base Fly(135) (`0x5fd19c` + the 0441 mount pin). gait_ok: bool, /// The largest |flying pitch| (radians) seen on the SELF transform mid-flight — the /// `sample_splines` tangent-climb attitude (decisions 0501/0516). The route climbs @@ -307,7 +307,7 @@ fn taxi_probe( } => { let wow = bevy_to_wow(player.pos); if player.server_riding() { - // The anim pair the flight must show (RF-0057 `0x5fd19c` + the 0441 mount pin): + // The anim pair the flight must show (`0x5fd19c` + the 0441 mount pin): // rider base Mount(91), mount child base Fly(135). Latched — the first frames // legitimately lag (mount attach, first selection). let rider = drivers.get(self_entity).ok().map(|d| d.playing().0); diff --git a/crates/benilla-app/src/capture/probes/lift_census.rs b/crates/benilla-app/src/capture/probes/lift_census.rs index 3e5f74df..1d8c0db2 100644 --- a/crates/benilla-app/src/capture/probes/lift_census.rs +++ b/crates/benilla-app/src/capture/probes/lift_census.rs @@ -125,9 +125,9 @@ fn fire_lift_census( for (guid, net, t, store, transport, anchor, seed, vis, inherited, kids, attached) in &entities { let go_type = store.0.gameobject_type_id(); - // Every ticking GO type (the reference's own RF-0051 pair) plus anything already wearing - // a transport component — so a type field we misread still shows up rather than vanishing - // from the instrument that exists to find it. + // Every ticking GO type (the pair the reference's per-frame tick `0x630970` fires) plus + // anything already wearing a transport component — so a type field we misread still shows + // up rather than vanishing from the instrument that exists to find it. if !(net.kind == EntityKind::GameObject && matches!(go_type, 11 | 15) || transport.is_some() || anchor.is_some() diff --git a/crates/benilla-app/src/char_create/mod.rs b/crates/benilla-app/src/char_create/mod.rs index 27044fbc..48e6cc10 100644 --- a/crates/benilla-app/src/char_create/mod.rs +++ b/crates/benilla-app/src/char_create/mod.rs @@ -372,8 +372,8 @@ fn class_file(class: u8) -> &'static str { // ── Input ──────────────────────────────────────────────────────────────────────────────────────── -/// The name box's caret blink clock (the ref's `blinkSpeed`, f32 default 0.5 s — wow-re -/// `rf82-editbox-runtime.md`, period `E+0x370` / accumulator `E+0x374`), reset on every keystroke +/// The name box's caret blink clock (the ref's `blinkSpeed`, f32 default 0.5 s — `0x77a790`, +/// period `E+0x370` / accumulator `E+0x374`), reset on every keystroke /// so the caret is solid while you type. /// /// Its own resource rather than a [`CreateSelection`] field on purpose: ticking it there would trip diff --git a/crates/benilla-app/src/char_select/addons.rs b/crates/benilla-app/src/char_select/addons.rs index a3837807..a6db2ce8 100644 --- a/crates/benilla-app/src/char_select/addons.rs +++ b/crates/benilla-app/src/char_select/addons.rs @@ -389,11 +389,11 @@ impl AddonsPanel { /// Is this addon enabled *for the current view* — the bit the gate is fed? A single character /// reads their own column. The All view answers **"any staged character has it on"**, which - /// 1293 stated as the natural reading of an un-carved mechanism and which the bytes have + /// 1293 stated as the natural reading of an untraced mechanism and which the bytes have /// since confirmed: `AddonList_Update` computes `enabled = (checkboxState > 0)` off /// `GetAddOnEnableState(nil, i)`, whose `1` (enabled for some) therefore counts as on - /// (wow-5875-re `addon-enable-store.md` §4). What 1293 got wrong was not this fold but what - /// each column holds — see [`Self::open_for`] and decision 2311. + /// (`0x51e470`). What 1293 got wrong was not this fold but what each column holds — see + /// [`Self::open_for`] and decision 2311. fn effective_enabled(&self, i: usize) -> bool { self.box_state(i) != BoxState::Off } @@ -604,7 +604,7 @@ pub(super) fn drive_addons_panel( for (entity, action, interaction) in &hovers { // **The scroll bar warps on the PRESS**, and only it: the reference's `CSimpleSlider` // OnMouseDown (`0x789ca0`, 45 bytes, zero branches) warps the value from any press inside - // the hit rect — there is no thumb hit-test in the class (wow-re `ui.md`). Every *button* + // the hit rect — there is no thumb hit-test in the class. Every *button* // in this panel fires on the RELEASE, over the button that took the press (1533). let click = if *action == AddonsAction::ScrollBar { interaction.is_changed() && *interaction == Interaction::Pressed @@ -692,7 +692,7 @@ pub(super) fn drive_addons_panel( } } - // The live `checkAddonVersion` mirror (1293 §5): read per frame like the gate's own per-query + // The live `checkAddonVersion` mirror (1293): read per frame like the gate's own per-query // read (1292 §2.2), so the ForceLoad click above — or any other writer — repaints the // statuses this same frame with nothing rescanned. Absent VM or table = the registrar // default: check ON. @@ -1846,7 +1846,7 @@ mod tests { ); } - /// The All view's gate input is OUR carve (1293): "enabled = any staged character has it on". + /// The All view's gate input is OUR reading (1293): "enabled = any staged character has it on". #[test] fn the_all_view_feeds_the_gate_any_character_on() { let mut p = panel_for( @@ -2019,10 +2019,9 @@ mod tests { /// (`effective_enabled`). One click there would then have written the all-enabled state back /// onto every other character's file: a one-way ratchet. /// - /// The reference does not do this and the mechanism is carved (wow-5875-re - /// `addon-enable-store.md` §4): the enable query counts only nodes with an **explicit** entry, - /// so a character with no file contributes no opinion and inherits the aggregate — here, the - /// unanimous `disabled` the player just saved. Decision 2311. + /// The reference does not do this: its enable query `0x51e470` counts only nodes with an + /// **explicit** entry, so a character with no file contributes no opinion and inherits the + /// aggregate — here, the unanimous `disabled` the player just saved. Decision 2311. #[test] fn a_new_character_inherits_the_disable_it_did_not_ask_for() { let _l = crate::local_state::test_env::ENV_LOCK @@ -2212,8 +2211,8 @@ mod tests { } /// A press on the TRACK — off the knob — warps the knob's centre under the cursor and drags - /// on from there, one gesture. Byte-verified 1.12 (wow-re `slider-mouse-law.md` §1, and the - /// director's own requirement in 0989); the glue bar runs the same law as every in-game one. + /// on from there, one gesture. 1.12 does this (`0x789ca0`, and the director's own requirement + /// in 0989); the glue bar runs the same law as every in-game one. #[test] fn a_press_on_the_bare_track_warps_the_knob_under_the_cursor() { let thumb_n = KNOB / BAR_H; diff --git a/crates/benilla-app/src/char_select/mod.rs b/crates/benilla-app/src/char_select/mod.rs index c64bf506..52540a34 100644 --- a/crates/benilla-app/src/char_select/mod.rs +++ b/crates/benilla-app/src/char_select/mod.rs @@ -212,16 +212,16 @@ pub(crate) struct Roster { /// /// The ref's `SelectCharacter` zeroes the select facing **unconditionally**: `0x472950`'s /// `mov ds:0xb4217c, 0` sits one instruction *above* the already-built discriminator, so it - /// dominates both legs, and the merged tail re-applies it geometrically (wow-re - /// `glue/scratch/glue-preview-facing-law.md`, 1533). A counter rather than change-detection on - /// `selected`, because the engine re-squares on a *re*-selection of the same index too — which - /// is exactly what a roster refresh does, calling it with the index it already holds. + /// dominates both legs, and the merged tail re-applies it geometrically (1533). A counter + /// rather than change-detection on `selected`, because the engine re-squares on a + /// *re*-selection of the same index too — which is exactly what a roster refresh does, calling + /// it with the index it already holds. /// /// **The click is the one caller that does not reach it**, and the gate is in the stock Lua - /// rather than in the engine — see [`Roster::click_row`] and decision 2194. The caller census - /// is wow-re `glue/scratch/select-character-caller-gate.md`: of the ten Lua call sites only the - /// two click handlers are gated, and of `0x472740`'s four C callers two are the roster teardown - /// passing `-1` (so `0x472950` exits above the reset) — every ungated path re-squares. + /// rather than in the engine — see [`Roster::click_row`] and decision 2194. Of the ten Lua call + /// sites only the two click handlers are gated, and of `0x472740`'s four C callers two are the + /// roster teardown passing `-1` (so `0x472950` exits above the reset) — every ungated path + /// re-squares. pub(super) select_seq: u64, /// The guid we answered the IO thread with; `Some` = a login is requested/live. pub(super) pending_pick: Option, @@ -305,9 +305,8 @@ impl Roster { /// So the row you are already on is never re-selected from a click, and the facing zero /// [`Self::select`] owes never fires for it: the angle you dragged the character to survives /// clicking it again. Decision 2194, correcting 1533 — the engine function is unconditional as - /// recorded (verified again, three ways, in wow-re - /// `glue/scratch/select-character-caller-gate.md`), but the click never reaches it: the binding - /// has exactly one live call site, and the two handlers that lead to it both gate. + /// recorded (`0x472740`), but the click never reaches it: the binding has exactly one live call + /// site, and the two handlers that lead to it both gate. pub(super) fn click_row(&mut self, row: usize) { if self.selected != Some(row) { self.select(Some(row)); @@ -376,7 +375,7 @@ impl Roster { // ── The remembered character (`lastCharacterIndex`, decision 1622) ─────────────────────────────── -/// The CVar the select screen remembers you by — a **real 1.12 CVar**, byte-verified in wow-re +/// The CVar the select screen remembers you by — a **real 1.12 CVar** /// (registered at `0x402d93`, name `0x82e8f8`, help "Last character selected", default `"0"`, /// pointer cached at `[0x882674]`), and written engine-side: no shipped GlueXML names it and the /// binary never looks it up by name. @@ -670,9 +669,8 @@ fn back_on_login_refused( /// **skipping `0x41`**, `CHAR_LOGIN_FAILED`, which is reachable only as the default. So the /// tempting `0x3d + byte` is right for four rows and wrong for the rest, and `0` and anything past /// `6` are "Login failed" rather than an out-of-bounds read: the `ja` guard precedes the table. -/// (VERIFIED off `WoW.exe`, cross-checked — wow-5875-re -/// `system/net/scratch/char-login-failed-law.md`. The strings are the shipped -/// `GlueStrings.lua:158-166`, quoted here only as the graceful-absence fallback.) +/// (The strings are the shipped `GlueStrings.lua:158-166`, quoted here only as the +/// graceful-absence fallback.) /// /// Both emulators speak this dialect. vmangos sends a bare `1` for all three of its refusal /// guards (`PlayerLoading() || GetPlayer() || !guid.IsPlayer()`), so **every** refusal from our @@ -764,10 +762,9 @@ fn delete_result( /// success (`0x39`), which the refreshed roster says instead. /// /// The keys are the `CHAR_DELETE_*` block of the reference's status-key table `0x85cae8` -/// (`0x38..=0x3b`, wow-5875-re `system/glue/scratch/login-failure-dialogs.md` §3.1), numbered -/// exactly as vmangos's `ResponseCodes` (`SharedDefines.h`). vmangos sends only two of them: -/// `0x39` on success and `0x3a` for a guild master; its three other refusals (a character still -/// loaded, one not found, one on another account) send nothing at all. +/// (`0x38..=0x3b`), numbered exactly as vmangos's `ResponseCodes` (`SharedDefines.h`). vmangos +/// sends only two of them: `0x39` on success and `0x3a` for a guild master; its three other +/// refusals (a character still loaded, one not found, one on another account) send nothing at all. /// /// **The default arm is benilla's, not the reference's.** The reference indexes the whole 83-row /// table with the byte, so an out-of-block byte would read some other family's sentence (and one diff --git a/crates/benilla-app/src/chat_bubble.rs b/crates/benilla-app/src/chat_bubble.rs index e7c21bc0..0542093f 100644 --- a/crates/benilla-app/src/chat_bubble.rs +++ b/crates/benilla-app/src/chat_bubble.rs @@ -1,6 +1,5 @@ -//! **Chat bubbles** — `CGChatBubbleFrame` (wow-re `object-layer/scratch/chat-bubble.md`, -//! §5-verified 2026-07-11; decision 0288's phase-9 tail, landed by decision 0598): the -//! over-the-head speech bubble a chat line spawns, a **2-D overlay** like the V-plate +//! **Chat bubbles** — `CGChatBubbleFrame` (decision 0288's phase-9 tail, landed by decision +//! 0598): the over-the-head speech bubble a chat line spawns, a **2-D overlay** like the V-plate //! ([`crate::vplates`]) and unlike the world-pass overhead *names* ([`crate::nameplates`]). //! //! The pinned law, transcribed: @@ -31,15 +30,14 @@ //! the **MD20 header image**, i.e. the **Stand sequence CAaBox's Z extent**, a file constant //! with no bone matrix anywhere in its call tree — and the scaled product is **latched** at //! `bubble+0x354` behind a parity guard, so it is queried exactly **once per chat line** -//! ([`crate::entities::StandBoxHeight`], wow-re's anchor cross-check 2026-08-17; 1406). +//! ([`crate::entities::StandBoxHeight`], 1406). //! - **Stacking** (`0x4b1060`, the per-frame pass over ALL live bubbles — one caller, `0x4817a3`): //! each bubble is its own frame and its frame LEVEL is restamped every frame from the CAMERA's //! distance to the speaker. The list re-sorts farthest-first (`0x4b1360`, comparing //! `[bubble+0x350]` = |cam − unit|², written at `0x4b12c0`), then a head→tail walk stamps //! `SetFrameLevel(2), (3), (4)…` (`0x4b1309`). Level outranks draw layer in the client's total //! order, so overlapping bubbles stack as WHOLE cards with the nearest speaker's on top — -//! nothing interleaves. ([`level_z`]; benilla read the bytes for this one, 1504 — wow-re's -//! `chat-bubble.md` covers the spawn/geometry/anchor and never reached the manager tick.) +//! nothing interleaves. ([`level_z`]; benilla read the bytes for this one, 1504.) //! //! Named divergences (all deliberate): //! - ~~**`ChatBubblesParty` defaults ON**~~ — **no longer a divergence (1804).** It shipped `"1"` @@ -48,8 +46,8 @@ //! [`crate::vplates::VPlateMode`], whose enemy plates were the same shape of pin. //! - **The v1 kind set is SAY/YELL/PARTY + monster say/yell.** The byte gate is //! "sender resolves", not a type whitelist, which *implies* guild/officer/whisper/emote -//! bubbles too — but that category claim is INFERRED on an OPEN wire-type remap -//! (chat-bubble.md §1) and contradicts the remembered reference look, so the uncontested +//! bubbles too — but that category claim is inferred, resting on an open wire-type remap +//! (`0x49a870`), and contradicts the remembered reference look, so the uncontested //! set ships and a capture can widen it ([`bubble_cvar`]). //! - ~~**The anchor height is the posed overhead attachment**~~ — **REFUTED and removed (1406).** //! This shipped as an INFERRED equivalence ("both are the head-region attachment height, @@ -91,9 +89,9 @@ use benilla_world::view::WorldCamera; /// they were a pair of `const bool` from 0598 until the options window had a page to put /// them on, which is exactly the shape 1134 calls a row over a frozen gate. /// -/// **Both defaults are the binary's own** (`ChatBubbles` `"1"`, `ChatBubblesParty` `"0"` — wow-re -/// `object-layer/scratch/chat-bubble.md`). `party` shipped ON from 0598 to 1804 on the director's -/// `/p` ask; the row is on the Chat page, one click from where it was. +/// **Both defaults are the binary's own** (`ChatBubbles` `"1"`, `ChatBubblesParty` `"0"`). +/// `party` shipped ON from 0598 to 1804 on the director's `/p` ask; the row is on the Chat page, +/// one click from where it was. #[derive(Resource)] pub(crate) struct BubbleConfig { /// `ChatBubbles` — say/yell and their monster variants. @@ -136,7 +134,7 @@ const BORDER_FRAC: f32 = 16.0 / 1024.0; /// over the frame's bottom edge piece: its art carries the border lines that make the seam read /// continuous, and the reference gets the same result from its own layer key (the tail is an /// ARTWORK texture, the text an ARTWORK font string, and a batch drains every texture before -/// every font string — wow-re `ui/scratch/draw-order-law.md` §4). +/// every font string — `0x76fb00`). const Z_BG: u64 = 0; const Z_EDGE: u64 = 1; const Z_TAIL: u64 = 2; diff --git a/crates/benilla-app/src/cinematic.rs b/crates/benilla-app/src/cinematic.rs index 687a09fe..e0c3bcd1 100644 --- a/crates/benilla-app/src/cinematic.rs +++ b/crates/benilla-app/src/cinematic.rs @@ -9,8 +9,7 @@ //! //! # What the reference does, and what we match //! -//! Byte-verified in wow-re (the cinematic dispatch, 2026-08-29), and the reason each piece here is -//! shaped the way it is: +//! The reason each piece here is shaped the way it is: //! //! - **A trigger that arrives before the world is up is deferred, not dropped.** The reference //! stashes the sequence id in a single-slot latch (`0xc4d75c`) whenever its world-load gate is @@ -50,8 +49,7 @@ //! its *completion callback*: `0x4c0d10` builds a fullscreen opaque-black quad (`0xff000000`) and //! latches what to run when it is fully up, so `CINEMATIC_START`, each shot advance and //! `EndCinematic` all execute at full black and `0x4c1280` fades back in. Six sites, both edges, -//! always black, always 0.25 s (wow-re `ui/scratch/cinematic-camera-law.md` §3.7, a CORRECTION to -//! the earlier "deferred by a delay" reading). There is **no audio fade anywhere on this path**. +//! always black, always 0.25 s. There is **no audio fade anywhere on this path**. //! //! benilla plays and acks immediately. Building the picture is what would let the timing be //! faithful too, so the two go together and neither is faked without the other — decision 1724 @@ -97,10 +95,10 @@ pub(crate) struct Cinematic { /// is **holding at black** until the world around the body is presentable again. /// /// This is the reference's `0x48f080` — EndCinematic's forced terrain load back around the - /// followed unit, which runs at full black and *blocks* (law §8.2, and §8.4: no loading cover - /// is raised anywhere on this path). benilla streams asynchronously and cannot block, so it - /// holds the black the reference was already showing for as long as that load would have - /// taken. Same thing on screen; ours keeps rendering. + /// followed unit, which runs at full black and *blocks* (no loading cover is raised anywhere on + /// this path: its raise, `0x406800`, has no caller there). benilla streams asynchronously and + /// cannot block, so it holds the black the reference was already showing for as long as that + /// load would have taken. Same thing on screen; ours keeps rendering. /// /// The value is the seconds of black left to spend waiting. **Black is never held forever:** /// if the world does not come back inside the budget the fade comes in anyway, and the loading @@ -321,8 +319,7 @@ fn drive_letterbox( // `UiHidden` goes back to meaning only what ALT-Z means. // **The hardware cursor goes with it** — `0x58b590(0)` at StartCinematic, `(1)` at - // EndCinematic and at the leave-world teardown (wow-re `ui/scratch/cinematic-camera-law.md` - // §3.4, the complete 10-site census of the cinematic state cell). Nothing else in benilla + // EndCinematic and at the leave-world teardown. Nothing else in benilla // would: this flag is otherwise written only by the mouse-look session in `player::camera`, // and `control` skips that whole branch while the view is detached, so the pointer the player // arrived at character-select with simply sat on top of the fly-by. @@ -541,8 +538,8 @@ const SETTLE_BUDGET: f32 = 8.0; /// /// **The FOV is not touched at all**, and that is the correction decision 1711 landed. A fly-by is /// rendered through the world camera's own optics, re-stamped every frame — the M2 camera record's -/// `fov` is written at model load and read by nothing on this path (wow-re -/// `ui/scratch/cinematic-camera-law.md`, a 24-site census; its one raw reader is reachable only +/// `fov` is written at model load (`0x70f336`) and read by nothing on this path (its one raw +/// reader, `0x7ac640`, is reachable only /// from the portrait and `` frames). Feeding the authored 45 degrees through the reference's /// own `theta_v = F / sqrt(aspect^2 + 1)` builder — which is otherwise right, and angle-space /// division really is what `0x5c3cc0` does — rendered fifteen of the sixteen shipped shots at @@ -965,11 +962,11 @@ mod ack_ledger { /// **The end boundary holds the black until the world is back around the body.** /// -/// The reference's EndCinematic does a *blocking* terrain load at full black (`0x48f080`, law -/// §8.2) and raises no loading cover anywhere on the cinematic path (§8.4). benilla streams -/// asynchronously, so "block" is not available — but "keep showing the black the fade already put -/// up" is, and it produces the same thing on screen. Without it the fly-by ends into a loading -/// cover for half a second, measured, every time. +/// The reference's EndCinematic does a *blocking* terrain load at full black (`0x48f080`) and +/// raises no loading cover anywhere on the cinematic path (its raise, `0x406800`, has no caller +/// there). benilla streams asynchronously, so "block" is not available — but "keep showing the +/// black the fade already put up" is, and it produces the same thing on screen. Without it the +/// fly-by ends into a loading cover for half a second, measured, every time. #[cfg(test)] mod settling_under_black { use super::*; diff --git a/crates/benilla-app/src/combat_log/chat.rs b/crates/benilla-app/src/combat_log/chat.rs index c8b1d3a1..1fbbf2f4 100644 --- a/crates/benilla-app/src/combat_log/chat.rs +++ b/crates/benilla-app/src/combat_log/chat.rs @@ -69,11 +69,11 @@ impl ChatCtx<'_> { } /// A spell's display name, or `None` when the reference would emit **no line at all** for this - /// spell (§5.5 of the §5 verdict). Two gates, both of which 1571 was missing: + /// spell (`0x62cd80`, like every spell formatter). Two gates, both of which 1571 was missing: /// /// - **`Attributes & 0x180`** — `SPELL_ATTR_DO_NOT_DISPLAY | SPELL_ATTR_DO_NOT_LOG` (the mask - /// and the `SpellRec+0x18` offset are VERIFIED; the two enum names are wow-re's INFERRED - /// corroboration from vmangos). This is what keeps the invisible book-keeping spells every + /// and the `SpellRec+0x18` offset are the binary's; the enum names are inferred from vmangos + /// `SpellDefines.h:799-800`). This is what keeps the invisible book-keeping spells every /// server casts constantly — proc triggers, aura tickers — out of the log entirely. /// - **An empty localized name.** The reference gates on the name it is about to print, so a /// row with no text in this locale produces silence rather than a sentence with a hole in it. @@ -93,8 +93,8 @@ impl ChatCtx<'_> { } /// `0x6ea280 == 2` — **"this spell targets enemies"**, the predicate the chat-type stubs - /// `0x627d30`/`0x627d60` run to choose between a family's `…_DAMAGE` and `…_BUFF` rows - /// (§2.3). It reads the row, never the endpoints, which is why it lives beside + /// `0x627d30`/`0x627d60` run to choose between a family's `…_DAMAGE` and `…_BUFF` rows. + /// It reads the row, never the endpoints, which is why it lives beside /// [`Self::spell_name`] and not beside the classifier. /// /// A row the catalog cannot answer reads as **harmful**, which is not a claim about the @@ -149,7 +149,7 @@ pub(super) fn attacker_state( amount: i64::from(s.damage), // **Only the landed-hit family grows a trailer**, and it is the only family in the whole // block that can show GLANCING/CRUSHING/BLOCK: `0x628410`'s four call sites, and this is - // the one that passes a real `blocked` and a real `HitInfo` (§4.2). A dodge or a full + // the one that passes a real `blocked` and a real `HitInfo` (`0x629ee7`). A dodge or a full // absorb prints no amount at all — the reference does not append to those lines. trailers: landed.then_some(combat::Trailers { absorbed: s.absorb, @@ -260,7 +260,7 @@ pub(super) fn spell_damage_log( return; }; // Nothing through: the reference words it as the reason rather than as a zero, and the order - // of the three tests is its own — absorb, then BLOCK, then resist (§4.3). + // of the three tests is its own — absorb, then BLOCK, then resist (`0x62cd80`). let family = if s.damage == 0 && s.absorb > 0 { combat::SPELLLOGABSORB } else if s.damage == 0 && s.blocked > 0 { @@ -325,8 +325,8 @@ pub(super) fn spell_log_miss( // which routes `0x62bc5a` to `0x62c140` — the damage-shield two-way selector — instead of the // usual eight-row spell matrix. Its five other callers pass 0 and take the normal path. // - // Whether that is deliberate or a 1.12 bug is **not derivable from the binary**, and wow-re - // says so rather than guessing. We reproduce the behaviour, because an addon filtering on + // Whether that is deliberate or a 1.12 bug is **not derivable from the binary**. We reproduce + // the behaviour, because an addon filtering on // chat type has to see what the reference shows; the discriminator, if anyone wants it, is a // live cast that misses and a look at which ChatFrame filter catches the line. 1571 sent these // through `spell_kind` and had them land in `SPELL_*_DAMAGE`. @@ -1214,7 +1214,7 @@ fn power_drain_line( let family = leech_family(multiplier); // Arm 6. Both stubs ask the SPELL, not the family: `0x627d30` is // `call 0x6ea280; cmp eax,2; je -> 0x626be0 (damage) else 0x627820 (buff)` and `0x627d60` is - // its periodic twin over `0x627d80`/`0x6274a0` (§2.3 of wow-re's combat-log chat law). So a + // its periodic twin over `0x627d80`/`0x6274a0`. So a // helpful drain files under `…_BUFF` and a harmful one under `…_DAMAGE`, which is a property // of the row in `Spell.dbc` and not something either call site may hardcode — both of ours // did, in opposite directions. @@ -1433,7 +1433,7 @@ fn queue( } /// [`queue`] for a family whose `Named` slot still has to be looked up — an item entry or a unit -/// guid rides along and the drain resolves it, holding the line until it lands (§5.7). +/// guid rides along and the drain resolves it, holding the line until it lands (`0x6294b0`). fn queue_named( log: &mut ChatLog, ctx: &ChatCtx, @@ -1466,7 +1466,7 @@ fn queue_reported( fills: Fills, named: combat::Named, ) -> bool { - // **The gate is an OR over the two endpoints, not an AND** — the §5 verdict's own wording: + // **The gate is an OR over the two endpoints, not an AND** — `0x626630`'s own test: // `dist²(player, src) < range(srcClass)²` **OR** `dist²(player, tgt) < range(tgtClass)²`. 1571 // required both and therefore dropped lines the client shows: your own pet (range 100000) // fighting something 80 yards away is logged by the reference and was silent here. diff --git a/crates/benilla-app/src/combat_log/text.rs b/crates/benilla-app/src/combat_log/text.rs index 1d44b69f..6f0ee238 100644 --- a/crates/benilla-app/src/combat_log/text.rs +++ b/crates/benilla-app/src/combat_log/text.rs @@ -1,5 +1,5 @@ //! The combat log's **floating number** leg — the combat-text **spawn table** (decision 0137 -//! phase 2; wow-re `worldtext-spawn-and-law.md`). One fn per packet, mirroring the client's +//! phase 2; the callers of `SubmitWorldText 0x6c7840`). One fn per packet, mirroring the client's //! handler → emitter structure; [`super`]'s handlers call it after the chat line: each classifies //! the damage **source** (the color law's `K` — self / owned-by-me / anything else SUPPRESSED), //! resolves the outcome **recipient**, applies Gate A (self-anchored damage text is @@ -73,8 +73,8 @@ pub(super) fn melee_styled(spells: Option<&crate::ui_action::Spells>, spell_id: /// The spell arms' shared `UNIT_COMBAT` split: a landed amount is `WOUND` (`CRITICAL` descriptor /// on crit), zero damage degrades to the full-`ABSORB`/`RESIST` descriptor, else nothing (a clean -/// spell miss arrives via `SMSG_SPELLLOGMISS` instead). PROVISIONAL string mapping pending the -/// wow-re UNIT_COMBAT emission pin (decision 0576). +/// spell miss arrives via `SMSG_SPELLLOGMISS` instead). PROVISIONAL string mapping until the +/// reference's UNIT_COMBAT emission is pinned down (decision 0576). fn spell_feedback( damage: u32, absorb: u32, @@ -92,12 +92,11 @@ fn spell_feedback( } } -/// Spell packet → the center text's messageType (§5-verified against the emission law, wow-re -/// `combat-text-update-emission-law.md`): landed damage is ALWAYS `SPELL_DAMAGE` — **a spell +/// Spell packet → the center text's messageType: landed damage is ALWAYS `SPELL_DAMAGE` — **a spell /// crit does NOT fire DAMAGE_CRIT** (no crit-distinct type exists for spell damage; the `0x62cd80` /// emitter fires 29 regardless, crit changes only the chat template) — zero damage the /// `SPELL_ABSORBED`/`SPELL_RESISTED` word. Spell-side partial trailers stay a named residual (the -/// verdict pinned the melee helper-B partials; the spell emitter's partial path is unread). +/// melee partials go through helper B `0x6268f0`; the spell emitter's partial path is unread). fn spell_center_text( damage: u32, absorb: u32, @@ -114,12 +113,12 @@ fn spell_center_text( } } -/// Melee packet → the center text's messageType + args (§5-verified emission shape, wow-re -/// `combat-text-update-emission-law.md`; fired **synchronously at packet receive** — the -/// impact-keyframe deferral belongs to the worldtext/UNIT_COMBAT victim dispatch, NOT this Lua -/// event). Helper-B partials CONFIRMED: a landed hit with a partial block/absorb/resist fires -/// the word type with `(damage, partial)` — the addon renders `"25 (10 blocked)"`. The -/// partial-vs-crit precedence when both apply is unpinned — partials win here, flagged in 0580. +/// Melee packet → the center text's messageType + args (`0x629d30`; fired **synchronously at +/// packet receive** — the impact-keyframe deferral belongs to the worldtext/UNIT_COMBAT victim +/// dispatch, NOT this Lua event). Helper-B partials CONFIRMED: a landed hit with a partial +/// block/absorb/resist fires the word type with `(damage, partial)` — the addon renders +/// `"25 (10 blocked)"`. The partial-vs-crit precedence when both apply is unpinned — partials +/// win here, flagged in 0580. pub(crate) fn melee_center_text( hit_info: u32, victim_state: u32, @@ -334,9 +333,8 @@ pub(super) fn periodic_aura_log( (action, flags, amount, school) } PeriodicTick::Heal { amount } => ("HEAL", "", amount, 0), - // NO ENERGIZE: 5875 never emits it (the string is absent binary-wide — - // §5-verified, wow-re `unit-combat-event-law.md`); the power gain reaches the - // center text only. + // NO ENERGIZE: 5875 never emits it (the string is absent binary-wide); the power + // gain reaches the center text only. PeriodicTick::Energize { .. } | PeriodicTick::ManaLeech { .. } => continue, }; feedback.write(UnitCombatFeedback { @@ -461,8 +459,7 @@ pub(super) fn spell_heal_log( /// `SMSG_SPELLENERGIZELOG` → the center text's `MANA`/`RAGE`/`FOCUS`/`ENERGY` line (self-only). /// NO `UNIT_COMBAT` fires here: the 5875 engine has no ENERGIZE emission — the string is absent -/// from the whole binary (§5-verified, wow-re `unit-combat-event-law.md`; the shipped -/// CombatFeedback.lua ENERGIZE arm is dead code in 1.12). +/// from the whole binary (the shipped CombatFeedback.lua ENERGIZE arm is dead code in 1.12). /// /// The amount is the **displayed** figure: the handler `0x5e8a90` divides by /// `0x6e7130(powerType)` at `0x5e8af3` and hands that one number to the COMBAT_TEXT push @@ -605,8 +602,8 @@ pub(super) fn xp_gain( } /// `SMSG_EXPLORATION_EXPERIENCE` → the discovery announcement (decision 0828; surfaces and -/// sound byte-verified by the 0829 RE, wow-re `system/net/net.md`). Three legs, mirroring the -/// real handler's case body `[0x5e41d2, 0x5e42bb]`: +/// sound from the reference, 0829). Three legs, mirroring the real handler's case body +/// `[0x5e41d2, 0x5e42bb]`: /// - the ERR_ZONE_EXPLORED toast + (xp > 0) the ERR_ZONE_EXPLORED_XP chat line, queued via /// [`ChatLog::push_exploration`] — the area name is the packet's `AreaTable.dbc` row id /// resolved through the shared catalog; a miss (no catalog, or an id the DBC doesn't know) @@ -658,10 +655,9 @@ pub(super) fn exploration_xp( } /// `SMSG_LEVELUP_INFO` → the ding's chat lines (decision 0304). The talent-count arg is not on the -/// wire — the client computes `(newLevel >= 10) ? 1 : 0` (byte-verified `0x5e407c`, wow-re -/// levelup-ding.md — the 0305 fold-back), exactly this. The ding's VISUAL is deliberately absent -/// here: it rides the UNIT_FIELD_LEVEL change-watcher (`entities` spell_fx::level_up_flash), never -/// this packet. +/// wire — the client computes `(newLevel >= 10) ? 1 : 0` (`0x5e407c`, the 0305 fold-back), exactly +/// this. The ding's VISUAL is deliberately absent here: it rides the UNIT_FIELD_LEVEL +/// change-watcher (`entities` spell_fx::level_up_flash), never this packet. pub(super) fn level_up(l: LevelUpInfo, chat_log: &mut ChatLog) { let talent_points = u32::from(l.level >= 10); // Park the raw tuple for `ui_unit`'s `PLAYER_LEVEL_UP`, whose nine args the reference's own diff --git a/crates/benilla-app/src/combat_text/law.rs b/crates/benilla-app/src/combat_text/law.rs index 3be4afcf..f76658cc 100644 --- a/crates/benilla-app/src/combat_text/law.rs +++ b/crates/benilla-app/src/combat_text/law.rs @@ -118,8 +118,7 @@ pub(crate) const COLOR_PET_MELEE_ORANGE: u32 = 0xFFFF_8400; /// /// Each has a **closed reader census** in the reference: one store and exactly two reads /// image-wide, one in the localized-WORD emitter `0x607140` and one in the `"%d"` NUMBER emitter -/// `0x6128b0` (wow-re `playername/scratch/worldtext-spawn-and-law.md` §4, -/// `combattext-color-law.md`). Both branch targets are function epilogues, so a gate that fails +/// `0x6128b0`. Both branch targets are function epilogues, so a gate that fails /// **suppresses the emit entirely** — it never falls through to a default colour, which is why /// [`damage_color`] returns `Option` rather than a colour. /// @@ -132,7 +131,7 @@ pub(crate) const COLOR_PET_MELEE_ORANGE: u32 = 0xFFFF_8400; /// **`PetMeleeDamage` and `PetSpellDamage` do not split on "melee vs spell" the way a player /// would mean it.** The selector is `B` — no spell record (a real melee swing) *or* the spell's /// `AttributesEx` bit 15 — so a pet spell carrying that bit is gated by `PetMeleeDamage` and -/// coloured orange. Which spell ids carry it is on wow-re's own Open board. +/// coloured orange. Which spell ids carry it is an open question. /// /// These were `const bool`s with the comment "consts until a cvar system exists". The CVar system /// existed; this is the row 2077's census was holding the place for. @@ -267,18 +266,17 @@ fn round_px(t: f64) -> f32 { /// The composed size law (module doc): category value `v` → on-screen pixel height. One gx unit /// is the **screen diagonal** `√(W²+H²)`: the screencoord device space spans `[0,G44]×[0,G48]` /// with `G44 = s/√(s²+1)`, `G48 = 1/√(s²+1)` (`s = W/H` — the live globals `0x832a44/48`), so -/// `v/G48 × H = v·√(W²+H²)`. The first reading hardcoded G48's **4:3 value** (0.6 — the -/// resolution the difftest ran at), which under-sizes ~22% at 16:9 — the director's "damage -/// numbers should be 1–2 sizes bigger", one root cause with the small nameplates (wow-re -/// `nameplate-vkey.md` §8 Q4). The round is the gx px law ([`round_px`]); constant with unit -/// distance. +/// `v/G48 × H = v·√(W²+H²)`. The first reading hardcoded G48's **4:3 value** (0.6), which +/// under-sizes ~22% at 16:9 — the director's "damage numbers should be 1–2 sizes bigger", one +/// root cause with the small nameplates. The round is the gx px law ([`round_px`]); constant +/// with unit distance. pub(super) fn text_px(v: f32, viewport: Vec2) -> f32 { round_px(f64::from(v) * f64::from(viewport.x).hypot(f64::from(viewport.y))) } -/// The shadow's offset — the verified static at `0xce8804` (`{0.002, 0.002}`, init `0x6c7c20`): -/// a **viewport fraction**, resolved per-axis and integer-rounded at draw (module doc; wow-re -/// `worldtext-shadow-render-law.md`, TU-A — which corrected our first `× diagonal` reading: the +/// The shadow's offset — the static at `0xce8804` (`{0.002, 0.002}`, init `0x6c7c20`): +/// a **viewport fraction**, resolved per-axis and integer-rounded at draw (module doc; `0x5c8710`, +/// not our first `× diagonal` reading: the /// `√(W²+H²)` lives only in the unrelated Lua `GetScreenWidth/Height` path). const SHADOW_OFFSET_FRAC: f32 = 0.002; @@ -293,9 +291,9 @@ pub(super) fn shadow_offset_px(viewport: Vec2) -> Vec2 { } /// The anti-overlap CLAIM box (full width × height, px) — the ref's measured block under its -/// own units quirk (wow-re `worldtext-measured-block-wh-law.md`, §5 b2d59e6e): `0x6c81a0` +/// own units quirk: `0x6c81a0` /// stores the halves as SCREEN FRACTIONS — height = the raw size value ÷ G48 verbatim (single -/// line: no line gap, and the shadow-Y term is a byte-verified dead store), width = the advance +/// line: no line gap, and the shadow-Y term is a dead store), width = the advance /// sum ÷ screen width plus a `round(0.002·diag)` pen seed — and `0x6c7cc0` then spends those /// fractions as DDC lengths in the solver rect. A fraction in a DDC slot inflates by diag/dim /// (1/G48 ≈ 1.667× tall, 1/G44 ≈ 1.25× wide at 4:3): the reference's generous, @@ -310,8 +308,8 @@ pub(super) fn claimed_box_px(ink_w: f32, size_value: f32, viewport: Vec2) -> Vec ) } -/// The text and RENDERED shadow alpha bytes at `elapsed_ms` — `time_alpha_fade 0x6c82e0`'s two -/// lanes (§5-verified constants 255.0 / 127.0) composed through the store seam (module doc, the +/// The text and RENDERED shadow alpha bytes at `elapsed_ms` — the alpha fade `0x6c82e0`'s two +/// lanes (constants 255.0 / 127.0) composed through the store seam (module doc, the /// STORE law): the shadow's stored alpha is **`min(shadow lane, text alpha)`** (`SetShadowColor /// 0x5cd650`, font node — SetColor runs first each tick, so `mainA` is the fresh text byte). /// Branch order is the client's: fade-in first (below fade-in-end the ramp arm wins even when @@ -342,7 +340,7 @@ pub(super) fn fade_alpha(cat: &Category, elapsed_ms: f32) -> (u8, u8) { (text, shadow.min(text)) } -/// The category's scale value at normalized life `t` (`keyframe_interp 0x6c80b0`): category 2 runs +/// The category's scale value at normalized life `t` (`0x6c80b0`): category 2 runs /// the crit-pop keyframes × `valueHi`; every other row is the affine `lo + (hi − lo)·t` (constant, /// since `lo == hi` outside the crit row). Floor 0.001, as the client clamps. pub(super) fn scale_value(category: u8, t: f32) -> f32 { @@ -378,9 +376,9 @@ mod tests { /// The composed size law (`v · diagonal` → `ScreenToPixelHeight`): the exact pixel heights /// at the 1024×768 reference window (diag 1280 — where the old `/0.6 × H` law coincides), - /// the round-half-away behavior, and the ASPECT correction (the whole point of the §8 Q4 - /// re-pin: a 16:9 window sizes by ITS diagonal, not the 4:3 constant). - /// The claimed-box anchors from wow-re `worldtext-measured-block-wh-law.md` (b2d59e6e) at + /// the round-half-away behavior, and the ASPECT correction (the whole point: a 16:9 window + /// sizes by ITS diagonal through the live G48 `0x832a48`, not the 4:3 constant). + /// The claimed-box anchors (`0x6c81a0`) at /// 1024×768: the box the solver sees runs 1/G48 taller than the glyph render — 39.1 px for /// the 23 px steady number, 58.7 px for the 35 px crit settle — and 1/G44 (1.25×) wider /// than ink + the `round(0.002·diag)` pen seed. @@ -413,18 +411,18 @@ mod tests { assert_eq!(text_px(scale_value(0, 0.5), ref43), 23.0); } - /// The shadow offset law (`worldtext-shadow-render-law.md` TU-A): a per-axis viewport + /// The shadow offset law (`0x5c8710`): a per-axis viewport /// fraction, gx-rounded — NOT the isotropic diagonal (which overstated the vertical ~2.2× /// at 16:9 and grew with resolution). #[test] fn shadow_offset_is_a_per_axis_viewport_fraction() { - // The verdict's own example: 1920×1080 → {round(3.84), round(2.16)} = {4, 2}. + // 1920×1080 → {round(3.84), round(2.16)} = {4, 2}. assert_eq!( shadow_offset_px(Vec2::new(1920.0, 1080.0)), Vec2::new(4.0, 2.0) ); - // The 4:3 reference window: {round(2.048), round(1.536)} = {2, 2} — the old note's - // "~2.6 px" diagonal read never rendered. + // The 4:3 reference window: {round(2.048), round(1.536)} = {2, 2} — the "~2.6 px" + // diagonal read never rendered. assert_eq!( shadow_offset_px(Vec2::new(1024.0, 768.0)), Vec2::new(2.0, 2.0) @@ -442,9 +440,9 @@ mod tests { assert_eq!(CATEGORIES[5].color, 0xFFE0_CA0A); } - /// `time_alpha_fade 0x6c82e0` at the pinned points of row 0 (in 150 / out 760 / dur 1500), + /// The alpha fade `0x6c82e0` at the pinned points of row 0 (in 150 / out 760 / dur 1500), /// composed through the store seam (rendered values): fade-in ramps 255·t / 127·t over the - /// DURATION (so the fade-in boundary is a step to 255, the byte-verified pop-in), the + /// DURATION (so the fade-in boundary is a step to 255, the pop-in), the /// plateau is the unconditional `(0xFF, 0x7F)`, the fade-out drops the text /// `255 − clamp(255·u)` — and the SHADOW's raw `[128, 255]` inversion is min-capped to the /// text alpha by the `0x5cd650` store, so it steps to ~0xFF at fade-out start and then diff --git a/crates/benilla-app/src/combat_text/mod.rs b/crates/benilla-app/src/combat_text/mod.rs index d4cdacb9..d68816c7 100644 --- a/crates/benilla-app/src/combat_text/mod.rs +++ b/crates/benilla-app/src/combat_text/mod.rs @@ -1,5 +1,4 @@ -//! **Floating combat text** (decision 0137 phase 2) — the client's WORLDTEXTSTRING engine -//! (wow-re `playername/scratch/worldtext-spawn-and-law.md`, §5-verified 2026-07-05). +//! **Floating combat text** (decision 0137 phase 2) — the client's WORLDTEXTSTRING engine. //! //! The law, transcribed: a combat-log SMSG's handler picks a **category 0–5** and submits a string //! over the damage/outcome **recipient**; the text is **world-anchored** — the unit's OVERHEAD @@ -14,10 +13,9 @@ //! head; the XP emitter (category 4) is self-anchored by design and skips the gate. Heals and //! energize NEVER float in 5875 (chat log only; there is no `CombatHealing` cvar). //! -//! **The COLOR law** (wow-re `playername/scratch/combattext-color-law.md`, §5-verified -//! 2026-07-06 — the director's "melee vs spell differ" CONFIRMED): the emitters key two bits — -//! `B` (record NULL ⇒ melee, or **AttributesEx3 bit-15** — `SpellRec+0x24`'s word; the note's -//! "+0x25 sign" byte) and `K` (the `0x5efea0` **source-ownership** class: self / owned-by-me / +//! **The COLOR law** (the director's "melee vs spell differ" CONFIRMED): the emitters key two bits +//! — `B` (record NULL ⇒ melee, or **AttributesEx3 bit-15** — `SpellRec+0x24`'s word, tested as the +//! sign of byte `+0x25`) and `K` (the `0x5efea0` **source-ownership** class: self / owned-by-me / //! other). Master gate: the CombatDamage cvar. Self melee → **WHITE** (NULL override ⇒ the row //! default); self spell/periodic → **GOLD** `0xFFFFDE00`; pet melee → **ORANGE** `0xFFFF8400` //! (PetMeleeDamage cvar); pet spell → GOLD (PetSpellDamage cvar); **any OTHER source is @@ -43,14 +41,14 @@ //! reads "Resist" when the ball lands, a missed Sinister Strike at packet receive. //! [`missile_miss_text`] is that arrival half, fed by [`crate::entities::MissileMiss`]. //! -//! **The render geometry** (wow-re `worldtext-geometry-law.md`, §5-verified, 9af65294 — zero free +//! **The render geometry** (zero free //! parameters; the anchor-seat half corrected here, see the seating comment in //! [`float_combat_text`]): the rise is added to **world z before projection**; the block is //! **h-centered with its bottom at** the projected point (rising above it), clamped on-screen — //! but only once the projector has ACCEPTED the point at all: a number whose anchor lands behind //! the camera or outside the viewport is **destroyed and its slot released** -//! (`6c7d9d` → `6c6dda` → `0x6c86a0`; wow-re `object-layer/scratch/nameplate-offscreen-cull.md`, -//! §5-VERIFIED 2026-08-15), never clamped to a border and never claiming a bucket-1 rect. Ours +//! (`6c7d9d` → `6c6dda` → `0x6c86a0`), +//! never clamped to a border and never claiming a bucket-1 rect. Ours //! clamped, which put another unit's numbers on the screen edge — the worldtext half of 1341's //! phantom plates, and the half that record's §7 wrongly called byte-attested; //! the on-screen size is @@ -58,8 +56,8 @@ //! and composes as `px = round_half_away(v × √(W²+H²))` — `v` the category's interpolated scale //! value, the diagonal the gx screencoord unit (`0x832a44/48` hold the live aspect basis; the //! first reading hardcoded its 4:3 value 0.6 — see [`text_px`]), the round being the gx -//! `ScreenToPixelHeight` law (`0x5c6fa0`, wow-re `crates/font/src/screen_pixel.rs`, bit-exact -//! difftested). At 1024×768 (diag 1280): normal number 23 px, crit settles 35 px, pop peaks ~70 px. +//! `ScreenToPixelHeight` law (`0x5c6fa0`). At 1024×768 (diag 1280): normal number 23 px, crit +//! settles 35 px, pop peaks ~70 px. //! The font is **whatever the Lua global `DAMAGE_TEXT_FONT` holds** when `0x6c8470` reads it — //! see [`DamageTextFont`] for the binding law (a plain `lua_gettable`, evaluated once, at the tail //! of the world-entry UI load, i.e. after every addon's `ADDON_LOADED`). `Fonts\FRIZQT__.TTF` is @@ -68,9 +66,8 @@ //! NULL handle and the combat text simply does not draw. Created flags 0 (`6c8493 xor edx,edx` — //! not `6c8498`, which is `mov ecx,esi`): no outline. //! -//! **The ALPHA + SHADOW law** (`time_alpha_fade 0x6c82e0` — wow-re -//! `playername/scratch/worldtext-alpha-shadow-law.md`, §5 pair + the emulated bit-exact difftest, -//! 2026-07-12; the director's "XP text less visible than ref"): the per-tick fade REPLACES the +//! **The ALPHA + SHADOW law** (the alpha fade `0x6c82e0`; the director's "XP text less visible +//! than ref"): the per-tick fade REPLACES the //! live color's alpha byte (`mov [obj+0x23], bl` — the ARGB high byte), it never multiplies. The //! plateau between fade-in end and fade-out start is an **unconditional `(0xFF, 0x7F)`** — so the //! config table's packed alpha (row 4's `0x80`) is dead data, overwritten before it ever renders: @@ -85,9 +82,9 @@ //! black, `(al<<24)|0` → `SetShadowColor 0x5c27a0` with the static offset `0xce8804 = //! {0.002, 0.002}` (init `0x6c7c20`), every category, unconditionally. //! -//! **The STORE seam caps the shadow** (font node `outline-bake-tint.md` §5, byte-verified): +//! **The STORE seam caps the shadow**: //! `SetShadowColor`'s persistent store `0x5cd650` writes the shadow colour's alpha byte as -//! **`min(shadowA, mainA)`** — and `time_alpha_fade` calls SetColor (text) *then* SetShadowColor +//! **`min(shadowA, mainA)`** — and the alpha fade calls SetColor (text) *then* SetShadowColor //! every tick, so the RENDERED shadow alpha is `min(shadow lane, text alpha)`. The fade-out //! lane's byte-true `[128, 255]` inversion is dead past the text's own value: the rendered //! shadow steps up to ~0xFF at fade-out start, then tracks the text alpha down to **0** — it @@ -96,13 +93,11 @@ //! fraction**, converted per-axis at draw (`0x5c8710` → `ScreenToPixelWidth 0x5c7010` ×W / //! `ScreenToPixelHeight 0x5c6fa0` ×H, each `+0.5`-truncate rounded): `offset_px = //! {round(0.002·W), round(0.002·H)}`, drawn **down-right** (the Y-down ortho), shadow pass -//! before main — the whole as-rendered law is wow-re -//! `playername/scratch/worldtext-shadow-render-law.md` (§5 three pairs, 2026-07-13). +//! before main. //! //! **The anti-overlap push IS real** — decision 0367, superseding 0363's "none exists" (that //! verdict was true of the worldtext code and wrong about the system: the push lives in the -//! shared `UIUtil\SmartScreenRect` draw-time solver, wow-re -//! `ui/scratch/smartscreenrect-solver-law.md`, §5 2026-07-13). Every frame each live string +//! shared `UIUtil\SmartScreenRect` draw-time solver). Every frame each live string //! submits its desired rect — the clamped projected center ± its **crit-pop-scaled** measured //! half-extents — to claim **bucket 1** ([`crate::smart_rect`]), gets relocated off the strings //! already seated this frame, and draws from the returned rect (`0x6c7cc0` → `0x509520`, @@ -187,7 +182,7 @@ pub(crate) struct WorldTexts(Vec); /// pointer is a borrowed `const char*` into the Lua TString, consumed before the call returns. /// `0x6c847c` is the **only** instruction in the image that reads this global. /// -/// **When** is the whole finding. `0x6c8470` is not CRT init, as three wow-re notes had it — it +/// **When** is the whole finding. `0x6c8470` is not CRT init — it /// runs from `0x401570 + 0x1620`, *after* the UI load `0x401602`, so after FrameXML has run /// `Fonts.xml` and after every non-LoadOnDemand addon's `ADDON_LOADED`. That is exactly where /// MikScrollingBattleText (`MikScrollingBattleText.lua:255`) and pfUI (`pfUI.lua:179`) assign it, @@ -341,8 +336,7 @@ pub(crate) fn float_combat_text( order.sort_by_key(|&i| (texts.0[i].anchor, std::cmp::Reverse(texts.0[i].slot))); // A number whose anchor fails the projector's accept verdict is **destroyed, and its slot // released** (`6c7d9d` → `6c6dda` → `0x6c86a0`) — not skipped for a frame, and never clamped - // to a border (wow-re `object-layer/scratch/nameplate-offscreen-cull.md`, §5-VERIFIED - // 2026-08-15). Collected here and reaped after the walk, since the walk holds the list. + // to a border. Collected here and reaped after the walk, since the walk holds the list. let mut culled: Vec = Vec::new(); for i in order { let t = &texts.0[i]; @@ -402,7 +396,7 @@ pub(crate) fn float_combat_text( // half-extents, run it through claim bucket 1 — normalize → solve → clamp → claim // ([`crate::smart_rect`]) — and place the string at the SOLVED rect's center. The ink // then sits h-centered with its BOTTOM at that point: the string's own justify pair at - // creation (`6c8254 push 0x2` in `string_measure_layout`'s `GxuFontCreateString` — + // creation (`6c8254 push 0x2` in the measure layout `0x6c81a0`'s `GxuFontCreateString` — // vertical justify 2 = BOTTOM through `ComputeAnchor 0x5cdf70`'s `anchor.y = h + // anchor.y` arm; horizontal 0 = left, the caller pre-subtracts hw → net h-centered), so // the ink rises above the seat point while the claimed rect brackets it — the ref's @@ -411,7 +405,7 @@ pub(crate) fn float_combat_text( // The claimed box is the ref's MEASURED BLOCK under its own units quirk — the box // the solver sees runs 1/G48 (~1.667×) taller and 1/G44 (~1.25×) wider than the // rendered glyphs, the reference's generous size-proportional padding - // ([`claimed_box_px`], wow-re `worldtext-measured-block-wh-law.md`). + // ([`claimed_box_px`], `0x6c81a0`). let claim = claimed_box_px(b.width(), size_value, viewport); let (hw, hh) = (claim.x * 0.5, claim.y * 0.5); let cx = screen @@ -663,8 +657,8 @@ mod tests { } /// **A travelling spell's outcome word is DEFERRED, and it is GOLD** (decision 2229) — the - /// arrival half of the miss text, and the case the whole round came from: a resisted Fireball - /// reads "Resist" in spell gold when the ball lands, not white at GO. + /// arrival half of the miss text: a resisted Fireball reads "Resist" in spell gold when the + /// ball lands, not white at GO. /// /// Four legs, each an independent branch of the law `0x607140` runs for every one of its /// callers: diff --git a/crates/benilla-app/src/console.rs b/crates/benilla-app/src/console.rs index 00bb6a44..161b8fdc 100644 --- a/crates/benilla-app/src/console.rs +++ b/crates/benilla-app/src/console.rs @@ -1,6 +1,6 @@ //! The in-game **console command registry** (decision 2303) — the reference's `ConsoleCommand` //! table, host side: `ConsoleCommandRegister 0x63f9e0` over a `TSExplicitList`, -//! with parse / register / lookup / execute (wow-re `console/console.md`, the upper band). A +//! with parse / register / lookup / execute (the registry runtime, `[0x63f880, 0x640c50)`). A //! subsystem registers its commands from its own plugin ([`ConsoleCommandApp::console_command`]), //! the way each of the reference's registers into the one table, and `/console ` reaches //! [`execute`] through the chat drain. diff --git a/crates/benilla-app/src/creature_anim.rs b/crates/benilla-app/src/creature_anim.rs index 3d23fc4f..06438e09 100644 --- a/crates/benilla-app/src/creature_anim.rs +++ b/crates/benilla-app/src/creature_anim.rs @@ -8,8 +8,8 @@ //! //! Regimes (decisions 0049 → 0083/0087): //! - **Gait** — the directly-cross-faded ground/swim gaits + idle: Stand / Walk / Run / Fast-run by speed, -//! WalkBackwards when moving back, the swim ids when swimming (verified client selector, wow-5875-re -//! RF-0057). A change cross-fades to the new clip over its `blend_time` instead of snapping. +//! WalkBackwards when moving back, the swim ids when swimming (the client's selector `0x5fd8b0`). +//! A change cross-fades to the new clip over its `blend_time` instead of snapping. //! - **Special** — the stand-state poses (sit/sleep/kneel: down → loop → up) as one-shot-bracketed //! loops, **preemptible** (decision 0055), plus the jump's enter/hang. The jump's *landing* is NOT a //! bracket: [`select::Mode::Land`] is a plain pick from the input at touchdown, freely overwritten the @@ -20,7 +20,7 @@ //! torso-masked overlay ([`AnimClip::upper_node`]) while the base keeps driving the legs underneath — //! never dropped, never bracket-gated. //! -//! The playback **rate** of a locomotion clip is `speed / sequence.moveSpeed` (wow-5875-re `0x5fe2f0`), so +//! The playback **rate** of a locomotion clip is `speed / sequence.moveSpeed` (`0x5fe2f0`), so //! a unit moving faster than the clip's design speed cycles its legs proportionally faster (the fix for a //! backpedal that otherwise drags). Non-locomotion clips (idle, the jump/sit transitions — `moveSpeed 0`) //! play at 1×. Death (decoded health 0) overrides everything. @@ -42,9 +42,9 @@ use bevy::prelude::*; use benilla_assets::AssetSet; use benilla_world::schedule::WorldStage; -/// The pure animation-selection logic (RF-0057/0073 tables, movement/Special state, gait/swing/ready -/// picks, playback-rate math) — kept in its own file as it carries the bulk of the unit-tested selector -/// logic, separate from the Bevy driver systems in [`driver`]. +/// The pure animation-selection logic (the `0x5fd100`/0073 tables, movement/Special state, +/// gait/swing/ready picks, playback-rate math) — kept in its own file as it carries the bulk of the +/// unit-tested selector logic, separate from the Bevy driver systems in [`driver`]. pub(crate) mod select; pub(crate) use select::{ ease_strafe_yaw, move_flags, strafe_body_offset, swim_body_rotation, MovementState, @@ -59,8 +59,8 @@ pub(crate) use twist::{wrap_pi, BodyTwist}; /// The unit animation-LOD gate (decision 0448): park an off-frustum rig's per-bone pose /// evaluation — the clocks, the driver state machine, and the event tracks keep running, so /// off-screen combat stays audible (0075) and a re-appearing unit snaps to the absolute-clock -/// pose. Shipped as a modernization; wow-re's 2026-08-13 election correction -/// (`outdoor-object-pass-election.md`) made it the faithful direction instead — decision 1473 +/// pose. Shipped as a modernization, it is the faithful direction instead: the reference does not +/// tick an off-frustum unit (`0x683dd0`'s second walk) — decision 1473 /// records that, and what still diverges (parked rigs keep drawing; the reference keeps only /// `MORE_AUDIBLE` creatures audible off-screen). mod lod; @@ -86,8 +86,8 @@ pub(crate) struct Wielded { /// ([`select::ranged_load_anim`], the client's `0x5fd530` LUT; decision 0099 phase 5). pub(crate) ranged: Option<(u8, u8)>, /// The mainhand/offhand items' sheath *types* (1 back-2H · 2 back-staff · 3 hip · 4 shield · - /// 0 none) — each picks its arm's draw/stow one-shot independently (VERIFIED wow-re - /// `sheath-anim-pick.md`: `(1 << (rec[+4] & 0x1f)) & 0x88` → HipSheath 90 for types {3, 7}, + /// 0 none) — each picks its arm's draw/stow one-shot independently (`0x611930`: + /// `(1 << (rec[+4] & 0x1f)) & 0x88` → HipSheath 90 for types {3, 7}, /// Sheath 89 otherwise; the slot's own byte is the *only* input). pub(crate) main_sheath: u8, pub(crate) off_sheath: u8, @@ -129,8 +129,7 @@ pub(crate) const UNIT_FLAG_DISARMED: u32 = 0x0020_0000; pub(crate) const ITEM_CLASS_WEAPON: u8 = 2; /// **The disarm ladder** — which single held slot `UNIT_FLAG_DISARMED` hides, given what each -/// melee hand holds (decision 1863; wow-re `disarm-weapon-gate-law.md` §2, byte-verified by a -/// trio including a cold decoder-first read, both class bodies identical). +/// melee hand holds (decision 1863; both class bodies identical). /// /// The reference does not "empty a disarmed unit's hands". `GetWeapon(slot, visFlag = 0)` runs a /// **two-probe ladder with main-hand precedence**, and the off-hand probe's polarity is the @@ -198,46 +197,44 @@ impl Wielded { /// **It carries that GUID**, because the reference's `[+0xc48]` is the target and not a flag, and /// a second reader wants the unit and not just the fact: `0x6e3480`'s melee arm resolves /// `[caster+0xc48]` and uses THAT unit's combat reach, which is what the spell tooltip's range -/// cell prints while you are auto-attacking (wow-re -/// `tooltip-damage-matrix-and-container-slots.md` §D4.2b). Every animation reader still asks only +/// cell prints while you are auto-attacking. Every animation reader still asks only /// `With`/`Has`, which is unchanged by the payload. #[derive(Component)] pub(crate) struct Engaged(pub(crate) u64); /// The local player has fired an auto-repeat spell (Auto Shot / wand Shoot) — the client's /// `[+0xd58] & 0x200`, whose **only writer binary-wide** is the local cast-send tail (`0x6e593b`, -/// gated `AttributesEx2 & 0x20`; byte-verified, wow-re `ranged-shot-anim.md`). While it and the +/// gated `AttributesEx2 & 0x20`). While it and the /// ranged **sheath** gate (`CUR == 2`) both hold, a standing unit's base idle is the ranged /// weapon's Load clip ([`select::ranged_load_anim`]) — the client's resolver `0x5fd460`, /// which gates on exactly this bit; local-player-only by construction (remote units never run /// the local cast-send). A REMOTE shooter has no drawn idle at all: it plays LoadBow **once** at -/// the volley's single `SMSG_SPELL_START` through the PrecastKit ([`CastHold`]), then the fire -/// clip per GO over its ordinary idle (wow-re `shooter-stop-law.md` §J6 claim 2). Cleared +/// the volley's single `SMSG_SPELL_START` through the PrecastKit ([`CastHold`], `0x6e7901`), then +/// the fire clip per GO over its ordinary idle. Cleared /// only by [`cancel_auto_repeat_local`]'s callers — the client's cancel `0x6ea080` (its `0x200` /// clear `0x6ea113`), reached from the cast-result fail of the cached spell, the button /// re-press toggle, melee attack-start, target death, the wand-only new-cast handoff, and /// `SMSG_CANCEL_AUTO_REPEAT` — which vmangos DOES send (corrected 2026-08-05; see /// `crate::spell::net`'s `cancel_auto_repeat`), so against a live server that packet, not -/// a local death watcher, is what ends a volley whose target dies (wow-re -/// `nocked-ammo-cancel.md`). +/// a local death watcher, is what ends a volley whose target dies. #[derive(Component)] pub(crate) struct AutoRepeatArmed; /// The weapon-visual hold — the client's `[+0xd58] & 0x400`, set for **ANY caster** (self, /// remote players, NPCs) whose **ranged** spell visual plays (`0x60d020`, sole caller `0x6ec2cf` -/// inside `PlaySpellVisual`; byte-verified, wow-re `ranged-sheath-exempt-autorepeat.md` §Q4). +/// inside `PlaySpellVisual`). /// Cleared like the client's id-matched `0x60d040` + its unconditional sibling `0x60fc50`: a /// **non-ranged** visual play by the same caster (the stale-visual cleanup `0x6ec39e`), the -/// local cancel (`0x6ea12b`), melee attack-start, and the spline-move apply (`0x6020e8`, wow-re -/// `shooter-stop-law.md` §J1.3). Deliberately NOT cleared on sheath change or volley end — the +/// local cancel (`0x6ea12b`), melee attack-start, and the spline-move apply (`0x6020e8`). +/// Deliberately NOT cleared on sheath change or volley end — the /// client's bit persists latent. /// /// **It gates nothing here — but no longer for the reason this doc used to give.** `0x400` is /// tested in exactly one place image-wide: `0x5fc3f0`'s Hold self-loop gates (`test ah,0x6` for /// HoldBow 109, `test ah,0x4` for 110/111/112). 0994 recorded that the dispatcher is never -/// reached for a bow id (`shooter-stop-law.md` §J4.1) and concluded the bit is dead. **wow-re's -/// §5 refuted that absence proof** (decision 1544): the dispatcher has a second, deferred fire -/// site, so those gates DO execute — they are the Hold's own per-completion re-arm. +/// reached for a bow id and concluded the bit is dead. **That absence proof is wrong** +/// (decision 1544): the dispatcher has a second, deferred fire site (`0x7075af`), so those gates +/// DO execute — they are the Hold's own per-completion re-arm. /// /// We still gate nothing on it, and that is now a *modelling* choice with a stated equivalence /// rather than a claim about the binary. Our Hold is armed once and loops until a real recompute @@ -251,10 +248,9 @@ pub(crate) struct AutoRepeatArmed; pub(crate) struct RangedHold; /// A unit's displayed **nocked ammo** — the client's `[+0xd28]`/`[+0xd2c]` ammo-model pair, -/// written only by `CGUnit::UpdateAmmoDisplay 0x60ba30` (byte-verified, wow-re -/// `nocked-ammo-cancel.md`): the ammo's `ItemDisplayInfo` id, rendered as a model on the unit's -/// **body** at HandArrow (35) — the ONE bone attach, bow-only (§E2; the old Special2/Special3 -/// reading is refuted — those are model-directory selectors, §E1). Set per shot from the +/// written only by `CGUnit::UpdateAmmoDisplay 0x60ba30`: the ammo's `ItemDisplayInfo` id, rendered +/// as a model on the unit's **body** at HandArrow (35) — the ONE bone attach, bow-only (`0x60bb19`; +/// `0x479f40`'s 0x18/0x19 pick a model directory, not Special2/Special3). Set per shot from the /// `SMSG_SPELL_START` `CAST_FLAG_AMMO` tail (any caster — the only source for a remote/NPC /// shooter). This is the display-id CACHE; whether the arrow is visibly in the hand is the /// [`NockLatch`] (the `$BWP`/`$BWR` cycle). Removed by the un-nock `0x60f530` paths: the local @@ -265,7 +261,7 @@ pub(crate) struct NockedAmmo { pub(crate) display_id: u32, } -/// The client's nock latch `[+0xd58] & 0x4000` (wow-re `nocked-ammo-cancel.md` §G1/G4): SET at +/// The client's nock latch `[+0xd58] & 0x4000` (set `0x624b70`, cleared `0x60016c`): SET at /// the body clip's **`$BWP`** BowPull keyframe (LoadBow 105 carries it at ~0.6 s — the visible /// pull-from-quiver moment, which also (re)attaches the in-hand arrow), CLEARED at **`$BWR`** /// BowRelease (AttackBow 46 carries it at ~0.067 s — the arrow leaves the hand as the missile @@ -301,8 +297,8 @@ pub(crate) fn drive_nock_latch( } } -/// The one local auto-repeat cancel — benilla's `0x6ea080` (byte-verified whole, wow-re -/// `nocked-ammo-cancel.md`): clear the live key (`[0xceac30]`), drop the Load/Hold idle gates +/// The one local auto-repeat cancel — benilla's `0x6ea080`: clear the live key (`[0xceac30]`), +/// drop the Load/Hold idle gates /// (the `0x200`/`0x400` bits — [`AutoRepeatArmed`] + [`RangedHold`]), un-nock the ammo /// (`0x60f530` — [`NockedAmmo`]), and ack the server (`CMSG_CANCEL_AUTO_REPEAT_SPELL`, the /// client's sole send site `0x6ea0c6`) — **sheath untouched**: the weapon stays drawn, the idle @@ -327,8 +323,7 @@ pub(crate) fn cancel_auto_repeat_local( let _ = net.0.send(crate::net::ClientCommand::CancelAutoRepeat); } -/// The one local **StopAttack** — benilla's `0x5ecac0` (wow-re §5 trio-verified, -/// `object-layer/scratch/melee-autorepeat-exclusion.md`): player-only, a no-op unless an attack +/// The one local **StopAttack** — benilla's `0x5ecac0`: player-only, a no-op unless an attack /// lock is held, then `CMSG_ATTACKSTOP` (its builder `0x624370` pushes opcode `0x142`) followed by /// the tail jump `0x5ecb63 → CancelQueuedCast 0x6e6f30` — which takes the **queued on-next-swing /// spell** down with the attack: `0x6e6f30`'s first leg tests the inflight id `[0xceca88]` for @@ -369,8 +364,8 @@ pub(crate) fn stop_attack_local( } } -/// The one local **StartAttack** — benilla's `0x5ecb70`, [`stop_attack_local`]'s twin (wow-re §5 -/// `melee-autorepeat-exclusion.md` §5c–§5f). Two halves, and keeping them apart is the whole point: +/// The one local **StartAttack** — benilla's `0x5ecb70`, [`stop_attack_local`]'s twin. Two halves, +/// and keeping them apart is the whole point: /// /// - **the send** (`0x5eccfd`) is gated by the already-attacking test at `0x5eccda` — skipped when /// the attack lock is held, *unless* a stop is in flight (`[+0xc54]`), which is why @@ -433,7 +428,7 @@ pub(crate) fn start_attack_local( /// pseudo-spell runs (`TryCast`'s effect-`0x4e` short-circuit at `0x6e4c7a` dispatches straight to /// it, ahead of every cast gate). It forks on the attack lock and nothing else: already attacking → /// `0x6131d9 call 0x5ecac0` [`stop_attack_local`], otherwise `0x6131ee call 0x5ecb70` -/// [`start_attack_local`] (wow-re `melee-autorepeat-exclusion.md` §5f). +/// [`start_attack_local`]. /// /// The fork is the whole content, and it is worth a name because both halves used to be wrong here: /// the button never toggled melee OFF, and it cancelled a running auto-repeat on *every* press — @@ -530,7 +525,7 @@ pub(crate) struct Casting { /// The client's PlayAnimation **call order**, reconstructed. The real client has no pecking /// order between a swing and a spell kit's anim landing in the same frame — its handlers run /// synchronously in packet order and the LATER `PlayAnimation` call simply overwrites bone 0 -/// (wow-re, the Eviscerate-timing round's arm C). benilla's swing and cast pipelines are +/// (`0x5fe2f0`). benilla's swing and cast pipelines are /// separate message streams, so that order must ride the messages: every emitter of an /// animation-bearing request ([`SwingMessage`], [`CastEvent`], [`EmoteAnim`], [`KitPush`]) /// stamps `next()` at emission — the wire drain stamps in packet order; scene-time emitters @@ -581,13 +576,13 @@ pub(crate) enum CastEventKind { Impact { weapon_visual: Option }, /// A projectile arrived at a **ground point** instead of on a unit — the client's per-tick /// missile dispatch taking `0x61e1d0` → `0x61d870`, which is the **no-live-target** arm, not - /// a ground arm as such (a target that despawned mid-flight lands there too; wow-re - /// `missile-arrival-dispatch.md`). Ours is reached only by the single + /// a ground arm as such (a target that despawned mid-flight lands there too). Ours is reached + /// only by the single /// missile a dest-targeted GO with an empty hit list launches /// ([`spell_visual::MissileSpawn::ground_aim`]). `entity` is the **caster** — `0x61d870` /// plays the kit on it, not on anything at the point — and `pos` is the arrival position, /// the client's `extra` override (`0x61d8e7`: `lea edx,[esi+0x20]`). Plays `SpellVisual` - /// **field 13 at stage 3** (wow-re `spell-visual-lifecycle.md` §Q4). + /// **field 13 at stage 3**. GroundImpact { pos: Vec3 }, } @@ -605,8 +600,7 @@ pub(crate) struct CastHold { pub(crate) spell_id: u32, /// A ranged-slot shot's wind-up (`Attributes & 0x2` — Throw's ReadyThrown, the bow's /// LoadBow): the client brackets every ranged kit play with ranged snaps (`0x60f34c` - /// before, the outer `0x6e5930`/`0x6e78f3` after — wow-re - /// `ranged-sheath-exempt-autorepeat.md`), so while this hold is live the sheath + /// before, the outer `0x6e5930`/`0x6e78f3` after), so while this hold is live the sheath /// reconcile's force-stow never survives the frame ([`driver`]'s bracket override). pub(crate) ranged: bool, } @@ -614,7 +608,7 @@ pub(crate) struct CastHold { /// Per-hand weapon-grip state: whether a weapon is held in the right/left hand, so that hand's fingers /// curl into a grip. The real client arms `HandsClosed` (AnimationData 15) on a hand's finger key-bones /// **purely by that hand's attach point being occupied by a weapon** — not combat, not sheath state; a -/// forearm-mounted shield or an empty hand stays open (wow-re `hand-grip-mechanism.md`, `0x60b590` / +/// forearm-mounted shield or an empty hand stays open (`0x60b590` / /// paperdoll `0x5059a0`). Written by the held-item resolver ([`crate::entities`], decision 0072) from the /// weapon's hand attach point; read by [`driver::drive_hand_grip`] to hold/release the finger overlay. #[derive(Component, Default, Clone, Copy, PartialEq, Eq)] @@ -681,7 +675,7 @@ pub(crate) struct EmoteAnim { } /// A **state kit's animation id, which is a comparison and never a play** — `PlaySpellVisualKit`'s -/// stage-2 leg (VERIFIED, wow-re `state-kit-anim-and-stun-pose.md` §1/§2; decision 2085). +/// stage-2 leg (decision 2085). /// /// `0x60edf0`'s tail has exactly one site that hands a kit's `+0x8` to the play primitive /// (`0x60f3c5 call 0x5fe2f0`), and **stage 2 is diverted around it**: `0x60f387 jne` takes the @@ -695,8 +689,8 @@ pub(crate) struct EmoteAnim { /// It is therefore an animation-**cutting** mechanism, and the cut is not incidental: Charge /// (22911) plays `Knockdown`(121) from its impact kit and then cuts it with its own state kit's /// `Stun`(14), because 121 ≠ 14 forces the recompute. That is also why no stun ever shows the -/// `Stun` pose despite 23 kits naming it — the selector `0x5fd8b0` cannot produce 14 (its literal -/// outputs are enumerated in the note), so the recompute lands on `Stand`. +/// `Stun` pose despite 23 kits naming it — the selector `0x5fd8b0` cannot produce 14 (14 is not +/// among its literal outputs), so the recompute lands on `Stand`. /// /// The driver holds the comparison because the armed id lives there. An aura **refresh** in place /// emits nothing: the watcher `0x604d00` fires neither arm when a slot is rewritten with the same @@ -709,7 +703,7 @@ pub(crate) struct BaseAnimRecompute { } /// Rear up a rider's mount (decision 0441 P2): resolved to a [`EmoteAnim`] one-shot of -/// MountSpecial(94) on the unit's MOUNT CHILD entity by [`flourish_to_anim`] — the §5-verified +/// MountSpecial(94) on the unit's MOUNT CHILD entity by [`flourish_to_anim`] — `0x608d50`'s /// routing (the flourish plays on the mount model; the rider holds Mount(91) throughout). /// Written by the net drain (`SMSG_MOUNTSPECIAL_ANIM` — observed riders only; our own echo /// is dropped there) and by the self space-bar gate (`crate::player`, locally at send time). @@ -776,15 +770,15 @@ pub(crate) struct SpellGoTargets { pub(crate) hits: Vec, /// Units it missed, with the wire's `SpellMissInfo` code — a projectile still flies at /// each, and its **arrival** plays the victim's defense clip for DODGE(3)/BLOCK(5) (the - /// client's `Missile_C::Update 0x61ceb0` dispatch — Dodge 30 / ShieldBlock 24, never Parry; - /// wow-re `smsg-attackerstate-consequences.md` §Q4). No impact kit plays. The deflect + /// client's `Missile_C::Update 0x61ceb0` dispatch — Dodge 30 / ShieldBlock 24, never Parry). + /// No impact kit plays. The deflect /// *flight* visual (the projectile glancing off) stays a named approximation — ours ends /// the flight at the target. pub(crate) misses: Vec<(Entity, u8)>, /// The ground point a dest-targeted cast launched at (`TARGET_FLAG_DEST_LOCATION`), in /// **bevy** coords (converted at the apply seam like every scene position). A ground AOE's /// GO arrives with empty hit/miss lists and only this. No consumer reads it yet — the - /// dest-anchored launch visual is pending the dispatched wow-re read (the persistent area + /// dest-anchored launch visual is still an open question (the persistent area /// effect anchors to the DynamicObject create, not to this packet). pub(crate) dest: Option, /// The GO's ammo block (`castFlags & 0x20`, a ranged shot): the `ItemDisplayInfo` id of the @@ -913,8 +907,8 @@ pub(crate) struct AnimDriver { /// (`0x5fe48e`, the fields' only reader), and `0x5fe2f0` is the **single** animation entry /// point in the image — 40 call sites, locomotion among them (`0x602c60` → `0x5fd9e0` → /// `0x5fd8b0` → `0x5fd100`), which is why this is not a one-shot flag: a unit that simply - /// starts running consumes the arm, and that is what starts Charge's trail (wow-re - /// `charproc8-trail-draw-state.md` §11.5/§11.6). benilla's driver is one batched system, not + /// starts running consumes the arm, and that is what starts Charge's trail. + /// benilla's driver is one batched system, not /// a per-play function, so the arm cannot be read at the call the way the reference reads it. started_anim: bool, /// A **masked upper-body one-shot** in flight (decision 0087): a swing/emote the live-state route @@ -953,15 +947,15 @@ pub(crate) struct AnimDriver { /// the landing of a bracket-less step-off fall (which must still run the `0x602c60` land pick /// even though no Special drove the arc). was_falling: bool, - /// The **deferred combat one-shot** — the client's `CGUnit+0xd60` cache (wow-re - /// `combat-anim-fastpath.md`, decision 0406): a combat clip requested while another combat + /// The **deferred combat one-shot** — the client's `CGUnit+0xd60` cache (decision 0406): + /// a combat clip requested while another combat /// clip plays is NOT armed — the playing clip's rate doubles (op6 2.0f) and the request /// parks here, played by the driver the moment no one-shot is live (the client's /// base-recompute read). Any normal arm clears it (the client's `0x5fe48e` writes −1 on /// every non-fast-path PlayAnimation; the driver clears at its play sites). deferred: Option, - /// The armed looping arm's **replay window** (decision 0516 — wow-re `loop-replay-fidget` - /// §7/§7d): the variation node the loop armed + its rolled budget `R` (the client's + /// The armed looping arm's **replay window** (decision 0516): the variation node the loop + /// armed + its rolled budget `R` (the client's /// `block+0xbc`; the window is `R` clip-lengths wide, op4 `0x7126d8` — live for LOOPS too, /// correcting 0117's "loops ignore it"). The per-frame watchdog (`0x719370`'s transcription /// in `drive_animations`) re-arms the id when the node — still the MAIN armed animation — @@ -1044,9 +1038,9 @@ struct Overlay { looping: bool, } -/// A key-bone cross-fade in flight ([`AnimDriver::overlay_fade`], decision 0878 — wow-re -/// `oneshot-lifecycle.md` §5.4). `out` is the **retiring** node, holding the outgoing pose the -/// client snapshots into the bone's secondary slot (`rep movsd +0x98 → +0xc4`); it is `None` when +/// A key-bone cross-fade in flight ([`AnimDriver::overlay_fade`], decision 0878). `out` is the +/// **retiring** node, holding the outgoing pose the client snapshots into the bone's secondary +/// slot (`0x7123af`: `rep movsd +0x98 → +0xc4`); it is `None` when /// a fresh clip fades in over the *inherited base* pose, which needs no node of its own — the base /// is already there. `left`/`total` are the window: the fixed 150 ms of a fade-to-rest, or the /// incoming clip's own blendTime on a blended re-arm. @@ -1105,7 +1099,7 @@ impl AnimDriver { /// The `AnimationData.dbc` id of the clip this unit is currently playing (target gait, a Special's /// enter/loop/exit one-shot, or Death — which the death override records as the gait). `None` before /// the first selection. The mouse pick reads it for the **current animation's** bounds sphere — the - /// real client's broad-phase volume tracks the playing sequence (wow-re pick-volume RE). + /// real client's broad-phase volume tracks the playing sequence (`0x7089c0`). pub(crate) fn active_anim(&self) -> Option { match self.mode { Mode::Gait => self.gait, @@ -1231,7 +1225,7 @@ impl Plugin for CreatureAnimPlugin { arm_mount_poof_fx, // The aura-slot watcher: state kits persist for the aura's life (the bread). arm_aura_state_fx, - // The pending-morph latch (wow-re `shapeshift-morph-cloud.md`): the aura + // The pending-morph latch (`0x5ff0c0` arms, `0x60abe0` replays): the aura // add/remove edges arm it, and a display swap's rebuild replays the latched // spell's impact kit — the shapeshift cloud, both directions. Arm before // replay: the swap message crosses from last frame's teardown, and its @@ -1316,7 +1310,7 @@ impl Plugin for CreatureAnimPlugin { } } -/// Resolve `id` to the id this model actually plays (decision 0082, wow-re `anim-id-resolution.md`): +/// Resolve `id` to the id this model actually plays (decision 0082, `0x711bf0`): /// [`ModelAnimations::resolve`] via `catalog` when `AnimationData.dbc` has loaded, else identity — a /// brief window at startup ([`load_anim_data`](sheath::load_anim_data) runs at `Startup`, so this /// degrade is only ever live for the first few frames, same shape as `anim_data.map_or(0, ..)` @@ -1399,7 +1393,7 @@ mod attack_stand_tests { mod nock_latch_tests { use super::*; - /// The `$BWP`/`$BWR` nock cycle (wow-re `nocked-ammo-cancel.md` §G1/G4): the pull keyframe + /// The `$BWP`/`$BWR` nock cycle (set `0x624b70`, cleared `0x60016c`): the pull keyframe /// latches (only a unit with a cached ammo display), the release keyframe clears — the arrow /// appears as she draws it and leaves the hand with the shot. #[test] @@ -1454,7 +1448,7 @@ mod nock_latch_tests { mod disarm_tests { use super::*; - /// **The ladder, hand by hand** (decision 1863; wow-re `disarm-weapon-gate-law.md` §2c). The + /// **The ladder, hand by hand** (decision 1863; `0x5ec240`/`0x605e30`). The /// reference hides **exactly one weapon**, main hand first — so a disarmed dual-wielder /// punches with the main hand and still swings its off-hand weapon. Every selector downstream /// reaches its own unarmed leg by its own table; there is no disarm case in any of them. diff --git a/crates/benilla-app/src/creature_anim/blood.rs b/crates/benilla-app/src/creature_anim/blood.rs index 3c647eb8..71281009 100644 --- a/crates/benilla-app/src/creature_anim/blood.rs +++ b/crates/benilla-app/src/creature_anim/blood.rs @@ -1,5 +1,5 @@ //! The melee **blood spurt** (decision 0137 phase 3) — the fourth element of the victim-feedback -//! set (wow-re `melee-blood-spurt.md`, byte-verified `0x624530 → 0x625010`): a landed melee blow +//! set (`0x624530 → 0x625010`): a landed melee blow //! hangs a small particle-emitter model (`Particles\BloodSpurts\*.mdx`) on the victim, front or //! back by where the attacker stands, sized by the crushing bit, colored by the creature's blood //! type. Independent of the wound flinch and the floating text; rides the 0122 kit-effect spawn @@ -30,7 +30,7 @@ use super::{SpellKitFx, SwingImpact, SwingMessage}; /// `cvars::REGISTERED` takes one row per knob a settings page actually wires (0137 phase 3). const VIOLENCE_LEVEL: usize = 2; -/// The victim-model M2 attachment ids the spurt hangs on (`melee-blood-spurt.md`: CEffect at +/// The victim-model M2 attachment ids the spurt hangs on (`0x625010`: CEffect at /// attach tag 0xf front / 0x10 back — present on every character and creature model checked). const ATTACH_FRONT: u16 = 15; const ATTACH_BACK: u16 = 16; diff --git a/crates/benilla-app/src/creature_anim/breath.rs b/crates/benilla-app/src/creature_anim/breath.rs index 41884c36..bc6ab111 100644 --- a/crates/benilla-app/src/creature_anim/breath.rs +++ b/crates/benilla-app/src/creature_anim/breath.rs @@ -1,6 +1,5 @@ //! **Cold breath** — the visible vapour a unit puffs from its mouth in a cold zone (B233, -//! decision 1149; wow-re `object-layer/scratch/cold-breath-law.md`, §5 two independent pairs + -//! byte arbitration). +//! decision 1149). //! //! The client hangs three mutually-exclusive puffs off one animation event, and they are the //! `$BTH` family: cold vapour, underwater bubbles, and the drunk's bubbles. This module owns the @@ -41,7 +40,7 @@ //! ## Stated divergences //! //! - **The underwater rung is not modelled** — but it is now fully specified rather than open: -//! what is missing is the work, not the law (`cold-breath-law.md` §4b). The test is +//! what is missing is the work, not the law (`0x607710`'s liquid arm). The test is //! //! ```text //! underwater ⟺ 5.0 + SCALE_X · boxHeight < liquidSurfaceZ − unitZ diff --git a/crates/benilla-app/src/creature_anim/driver.rs b/crates/benilla-app/src/creature_anim/driver.rs index c8d894a4..93432bc2 100644 --- a/crates/benilla-app/src/creature_anim/driver.rs +++ b/crates/benilla-app/src/creature_anim/driver.rs @@ -59,7 +59,7 @@ use wound::{wound_evict, wound_trigger, wound_upkeep, WoundEdge}; /// value as the per-arm sheath ceremony's overlay weight. const ONESHOT_OVERLAY_WEIGHT: f32 = 8.0; -/// The key-bone **fade-to-rest** window (decision 0878 — wow-re `oneshot-lifecycle.md` §5.4): a +/// The key-bone **fade-to-rest** window (decision 0878): a /// finished upper-body one-shot is never stopped. The client's per-frame advance latches the /// completion (`0x719370`), the deferred event reaches `CGUnit::OnAnimationFinished 0x5fc920` the /// same frame, and that calls op4 with `param_3 = -1` (`0x5fcacb`) — which snapshots the clip's @@ -83,7 +83,7 @@ enum OneShotReq { } /// The currently-armed one-shot the combat fast-path tests — the client's key-bone-else-bone-0 -/// current-id read (`0x5fe422`, wow-re `combat-anim-fastpath.md` §1): the masked overlay while +/// current-id read (`0x5fe422`): the masked overlay while /// its node still plays, else the full-body [`Mode::Swing`] clip while unfinished. Gait and /// Special clips are never returned; their ids aren't combat, so nothing is lost at the /// classifier gate. @@ -194,8 +194,7 @@ fn overlay_fade_upkeep(drv: &mut AnimDriver, player: &mut AnimationPlayer, dt: f } } -/// The **TRANSPLANT** (`0x5fe919` — wow-re `oneshot-lifecycle.md` §3a, the half -/// `anim-composition-model.md` §2 was missing; decision 0878). A base **locomotion** clip +/// The **TRANSPLANT** (`0x5fe919`, decision 0878). A base **locomotion** clip /// requested while bone 0 still plays a live **CAST** or **COMBAT** one-shot does not replace it: /// the client copies the bone-0 descriptor — its id, its rate, and `+0x08` the clip's **live /// elapsed position** — onto the key-bone with `blendFlag = 0`, then hands bone 0 the request. The @@ -319,8 +318,8 @@ pub(super) fn drive_animations( // …and the host's granted modes, for the same reason `unify` reads them on the rider. Option<&crate::net::UnitMoveModes>, // …and the rider's own `OBJECT_FIELD_SCALE_X`: a mount child's transform carries only its - // `CreatureDisplayInfo` column, and the mount renders at the PRODUCT of the two (the - // byte-verified mount composition, wow-re `0x613ef0`). The rate divisor wants that product + // `CreatureDisplayInfo` column, and the mount renders at the PRODUCT of the two + // (`0x613ef0`). The rate divisor wants that product // — the mount model's world scale — not the child's local column (decision 0903). &Transform, // …and whether the rider is US, which only the debug trace reads (decision 0906). @@ -564,7 +563,7 @@ pub(super) fn drive_animations( if movement.is_none() { mv.stand_state = store.map_or(0, |s| s.0.unit_stand_state()); } - // Mounted forces stand-state 0 on every recompute (`0x5fdf80`, wow-re sheath-policy §3): + // Mounted forces stand-state 0 on every recompute (`0x5fdf80`): // no sit/sleep/kneel pose can hold in the saddle. if mounted { mv.stand_state = 0; @@ -578,7 +577,7 @@ pub(super) fn drive_animations( // mounts are mutually exclusive anyway. mv.stealthed = store.is_some_and(|s| s.0.unit_is_stealthed()); let moving = mv.flags & move_flags::ANY_MOVE != 0; - // The airborne arc's bookkeeping (wow-re land-anim-height-gate + rf57b §2): on the arc's + // The airborne arc's bookkeeping (jump `0x60e480`, land `0x602c60`): on the arc's // FIRST airborne frame, its launch vertical speed splits a **jump** (upward — the client's // JumpStart/Jump bracket rides the MSG_MOVE_JUMP event) from a **step-off fall** (level or // downward — no bracket; the gait freezes until FALLINGFAR latches Fall 40, and an @@ -629,7 +628,7 @@ pub(super) fn drive_animations( if falling && (!was_falling || launched) { drv.jump_arc = mv.vertical_speed > JUMP_ARC_MIN_UP; } - // The airborne-freeze's exact gate (`0x5fd8e8`, §5-arbitrated — decision 0868): + // The airborne-freeze's exact gate (`0x5fd8e8`, decision 0868): // keep-current iff `FALLING && (FALLINGFAR || vz ≠ 0)`. The vz clause pins every real // arc (ballistics make vz ≠ 0 from the first integrated substep); the one uncovered // case is a fresh walk-off's vz == 0 substep, where the client genuinely may re-pick. @@ -638,7 +637,7 @@ pub(super) fn drive_animations( let airborne_frozen = falling && (mv.flags & move_flags::FALLING_FAR != 0 || mv.vertical_speed != 0.0); // The idle re-face turn-shuffle (decision 0123 — the client's facing-delta latch - // `0x607ed0`, wow-re `loop-replay-fidget.md` §5b): a stationary creature easing its yaw + // `0x607ed0`): a stationary creature easing its yaw // toward its target reads as *turning* to the anim layer, so the gait picks // ShuffleLeft/Right and each Shuffle→Stand return re-arms (and, relaxed, re-rolls) Stand // — the fidget's recurring trigger. Gated like the client's can-fidget gate (`0x5fce30`: @@ -709,8 +708,7 @@ pub(super) fn drive_animations( continue; } - // **The mount transition's own bone-0 arm** (wow-re `mount-cast-anim-disposition.md` §1 + - // `mount-composition.md` Q4, §5 2026-08-03). Both legs of the `UNIT_FIELD_MOUNTDISPLAYID` + // **The mount transition's own bone-0 arm**. Both legs of the `UNIT_FIELD_MOUNTDISPLAYID` // watcher arm the BODY's bone 0 through op4 `0x7121a0` on the PRIMARY slot: the build // `0x607b44` plays **91 `Mount`** with a cross-fade, the teardown `0x607ce0` plays seq // **0 `Stand`** with the cross-fade **off** (decision 0931 — the two legs are the same @@ -780,9 +778,8 @@ pub(super) fn drive_animations( drv.deferred = None; // a normal arm clears the fast-path cache (`0x5fe48e`) } - // The loot kneel's trigger (see [`select::LOOT`]; byte-verified, wow-re - // `loot-anim-leg.md` §5/§8 — decisions 0515 / 1471 / 1477): the leg needs **two** - // predicates, and both SPLIT on IsActivePlayer. + // The loot kneel's trigger (see [`select::LOOT`]; decisions 0515 / 1471 / 1477): the + // leg needs **two** predicates, and both SPLIT on IsActivePlayer. // // **Self.** Predicate A (`0x6126b0`) is "a loot session is open" — the client-local // loot-target latch. Predicate B (`0x612710`) is the per-object-CLASS filter: which @@ -818,9 +815,9 @@ pub(super) fn drive_animations( } else { current_special(&mv, drv.jump_arc) }; - // The loot leg outranks the standState pose resolver (the rf57 chain calls loot before - // standState): a looting unit kneels at the loot, never into a sit/sleep bracket. The - // airborne Specials keep their precedence (the chain's airborne freeze runs first). + // The loot leg outranks the standState pose resolver (the `0x5fd8b0` chain calls loot + // before standState): a looting unit kneels at the loot, never into a sit/sleep bracket. + // The airborne Specials keep their precedence (the chain's airborne freeze runs first). let special = special.filter(|s| !(looting && matches!(s, select::Special::Pose(_)))); // ── Sheath state (decision 0080). The unit's rendered sheath is the **client-side @@ -829,9 +826,9 @@ pub(super) fn drive_animations( // *changes* (the `0x604c70` field-apply — a remote unit's own client volunteered the // change; our own echo arrives already-committed and adopts as a no-op), and written by // the one-setter requests here + the per-animation reconcile below. Every transition - // **snaps by default** — byte-verified across all 24 SetSheatheState call sites (wow-re - // `sheath-policy.md`): only the manual toggle's request carries the ceremony, and the - // draw/stow sound rides the ceremony playback (no clip → no sound). + // **snaps by default** — across all 24 call sites of SetSheatheState `0x611cf0`: only the + // manual toggle's request carries the ceremony, and the draw/stow sound rides the ceremony + // playback (no clip → no sound). let sheath_frame_start = drv.sheath_cur; let sheath_byte = store.and_then(|s| s.0.unit_sheath_state()).unwrap_or(0); if drv.sheath_cur.is_none() || drv.sheath_byte != Some(sheath_byte) { @@ -849,7 +846,7 @@ pub(super) fn drive_animations( // stow point). A ceremony without playable clips degrades to the snap. if let Some(req) = pending_sheath.get(&entity) { let cur = drv.sheath_cur.unwrap_or(0); - // Mounted is a persistent DRAW-BLOCK (wow-re `sheath-policy.md` §3: the client's + // Mounted is a persistent DRAW-BLOCK (the client's // recompute `0x5fdf80` tests the mount model FIRST and forces stowed on every // PlayAnimation — nothing can stay drawn on a mount). The client's composite // recomputes constantly (the mount's own gait plays); our rider track goes silent @@ -917,8 +914,8 @@ pub(super) fn drive_animations( // (op4 `blendFlag≠0` copies the outgoing pose over `+0xc4..`). So this frame's plays are // tracked — full-body plays (bone 0: the base track) evict a full-body wound; masked-slot // plays (the key-bone) evict a masked wound. A play on the *other* bone leaves the wound - // decaying (the §5's inherited-swing case). Mode/gait changes proxy the mode machine's own - // base plays; the flags catch the same-id re-plays the proxy can't see. + // decaying (the kernel `0x714260`'s inherited-swing case). Mode/gait changes proxy the + // mode machine's own base plays; the flags catch the same-id re-plays the proxy can't see. // ── The **base-animation lock**'s clearer, keyed on the FINISHED id and run before // anything re-picks the base this frame — the reference clears at `0x5fc9c6`, above // `OnAnimationFinished`'s own reason branch, so completion and pre-emption both release it @@ -947,7 +944,7 @@ pub(super) fn drive_animations( return None; } // `DO_NOT_PLAY_WOUND_ANIM` takes the **parry** with the flinch (`0x60ec1f`, the bit's - // second and last consumer — wow-re `wound-parry-gate-and-injury-vocal.md` Q1/Q6). + // second and last consumer). // The gate is inside `0x60ec00`, which the `$CPP` ladder enters only on // victimState 3: DODGE/DEFLECT (30) and BLOCK (24) go straight to PlayAnimation from // `0x624a90`/`0x624a74` and are NOT gated — a flagged creature still dodges and still @@ -1012,7 +1009,7 @@ pub(super) fn drive_animations( // hit re-parks its own request — both what the client's `0x5fe48e`/`0x5fe480` do). // NEVER mid-air: the read (`0x5fd392`, inside the `0x5fd360` recompute arm) sits // downstream of the airborne-freeze, so a park made mid-arc waits — and dies at the - // landing play's clear (§5-verified, decision 0868). + // landing play's clear (decision 0868). if requests.is_empty() && drv.deferred.is_some() && !airborne_frozen @@ -1028,7 +1025,7 @@ pub(super) fn drive_animations( if drv.base_lock.refuses() { continue; } - // The COMBAT FAST-PATH (`0x5fe43c`–`0x5fe48b`, wow-re `combat-anim-fastpath.md`, + // The COMBAT FAST-PATH (`0x5fe43c`–`0x5fe48b`, // decision 0406): a combat clip requested while another combat clip is playing is // NOT armed — the CURRENT clip's rate doubles (op6 2.0f re-times its remainder, // pose-continuous) and the request parks in the `+0xd60` cache to play afterwards. @@ -1075,7 +1072,7 @@ pub(super) fn drive_animations( } continue; } - // Mounted forces the masked route (byte-verified, wow-re mount-composition B1: + // Mounted forces the masked route (`0x5fe2f0`'s mounted branch: // upper-body one-shots route to the key-bone while mounted, and the 91-force would // reclaim bone 0 on the next play anyway): a full-body /wave would replace the seat // pose with a standing wave floating over the saddle. @@ -1097,7 +1094,7 @@ pub(super) fn drive_animations( // swaps the torso in one frame. Note the full-body branch below deliberately does // NOT touch the overlay: on that route the client's key-bone slot keeps its current // descriptor (a dedup no-op), so a standing emote over a still-running masked swing - // plays legs-only underneath it (`0x5fe930`; wow-re claim 6 CONFIRMED). + // plays legs-only underneath it (`0x5fe930`). retire_overlay( &mut drv, &mut player, @@ -1119,7 +1116,7 @@ pub(super) fn drive_animations( // Full-body route (standing idle / airborne non-combat), or the split-boneless // masked fallback (the client's −1 key-bone sentinel arms bone 0 too): the clip // replaces the base on bone 0 — **even over a Special**. The client never drops a - // play: one slot, last-writer-wins (decisions 0083/0087; the wow-re §3 route puts + // play: one slot, last-writer-wins (decisions 0083/0087; the route `0x5fe6c8` puts // a jump-in-place cast/emote on bone 0, replacing the hang — decision 0864 is the // ref's mid-air cast). Cutting an airborne clip freezes the outgoing node first — // the pose-snapshot decay of the client's op4 blend, scoped exactly like @@ -1186,7 +1183,7 @@ pub(super) fn drive_animations( if special_edge { drv.deferred = None; } - // The looping-variation ADVANCE (decision 0516 — wow-re `loop-replay-fidget.md` §7/§7d, + // The looping-variation ADVANCE (decision 0516 — // the per-frame watchdog `0x719370`): every looping arm installed a window `R` // clip-lengths wide; when the armed node — still the MAIN animation (the client checks // the armed block: any newer arm superseded the window) — completes its `R` passes, the @@ -1341,7 +1338,7 @@ pub(super) fn drive_animations( if done { // Freeze the held frame, then retire it: the client's snapshot copies an already // *expired* window, so the secondary clamps to `seq.end` and the fade runs from - // the clip's final pose (`oneshot-lifecycle.md` §5.4). + // the clip's final pose (`0x7123af`). if let Some(a) = player.animation_mut(ov.node) { a.set_speed(0.0); } @@ -1480,18 +1477,18 @@ pub(super) fn drive_animations( // 1. `0x60ea9f` → `0x6125f0`: the victim's cached creature template carries // `type_flags` bit `0x8`, **DO_NOT_PLAY_WOUND_ANIM** — a skeleton, a ghost, a // bone golem has no flesh to recoil, and the reference refuses every flinch it - // would ever take, melee and spell alike (decision 2068; wow-re - // `melee-blood-spurt-suppression.md` §6). It gates the ANIMATION only: the blood - // spurt is a separate system reading [`SwingImpact`] itself, and wow-re §8 Q1 is - // explicit that nothing keyed on creature type suppresses it — a skeleton bleeds. + // would ever take, melee and spell alike (decision 2068). It gates the ANIMATION + // only: the blood spurt is a separate system reading [`SwingImpact`] itself, and + // nothing keyed on creature type suppresses it (`0x624530` → `0x625010`) — a + // skeleton bleeds. // A record we have not received yet reads as NOT flagged, which is `0x6125f0`'s // own null-record leg (`return record ? … : false`), so a creature whose query is // still in flight flinches there too. - // 2. `0x60eaac`–`0x60eac8` (wow-re `charproc-rate-override-wound-gate.md`, decision - // 2063): a CharProc-11 rate-override node on the unit's effect list — the freeze - // auras' node, Freezing Trap / Ice Block / petrify / web wrap — refuses EVERY - // flinch for as long as it lives. The gate is the node's presence, not its rate - // (kit 3071's 1.0 gates too), so it reads the node list, not the pause it applies. + // 2. `0x60eaac`–`0x60eac8` (decision 2063): a CharProc-11 rate-override node on the + // unit's effect list — the freeze auras' node, Freezing Trap / Ice Block / + // petrify / web wrap — refuses EVERY flinch for as long as it lives. The gate is + // the node's presence, not its rate (kit 3071's 1.0 gates too), so it reads the + // node list, not the pause it applies. // // `no_wound_anim` is read once above, where the parry pick — the flag's other // consumer — needs the same answer. @@ -1535,15 +1532,15 @@ pub(super) fn drive_animations( // (gossip, loot, mounts) inherit the policy with zero new sheath wiring. // The reconcile tests the **requested** id, not the model-resolved substitute (decision // 0125, director-eyes falsification of 0082's resolved-id reading): the client's arm - // descriptor carries the id that was ASKED FOR (the cast-override §2.1 writes the kit's - // own anim id even on a model lacking the sequence), and `0x5fdb50` reads that + // descriptor carries the id that was ASKED FOR (the cast-override `0x5fde80` writes the + // kit's own anim id even on a model lacking the sequence), and `0x5fdb50` reads that // descriptor. The two differ only when a model lacks the requested clip — the ground // truth is GnollCaster.m2: no ReadySpellDirected(51) at all, its hold falls back to a // flags-less Stand for *playback*, yet the ref still stows the staff for the whole // windup — only 51's own `&4` row can do that. (0082's chicken rationale collapses under // the same lens: requested Attack2H's 0x20 vs resolved AttackUnarmed's 0x10 is - // unobservable on a weaponless chicken.) Byte arbitration of `0x5fdb50`'s miss path is - // dispatched to wow-re; the director's eyes outrank the old reading meanwhile. + // unobservable on a weaponless chicken.) The byte reading of `0x5fdb50`'s miss path is + // open; the director's eyes outrank the old reading meanwhile. // The reconcile must see **every play** (decision 0087 (d)): the client's `0x5fdf80` runs // inside `PlayAnimation` itself, so a *masked* one-shot — which never touched `mode`, so // `active_anim()` still reports the base gait — is reconciled all the same. A masked emote @@ -1577,9 +1574,8 @@ pub(super) fn drive_animations( if let Some(forced) = select::reconcile_sheath(cur, anim, flags, engaged, is_self, sheath_byte, mounted) { - // The ranged wind-up **bracket** (wow-re `ranged-sheath-exempt-autorepeat.md`, - // Q1's ordering law): the client surrounds every ranged kit play with ranged - // snaps — `0x60f34c` before the play, the outer send/START `SetSheatheState + // The ranged wind-up **bracket**: the client surrounds every ranged kit play with + // ranged snaps — `0x60f34c` before the play, the outer send/START `SetSheatheState // (2,1,1)` after — so ReadyThrown 108's genuine force-stow (NOT in the `0x5fe180` // exempt set) exists but never renders: the snap always wins the frame. Our // frame-spread ECS can't reproduce the call-stack order, so the bracket is @@ -1607,7 +1603,7 @@ pub(super) fn drive_animations( } } // Leaving the ranged sheath stance un-nocks (the client's `0x60fc72` un-nock, gated - // `[+0xd40] != 2`; wow-re `nocked-ammo-cancel.md`): however the state left 2 this frame + // `[+0xd40] != 2`): however the state left 2 this frame // — a stow/melee request, the reconcile, the server byte — the ammo model drops. // Re-drawing shows no ammo until the next `SMSG_SPELL_START` re-affirms it. if sheath_frame_start == Some(2) && drv.sheath_cur != Some(2) { @@ -1617,8 +1613,7 @@ pub(super) fn drive_animations( } // …and **TRANSLATING** un-nocks too — the arrow leaves the hand and the string relaxes, // while the cached ammo display survives for the next pull and the weapon stays DRAWN. - // Byte-verified (wow-re `shooter-stop-law.md` §J1.1, which refuted our recorded "nothing - // in the client un-nocks on movement"): `0x60e480` is a movement-OPCODE dispatcher whose + // `0x60e480` is a movement-OPCODE dispatcher whose // jump-table arm 0 runs `0x60e4ac call 0x60f530` (un-nock) + `RecomputeBaseAnim(-1)`, and // arm 0 is reached by exactly MSG_MOVE_START_{FORWARD, BACKWARD, STRAFE_LEFT, // STRAFE_RIGHT, SWIM} (181/182/184/185/202). **Turning and stopping do NOT un-nock** — @@ -1628,8 +1623,8 @@ pub(super) fn drive_animations( // // The reference's trigger is the START opcode (an edge); a level test is equivalent here // because the only thing that can re-latch is `$BWP`, authored solely in the Load clips — - // which a translating unit never plays. Sheath is untouched (§J1's claim-4 split: REFUTED - // for un-nock, CONFIRMED for stow — `[+0xd40]` has no movement-path writer). + // which a translating unit never plays. Sheath is untouched (`[+0xd40]` has no + // movement-path writer: only the ctor's `0x5fafa0` and `0x611cf0` write it). if mv.flags & (move_flags::ANY_MOVE | move_flags::SWIMMING) != 0 && nock_latched { commands.entity(entity).remove::(); } diff --git a/crates/benilla-app/src/creature_anim/driver/grip.rs b/crates/benilla-app/src/creature_anim/driver/grip.rs index b9b53152..ee3689cf 100644 --- a/crates/benilla-app/src/creature_anim/driver/grip.rs +++ b/crates/benilla-app/src/creature_anim/driver/grip.rs @@ -7,7 +7,7 @@ use bevy::prelude::*; use super::super::{HandGrip, HAND_GRIP_WEIGHT}; /// Hold each hand's `HandsClosed` finger pose while that hand grips a weapon, release it when it doesn't -/// — the persistent weapon-grip overlay (wow-re `hand-grip-mechanism.md`: the client arms the finger +/// — the persistent weapon-grip overlay (`CloseHand 0x479660`: the client arms the finger /// key-bones with `HandsClosed` purely by a weapon occupying that hand's attach point). Played *over* the /// gait on the finger-mask nodes, so the arm keeps its animation while only the fingers curl; on release /// the fingers fall back to the base gait's flat Stand rest. Idempotent — plays only on the open→closed diff --git a/crates/benilla-app/src/creature_anim/driver/mode.rs b/crates/benilla-app/src/creature_anim/driver/mode.rs index 2cabfee6..e35210ed 100644 --- a/crates/benilla-app/src/creature_anim/driver/mode.rs +++ b/crates/benilla-app/src/creature_anim/driver/mode.rs @@ -91,8 +91,8 @@ pub(super) fn run( Mode::Entering(sp) => { // The swim re-latch does NOT cut the hop's kick: JumpStart PLAYS OUT over the // re-latch and the swim gait resumes only at its end (decision 0517 — - // director-corrected against the ref; the §5's static cut-at-relatch law could - // not reproduce the screen and is flagged wow-re-side for a live capture). + // director-corrected against the ref; the static byte reading, a cut where the freeze + // gate `0x5fd8e8` releases, could not reproduce the screen and is unconfirmed live). // Only the swim re-latch holds — a ground landing, a water exit, or a new // Special still cuts (0503's snapshot-freeze). let swim_relatch_hold = sp == select::Special::Jump @@ -266,10 +266,10 @@ pub(super) fn run( } else if matches!(under, Some(select::Special::Jump | select::Special::Fall)) { // The airborne-freeze (`0x5fd8e8` keep-current): mid-arc nothing re-picks // bone 0 — a finished clip clamps and holds its last frame for the rest of - // the arc (the §6 clamp path), and a mid-air flag change is a keep-current + // the arc (the clamp at `0x7145db`), and a mid-air flag change is a keep-current // no-op. The only exits are the edges above: the FALLINGFAR latch's Fall // and the land pick at touchdown. This holds over Fall too: 0864's per-tick - // Fall(40) re-assert was §5-REFUTED (decision 0868 — Fall plays ONCE, at the + // Fall(40) re-assert was refuted (decision 0868 — Fall plays ONCE, at the // latch edge `0x61a820@0x61a9eb`; `0x5ff030` is a wire-apply path, not a // tick), so a clip that takes bone 0 after the latch holds until landing. } else if let Some(sp) = under { @@ -293,10 +293,9 @@ pub(super) fn run( } } else if oneshot_finished(player, anims, id, catalog) || mv.flags != drv.gait_flags { // A finished one-shot recomputes the base — the ranged FIRE clips included - // (decision 1544, superseding 0994 §1). 0994 held them out on - // `shooter-stop-law.md` §J4's claim that the completion dispatcher `0x5fc3f0` - // is never reached for a bow id; wow-re's §5 refuted that as an absence proof - // whose census could not see its second fire site — the natural-completion path + // (decision 1544, superseding 0994 §1). 0994 held them out on the claim that + // the completion dispatcher `0x5fc3f0` is never reached for a bow id; it is, + // through a second fire site — the natural-completion path // enqueues the callback as a plain ARGUMENT (`0x7194f5` pushes mode 0) and // `0x7074b0` invokes it later as `call [esi+4]`, so a scan for // `call dword ptr [reg+0x70]` misses it by construction. 46/49/107 land on the @@ -367,7 +366,7 @@ pub(super) fn run( ); drv.gait = None; } else if airborne_frozen && drv.gait.is_some_and(|g| g != DEATH) { - // The airborne-freeze on the STEP-OFF arc — the exact §5-verified gate + // The airborne-freeze on the STEP-OFF arc — the exact gate // (`0x5fd8e8`: `FALLING && (FALLINGFAR || vz ≠ 0)`, decisions 0864/0868; // the selector chain's leg right after death): mid-air the selector never // re-picks, so the takeoff-frozen gait keeps rolling mid-cycle AND the live @@ -461,7 +460,7 @@ pub(super) fn run( // (self: the latch; remote: the flag — the `looting` predicate above) on a // stationary, unmounted unit, the gait slot holds Loot 50 — over the cast // pin, the Ready/ranged idles, the chair loops and the state-emote idle - // alike (the `0x5fd8b0` chain order, §5-verified: locomotion → LOOT → + // alike (the `0x5fd8b0` chain order: locomotion → LOOT → // standState → combat/channel). The trigger dropping cross-fades back to // whatever the slot picks next. let loot_cands; @@ -492,7 +491,7 @@ pub(super) fn run( }; let target = cands[0]; // **The turn-shuffle is released by its own clip window, not by the turn ending** - // (decision 1655, wow-re `object-layer/scratch/turn-shuffle-lifecycle.md`). + // (decision 1655). // // The client's only per-frame poll of a standing unit's base animation is // `0x607ed0`'s tail, and it *refuses* this exact transition: with the shuffle @@ -519,7 +518,7 @@ pub(super) fn run( } _ => target, }; - // Each RF-0057 candidate, in priority order, resolved through the model's own + // Each `0x5fd8b0` candidate, in priority order, resolved through the model's own // baked fallback (decision 0082) before moving to the next candidate — a model // missing the exact id still plays its baked substitute rather than stepping // down the selector's own list early. The state-emote idle's id (above) resolves @@ -537,8 +536,8 @@ pub(super) fn run( // sweeping every node of the id. A completed ranged Load simply clamps at // full draw and stays there; nothing promotes it (0994). } else if let Some(c) = clip { - // **The two bypasses the reference keeps** (wow-re - // `base-anim-lock-knockdown.md` §6/§7.6): the death poses and the mount attach + // **The two bypasses the reference keeps** (`0x5fc563`, + // `0x607b44`): the death poses and the mount attach // write the arm primitive directly and are not gated by the base-anim lock. In // benilla both arrive as ordinary gait targets, so they say so here — a body // knocked flat and then killed must still fall dead, and mounting during a @@ -557,8 +556,8 @@ pub(super) fn run( } else { // A looping base arm rolls its variation when relaxed (decision 0123 — // the client's base-arm `variationIdx = −1`; a combat/cast arm keeps the - // deterministic head) AND its replay budget (decision 0516 §7d — the - // watchdog window). A re-armed Stand landing on its rare look-around + // deterministic head) AND its replay budget (decision 0516, `0x712784` + // — the watchdog window). A re-armed Stand landing on its rare look-around // variations IS the idle fidget. let (c, budget) = roll_loop(anims, c, relaxed, rng); if traced && benilla_assets::trace::enabled() { @@ -618,7 +617,7 @@ pub(super) fn run( } /// Has the base slot's armed looping clip finished its **replay window** — the client's -/// `windowHi = windowLo + span·R` (decision 0516 §7d, wow-re `loop-replay-fidget.md` §7a)? +/// `windowHi = windowLo + span·R` (`0x712784`, decision 0516)? /// /// `true` when there is no window to wait on at all: a slot with nothing armed, or one whose /// window a newer arm superseded, is not something to hold a shuffle against. The driver's own diff --git a/crates/benilla-app/src/creature_anim/driver/play.rs b/crates/benilla-app/src/creature_anim/driver/play.rs index 72237cd7..90cd75f0 100644 --- a/crates/benilla-app/src/creature_anim/driver/play.rs +++ b/crates/benilla-app/src/creature_anim/driver/play.rs @@ -14,8 +14,7 @@ use super::super::{find_resolved, AnimDriver}; use super::select::{self, jump_land_pick, Mode, Special}; /// The **base-animation lock** — the reference's `[unit+0xd58]` bits `0xc0000`, which are the whole -/// reason a Lashed player visibly falls over (VERIFIED, wow-re `base-anim-lock-knockdown.md`; -/// decision 2096, correcting 2085's §3). +/// reason a Lashed player visibly falls over (decision 2096, correcting 2085's §3). /// /// `CGUnit::PlayAnimation 0x5fe2f0` opens with a guard that returns having done nothing at all: /// @@ -51,7 +50,7 @@ impl BaseAnimLock { } /// The guard: is a play refused right now? An **unconditional early return**, not a priority - /// comparison and not a dedup (`base-anim-lock-knockdown.md` §7.3). + /// comparison and not a dedup (`0x5fe3a1`). pub(super) fn refuses(self) -> bool { self.0.is_some() } @@ -66,7 +65,7 @@ impl BaseAnimLock { /// The clearer, keyed on the **finished** id. Called once a frame before anything re-picks the /// base, so completion and pre-emption both release it: an overridden clip's node still reports /// finished at the end of its own span, which is exactly the reference's "the base stays locked - /// for the rest of the clip's window" when a mount arms over a knockdown (§7.6). + /// for the rest of the clip's window" when a mount arms over a knockdown (`0x607b44`). pub(super) fn release_finished( &mut self, player: &AnimationPlayer, @@ -80,12 +79,13 @@ impl BaseAnimLock { } } - /// Drop the lock outright — the reference's other two clearing sites (§7.5/§7.6). + /// Drop the lock outright — the reference's other two clearing sites. /// /// A **model rebuild** needs no call here: benilla's rebuild removes [`AnimDriver`] itself /// (`entities::live_display`), so the lock goes with it and the unit comes back with the base /// unheld — which is the reference's own outcome at `0x60adee`, reached differently. What does - /// call it is the pair of arms the reference never gates: the death pose and the mount attach. + /// call it is the pair of arms the reference never gates: the death pose (`0x5fc563`) and the + /// mount attach (`0x607b44`). pub(super) fn release(&mut self) { self.0 = None; } @@ -191,7 +191,7 @@ pub(super) fn roll_oneshot<'a>( } /// Pick a looping arm's **variation** (decision 0123 — the client's base-arm `variationIdx = −1`, -/// wow-re `loop-replay-fidget.md` §5b): a **relaxed** arm makes the weighted `_rand` walk (the +/// `0x5fe697`): a **relaxed** arm makes the weighted `_rand` walk (the /// same roll as a one-shot's — this is where a re-armed Stand lands on its rare look-around /// variations, the fidget); a combat/cast arm is forced to the deterministic head /// ([`select::arm_forces_head`] decides which, at the call site). The kernel itself never @@ -213,8 +213,8 @@ pub(super) fn pick_loop_variation<'a>( } /// The looping arm's two rolls in op4's order (variation `0x71249a`, then the replay budget -/// `0x712692..` — the same two `_rand` sites as a one-shot's; decision 0516, wow-re -/// `loop-replay-fidget.md` §7d): the budget is live for **loops** too — not as a repeat cap but +/// `0x712692..` — the same two `_rand` sites as a one-shot's; decision 0516): the budget is live +/// for **loops** too — not as a repeat cap but /// as the watchdog **window**, `R` clip-lengths wide (`windowHi = arm + span·R`). Returns the /// armed clip + `R` = total passes before the watchdog re-arms (`R ∈ [min, max−1]` floored to 1 /// — `replayMax` is exclusive, and `(0,0)`/`(0,1)` both play exactly once, visibly). diff --git a/crates/benilla-app/src/creature_anim/driver/tests.rs b/crates/benilla-app/src/creature_anim/driver/tests.rs index 40ac6e8e..a626940e 100644 --- a/crates/benilla-app/src/creature_anim/driver/tests.rs +++ b/crates/benilla-app/src/creature_anim/driver/tests.rs @@ -246,7 +246,7 @@ fn stationary_cast_hold_stows_an_engaged_casters_weapon() { /// The committed-move route: the hold loops masked on the torso over the gait, and its stow must /// HOLD between plays — the reconcile is edge-triggered like the client's (`0x5fdf80` runs only -/// inside `PlayAnimation`, wow-re `sheath-policy.md`), so the base track's flags-less Run never +/// inside `PlayAnimation`), so the base track's flags-less Run never /// re-draws mid-hold on the frames where nothing plays. This was the caster staff bug's shape: /// the per-frame base-track re-assert yanked the weapon back out one frame after the retake. #[test] @@ -380,7 +380,7 @@ fn spawn_fidgeter(app: &mut App) -> (Entity, Vec) { (unit, nodes) } -/// The emergent idle fidget (decision 0123 — wow-re `loop-replay-fidget.md` §5b): a RELAXED base +/// The emergent idle fidget (decision 0123): a RELAXED base /// arm rolls its variation (the client's `variationIdx = −1`), an engaged one is forced to the /// deterministic head, and the idle re-face turn-shuffle ([`crate::net::FacingStep`]) drives the /// Shuffle↔Stand churn whose every return to Stand re-rolls. @@ -644,8 +644,8 @@ fn spell_flinch_picks_the_wound_by_engagement() { assert_eq!(node(idle), Some(2), "unengaged: StandWound(8)"); } -/// The wound trigger's fourth entry gate (`0x60eaac`–`0x60eac8`, wow-re -/// `charproc-rate-override-wound-gate.md`; decision 2063): a CharProc-11 rate-override node on +/// The wound trigger's fourth entry gate (`0x60eaac`–`0x60eac8`; decision 2063): a CharProc-11 +/// rate-override node on /// the unit — what a freeze aura (Ice Block, Freezing Trap, petrify, web wrap) leaves attached — /// refuses every flinch for its life. The gate is the node's *presence*: kit 3071's rate of 1.0 /// (no freeze at all) closes it exactly like the family's 0.0. A unit without the node flinches. @@ -801,7 +801,7 @@ fn whiff_slowdown_spares_a_non_swing_oneshot() { } /// A same-frame swing/kit-anim collision runs the client's COMBAT FAST-PATH (decision 0406, -/// wow-re `combat-anim-fastpath.md`): the requests replay in [`PlaySeq`] wire order, the FIRST +/// `0x5fe43c`–`0x5fe48b`): the requests replay in [`PlaySeq`] wire order, the FIRST /// arms, and the second — combat over combat — does NOT overwrite it: the armed clip doubles /// to 2× and the second parks in the deferred cache. Both wire orders keep the first arrival on /// the body. The director's ref ground truth this pins: the Eviscerate spin survives the @@ -1058,8 +1058,8 @@ fn a_movement_flag_change_cuts_a_full_body_oneshot_immediately() { /// A stationary caster mouselook-turning: the chase-step TURN flag flickers at mouse-event /// cadence (set on delta frames, clear on quiet ones — `drive_body_heading`'s fold), but the -/// client's cast pin tests `[9e8] & 0x20000f` — translation + swim, NEVER the turn bits (wow-re -/// `spell-visual-apply.md` §2.1, `move_flags::CAST_PIN_MOVE`) — so the full-body hold stays +/// client's cast pin tests `[9e8] & 0x20000f` — translation + swim, NEVER the turn bits +/// (`0x5fde80`, `move_flags::CAST_PIN_MOVE`) — so the full-body hold stays /// pinned through the flap. Routing this through the one-shot mask (`0x20003f`) instead churned /// the gait hold↔Shuffle on every mouse-delta frame — the frostbolt right-drag jitter /// (decision 0491). @@ -1415,7 +1415,7 @@ fn the_creep_vis_flag_prowls_the_body() { assert_eq!(gait(&app), Some(5), "broken stealth runs again"); } -/// The looping-variation ADVANCE (decision 0516 — wow-re `loop-replay-fidget.md` §7/§7d, the +/// The looping-variation ADVANCE (decision 0516 — the /// watchdog `0x719370`): a relaxed looping base arm installs a replay window (here `(1,1)` → one /// pass exactly); each completed window re-arms the id through the weighted, MEMORYLESS variation /// walk. Over a dozen windows both authored Stand variations must take the main slot — the @@ -1574,9 +1574,9 @@ fn a_jump_out_of_a_micro_detachment_still_enters_the_jump_bracket() { } /// The control on the launch rule: a step-off fall must stay a step-off fall for its whole arc. -/// The gait freeze is the §5-verified behaviour (decision 0868) and the new edge must not reach -/// into it — only a rise *past* the threshold is a launch, and a fall only ever accelerates -/// downward. +/// The gait freeze (`0x5fd8e8`) is the reference's behaviour (decision 0868) and the new edge +/// must not reach into it — only a rise *past* the threshold is a launch, and a fall only ever +/// accelerates downward. #[test] fn a_deepening_step_off_fall_never_becomes_a_jump() { let mut app = app(); @@ -1691,7 +1691,7 @@ fn landing_mid_cast_plays_the_land_pick() { /// The FALLINGFAR latch mid-one-shot is an edge (`Jump → Fall`): the client plays Fall(40) /// ONCE, on the substep it latches (`0x61a820` — the 0864-era per-tick re-assert was -/// §5-refuted, decision 0868), replacing the clip. A fresh cast armed AFTER the latch then +/// refuted, decision 0868), replacing the clip. A fresh cast armed AFTER the latch then /// holds bone 0 like any other airborne one-shot, until the landing pick cuts it. #[test] fn a_cast_over_the_fall_loop_holds_until_landing() { @@ -2061,8 +2061,8 @@ fn the_step_off_arc_freezes_the_gait_against_live_pins() { app.update(); // settle: Stand let gait = |app: &App| app.world().entity(unit).get::().unwrap().gait; assert_eq!(gait(&app), Some(0)); - // Step off a ledge (downward launch: no jump arc, no Special; vz ≠ 0 — the §5-verified - // freeze gate `FALLING && (FALLINGFAR || vz ≠ 0)`, decision 0868) … + // Step off a ledge (downward launch: no jump arc, no Special; vz ≠ 0 — the freeze gate + // `0x5fd8e8`, `FALLING && (FALLINGFAR || vz ≠ 0)`, decision 0868) … app.world_mut().entity_mut(unit).insert(MovementState { flags: move_flags::FALLING, vertical_speed: -3.0, @@ -2149,7 +2149,7 @@ fn a_midair_deferred_park_survives_the_level_and_dies_at_the_landing_play() { } /// The deferred cache's consuming read sits DOWNSTREAM of the airborne-freeze (`0x5fd392` -/// inside the `0x5fd360` recompute arm; §5-verified, decision 0868): a park made mid-arc is +/// inside the `0x5fd360` recompute arm; decision 0868): a park made mid-arc is /// never consumed mid-air, even with the body free — it waits, and the landing edge's play /// clears it. #[test] @@ -2194,8 +2194,8 @@ fn a_midair_park_is_not_consumed_before_landing() { ); } -/// **The ranged→melee handoff at every landed swing** (`0x625829`, wow-re `sheath-policy.md` §1's -/// `0x6255b0` row) — the director's report: a bow drawn by a shot that never fired, then a melee +/// **The ranged→melee handoff at every landed swing** (`0x625829` in +/// `0x6255b0`) — the director's report: a bow drawn by a shot that never fired, then a melee /// attack, and the swings keep coming out of the bow. The reconcile provably cannot fix it, and /// this pins both halves: the CONTROL (a sword swing while ranged-drawn leaves the stance at 2 — /// the client's melee force is gated `CUR != 2`, `0x5fe0f9`/`0x5fe13b`, so the ranged stance is @@ -2637,7 +2637,7 @@ fn a_scaled_creatures_walk_cycles_slower_than_an_unscaled_ones() { /// The mounted half of the same law: the client's divisor reads the MOUNT model /// (`[unit+0xdc] ?: [unit+0xd8]`), whose rendered scale is the rider's `OBJECT_FIELD_SCALE_X` -/// times the mount's own `CreatureDisplayInfo` column (wow-re `0x613ef0`). Our mount child carries +/// times the mount's own `CreatureDisplayInfo` column (`0x613ef0`). Our mount child carries /// only that column on its transform, so the driver must compose the host's in — a 1.5× sabre /// under a 2.0× rider divides by 3.0, not 1.5. #[test] @@ -3075,11 +3075,11 @@ fn the_weapon_visual_hold_alone_never_puts_a_shooter_in_the_drawn_idle() { /// The mid-volley half of the same report ("when it's on and I'm running it keeps repeating the /// aim animation weirdly") — **re-derived, and inverted, by decision 1544.** /// -/// 0994 read `shooter-stop-law.md` §J4 as: the completion dispatcher `0x5fc3f0` is never reached -/// for a bow id, so a finished AttackBow recomputes nothing and clamps on its tail. wow-re's §5 -/// refuted that absence proof — the dispatcher has a SECOND, deferred fire site (`0x719370` +/// 0994 read the reference as: the completion dispatcher `0x5fc3f0` is never reached for a bow +/// id, so a finished AttackBow recomputes nothing and clamps on its tail. That absence proof is +/// wrong — the dispatcher has a SECOND, deferred fire site (`0x719370` /// enqueues the callback as a plain argument with mode 0; `0x7074b0` invokes it later as -/// `call [esi+4]`, which an instruction-encoding census cannot see) — and decoded its jump table: +/// `call [esi+4]`, which an instruction-encoding census cannot see); its jump table: /// 46/49/107 land on slot 22, a bare `RecomputeBaseAnim(-1)`, and a finished Load lands on slot /// 11/12/15, which arms the Hold **unconditionally**. /// @@ -3732,7 +3732,7 @@ fn a_disarmed_attacker_swings_and_stands_unarmed() { /// The other rung of the ladder: with **no weapon in the main hand** the disarm falls to the off /// hand, and that — not the dual-wield case — is what reaches AttackUnarmedOff(117) (decision -/// 1863; wow-re `disarm-weapon-gate-law.md` §7's table). +/// 1863; the swing selector `0x6246a0`). #[test] fn an_off_hand_only_fighter_is_the_case_that_punches_off_hand() { let mut app = app(); @@ -3970,9 +3970,8 @@ fn a_no_wound_creature_takes_no_flinch() { } /// The flag's **second** consumer: `DO_NOT_PLAY_WOUND_ANIM` takes the victim's **parry** with its -/// flinch (`0x60ec1f` inside the parry pick `0x60ec00`, wow-re -/// `wound-parry-gate-and-injury-vocal.md` Q1/Q6 — the bit has exactly two callers and this is the -/// other one). The `$CPP` ladder enters `0x60ec00` only on victimState 3, so DODGE and BLOCK +/// flinch (`0x60ec1f` inside the parry pick `0x60ec00` — the bit has exactly two callers and this +/// is the other one). The `$CPP` ladder enters `0x60ec00` only on victimState 3, so DODGE and BLOCK /// reach `PlayAnimation` directly and are **not** gated: a flagged creature still dodges, it just /// never parries. Both halves are asserted here — the gate without its control is the bug this /// whole record is about. @@ -4074,9 +4073,8 @@ fn the_no_wound_flag_takes_the_parry_but_not_the_dodge() { /// `0x602c60` → `0x5fd9e0` → `0x5fd8b0` → `0x5fd100`, and no locomotion id is in the combat set /// that takes its no-latch fast path — so a unit that simply starts running consumes the pending /// arm. That is what starts Charge's trail: kit 44's anim id is `-1`, `0x60f366 jl` skips the play -/// block entirely, and the arm waits for the charge's own run (wow-re -/// `charproc8-trail-draw-state.md` §11.5/§11.6). A one-shot-only edge would leave 38 Charge spells -/// with no trail at all. +/// block entirely, and the arm waits for the charge's own run. A one-shot-only edge would leave 38 +/// Charge spells with no trail at all. /// /// The other half matters as much: there is **no per-frame animation recompute** in the reference /// (`0x5fd8b0` has one caller and `0x5fd9e0`'s 38 sites are all event-driven), so a unit standing @@ -4127,8 +4125,7 @@ fn a_gait_change_raises_the_anim_edge_and_a_steady_frame_does_not() { assert!(edge(&app), "and so does a one-shot"); } -/// **The combat fast path must NOT raise the anim edge** (decision 2076, wow-re -/// `format7-lighting-term.md`'s second question). +/// **The combat fast path must NOT raise the anim edge** (decision 2076). /// /// `0x5fe43c` returns at `0x5fe48b` — *before* `0x5fe48e`, the weapon-trail latch's only read — /// when the unit is already playing a combat animation and requests another one. So a pending @@ -4192,7 +4189,7 @@ fn a_combat_over_combat_fast_path_does_not_raise_the_anim_edge() { ); } -/// The **base-animation lock** (decision 2096, VERIFIED wow-re `base-anim-lock-knockdown.md`) — +/// The **base-animation lock** (decision 2096) — /// the reason a Lashed player visibly falls over, and the correction to 2085's reading. /// /// A stun's root recomputes the base unconditionally and the selector resolves `Stand(0)`; what diff --git a/crates/benilla-app/src/creature_anim/driver/wound.rs b/crates/benilla-app/src/creature_anim/driver/wound.rs index b19b7df4..086ab0fb 100644 --- a/crates/benilla-app/src/creature_anim/driver/wound.rs +++ b/crates/benilla-app/src/creature_anim/driver/wound.rs @@ -65,7 +65,8 @@ pub(super) fn wound_upkeep(entity: Entity, drv: &mut AnimDriver, player: &mut An /// the slot). So this frame's full-body plays (bone 0: a swing on the base, a gait/mode /// change) evict a FULL-BODY wound, and masked-slot plays (the key-bone: a masked swing, /// the cast-hold retake) evict a MASKED wound — while the *other* bone's plays leave the -/// wound decaying (§3's inherited-swing case: a full-body swing under a masked wound). +/// wound decaying (the kernel `0x714260`'s inherited-swing case: a full-body swing under a +/// masked wound). /// This is exactly why the real client's flinch never smothers the next attack: the swing /// reclaims the slot the instant it starts. Mode/gait changes proxy the mode machine's /// plays — a change with no play (Land's re-pick) merely evicts one frame before the play @@ -99,12 +100,12 @@ pub(super) fn wound_evict( } } -/// The victim wound flinch (decision 0111 — the §5 verdict, rebuilt from bytes after +/// The victim wound flinch (decision 0111 — rebuilt from bytes after /// the first routing was director-falsified): a landed hit lays the wound clip into this /// unit's **secondary slot** (`0x60ea70` → op4 `linkFlag=0`) — a decaying 0.75-amplitude /// blend overlay over whatever plays. It never touches the base track or the one-shot /// slot: the victim's own in-flight swing keeps running underneath (there is NO mid-swing -/// gate — both §5 trigger agents refuted that hypothesis), and the upkeep above blends it +/// gate), and the upkeep above blends it /// out and self-releases — until a same-bone re-arm evicts it (the block above; a wound /// triggered here is this frame's *last* write, matching the client's packet order). /// The trigger's own entry gates live in the caller ([`super::drive_animations`]): the victim's diff --git a/crates/benilla-app/src/creature_anim/emote_anim.rs b/crates/benilla-app/src/creature_anim/emote_anim.rs index 594bf9b0..6824f2fc 100644 --- a/crates/benilla-app/src/creature_anim/emote_anim.rs +++ b/crates/benilla-app/src/creature_anim/emote_anim.rs @@ -17,10 +17,10 @@ //! `SMSG_EMOTE` at all — so they never reach this consumer; they're the state-emote idle in the //! gait layer instead ([`super::select::is_bare_stand`]). //! -//! **The receive-side gate is `EmoteFlags`-BLIND** (wow-re `object-layer/scratch/emote-posture- -//! gate.md`, commit `f9584b45`, §2): the real client's `SMSG_EMOTE` handler (`0x5e66b0`) never -//! reads the `Emotes.dbc` record at all — it suppresses only on the performer's own state, **stand- -//! state == 3 (SLEEP)** or the **swim move-flag**, and nothing else (no sit check, no flags check). +//! **The receive-side gate is `EmoteFlags`-BLIND**: the real client's `SMSG_EMOTE` handler +//! (`0x5e66b0`) never reads the `Emotes.dbc` record at all — it suppresses only on the performer's +//! own state, **stand-state == 3 (SLEEP)** or the **swim move-flag**, and nothing else (no sit +//! check, no flags check). //! This is why a *seated* `/clap` still plays for observers though a seated `/bow` never even sent //! the packet (the send-side gate in `crate::ui_chat` stopped it there): the two gates are different //! and independent. [`receive_eligible`] mirrors exactly this predicate — do not add an EmoteFlags @@ -95,12 +95,13 @@ pub(super) fn receive_eligible(stand_state: u8, swimming: bool) -> bool { } /// The **shared player's** half of the gate — the client's `0x5fcd20`, the single function both -/// emote producers tail into (wow-re `chat-talk-gesture.md` §4.2/§8, gates 10 and 12). Beyond the -/// posture pair it refuses to play while the unit is **channeling** or **in combat**. +/// emote producers tail into. Beyond the posture pair it refuses to play while the unit is +/// **channeling** (`0x5fcd83`) or **in combat** (`0x5fcd9d`). /// /// Two of `0x5fcd20`'s own tests are not repeated here because benilla already enforces them -/// downstream or cannot see them: the *already-armed* test (gate 9) is the anim driver's same-id -/// dedup, and `[+0xd58] & 0x400` (gate 11) is an internal anim-state bit with no benilla equivalent. +/// downstream or cannot see them: the *already-armed* test (`0x5fcd5d`) is the anim driver's +/// same-id dedup, and `[+0xd58] & 0x400` (`0x5fcd8e`) is an internal anim-state bit with no +/// benilla equivalent. fn player_eligible(channeling: bool, in_combat: bool) -> bool { !channeling && !in_combat } @@ -176,7 +177,7 @@ mod tests { } /// The shared player refuses while channeling or in combat — the two `0x5fcd20` tests benilla - /// was missing on BOTH producers, not just the new one (wow-re `chat-talk-gesture.md` §9 claim 2). + /// was missing on BOTH producers, not just the new one. #[test] fn channeling_or_combat_suppresses_the_play() { assert!( diff --git a/crates/benilla-app/src/creature_anim/env_damage.rs b/crates/benilla-app/src/creature_anim/env_damage.rs index 0d603427..eab176a1 100644 --- a/crates/benilla-app/src/creature_anim/env_damage.rs +++ b/crates/benilla-app/src/creature_anim/env_damage.rs @@ -1,6 +1,5 @@ -//! Environmental-damage feedback — the fall-landing dust puff and pain grunt (decision 0412, -//! byte-verified wow-re `object-layer/scratch/smsg-environmentaldamage.md`). Two independent -//! sources fire it, matching the reference's own double-fire: +//! Environmental-damage feedback — the fall-landing dust puff and pain grunt (decision 0412). +//! Two independent sources fire it, matching the reference's own double-fire: //! //! 1. **The wire arm** (`net/apply`): `SMSG_ENVIRONMENTALDAMAGELOG` → [`EnvDamageTable`] maps the //! damage type (0 exhausted · 1 drowning · 2 fall · 3 lava · 4 slime · 5 fire) to a diff --git a/crates/benilla-app/src/creature_anim/events.rs b/crates/benilla-app/src/creature_anim/events.rs index aa907f2d..ecb6256d 100644 --- a/crates/benilla-app/src/creature_anim/events.rs +++ b/crates/benilla-app/src/creature_anim/events.rs @@ -34,7 +34,7 @@ pub(crate) struct AnimSoundEvent { /// event.position)`, resolved here at the fire and carried by value exactly as the M2 event /// kernel `0x719370` snapshots it into the deferred callback record that every dispatcher /// (`0x5ffbd0` units, `0x5f3e20` GameObjects, `0x6951e0` placed models) then reads (decision - /// 1904; wow-re `spell/scratch/camera-shake-producers.md` §5). + /// 1904). /// /// It is emphatically not the object's origin: corpus-wide 149 of 244 `$DSL` records sit off /// it, out to **67.6 yd** on `Maraudon_Waterfall01.m2`, and the six `$CSD` records every @@ -69,8 +69,7 @@ impl EventFrame<'_> { } /// A footfall is **two independent channels**, and a tag belongs to exactly one of them — the -/// client's event dispatcher `0x5ffbd0` routes them to two different handlers (wow-re -/// `footprint-decals.md` §1, §5 4-agent round, byte-arbitrated): +/// client's event dispatcher `0x5ffbd0` routes them to two different handlers: /// /// - **`$FSD` → `0x623390`: the footstep SOUND**, and nothing else — it never reaches the decal /// path ([`is_footstep_sound`]). @@ -95,8 +94,7 @@ pub(crate) fn is_footstep_sound(ident: &[u8; 4]) -> bool { /// origin (decision 1856). /// /// **Measured from the CAMERA EYE, not the local player.** `FUN_004818f0()` returns -/// `[[0xb4b2bc]+0x65b8]` — the *active camera* — and its `+0x8/+0xc/+0x10` is the eye; wow-re -/// corrected that mislabel in `dist2-gate.md` (2026-08-22, with the camera-dtor proof). +/// `[[0xb4b2bc]+0x65b8]` — the *active camera* — and its `+0x8/+0xc/+0x10` is the eye. /// /// **Unconditional** — the local player's own feet are gated like anyone else's. The reference's /// `GUID == local player` compare (`0x5fc042`–`0x5fc06b`) is the decal call's fifth argument, the @@ -117,8 +115,7 @@ const FOOTFALL_RADIUS_SQ: f32 = 2500.0; /// bit product is exact, and the sum of three ≤50-bit values is exact — which also makes the /// reference's `(dz² + dy²) + dx²` summation order unobservable here, so this does not pretend to /// map its axis names onto ours. Round once on the way out and the f32 handed to the compare is -/// bit-identical (wow-re `dist2-gate.md`, the shared kernel + its `fstp`-round-trip exception -/// list; `footfall-camera-distance-gate.md` for this call site). +/// bit-identical. /// /// The compare is **strict** — bail iff `> FOOTFALL_RADIUS_SQ`, so exactly 50 yd passes, and so /// does a NaN: `test ah,0x41; je` reads ZF and takes the unordered result as "keep", which Rust's @@ -171,7 +168,7 @@ pub(crate) type TrackMemory = bevy::ecs::entity::EntityHashMap; /// [`super::driver::drive_animations`] so the clip/seek state is this frame's. Per unit we /// remember the playing node and its seek ([`TrackSeek`]); a loop wrap fires the tail /// `(prev, duration]` then the head `[0, cur]` — a `t = 0` key really does re-fire on every wrap, -/// in the reference too (wow-re `m2-event-track-walker.md` §2), which is why a held turn-in-place +/// in the reference too (the walker `0x719370`), which is why a held turn-in-place /// lays a footprint pair twice a second there as well. /// /// Arming is [`advance_track`]'s: an arm frame fires nothing, the frame after it opens the clip's @@ -535,8 +532,8 @@ mod tests { /// The turn-in-place cadence, which this change deliberately leaves alone: HumanMale's /// ShuffleLeft (anim 11) is a **0.500 s loop** whose only keys are `$SL0` and `$SR0`, both at - /// `t = 0.000`, and the reference re-fires a `t = 0` key on **every wrap** (wow-re - /// `m2-event-track-walker.md` §2, byte-traced) — so a held turn lays a print pair twice a + /// `t = 0.000`, and the reference re-fires a `t = 0` key on **every wrap** (the walker + /// `0x719370`) — so a held turn lays a print pair twice a /// second in the real client too. #[test] fn a_loop_wrap_refires_the_head_keys() { diff --git a/crates/benilla-app/src/creature_anim/gesture.rs b/crates/benilla-app/src/creature_anim/gesture.rs index 0d3137ae..a417f4fa 100644 --- a/crates/benilla-app/src/creature_anim/gesture.rs +++ b/crates/benilla-app/src/creature_anim/gesture.rs @@ -11,8 +11,6 @@ //! //! The same dispatcher has a second caller — the **NPC-interact** path, always with code 0 (talk) — //! which is why [`crate::target::click`] pushes through here too instead of writing a raw AnimID. -//! -//! Byte-exact spec: wow-re `object-layer/scratch/chat-talk-gesture.md`. use bevy::prelude::*; diff --git a/crates/benilla-app/src/creature_anim/impact.rs b/crates/benilla-app/src/creature_anim/impact.rs index 83ddeb85..a8612080 100644 --- a/crates/benilla-app/src/creature_anim/impact.rs +++ b/crates/benilla-app/src/creature_anim/impact.rs @@ -1,6 +1,5 @@ -//! The melee **impact-frame deferral** — the client's swing-hit timing, §5-verified end to end -//! (wow-re `object-layer/scratch/melee-impact-timing.md`, f86e665a; the victim-consequence table -//! `smsg-attackerstate-consequences.md`, ffd0b016): at SMSG receive, a locally resolved attacker +//! The melee **impact-frame deferral** — the client's swing-hit timing: at SMSG receive, a locally +//! resolved attacker //! caches the swing record (`attacker+0xd70`), plays the swing clip (rate 1.0, `0x5fe2f0`), and //! does **not** dispatch the victim. When the clip's playhead crosses an authored **`$AH0–3` or //! `$CAH`** event keyframe (rel ~300–670 ms in every shipped attack clip; the `$AH` digit picks a @@ -41,8 +40,7 @@ use super::SwingMessage; use crate::net::ObjectStore; /// **The LOOTABLE front gate on the victim dispatcher** (`0x624552 test byte -/// [[victim+0x110]+0x224],1` / `0x62455a jne 0x624689` — `UNIT_DYNAMIC_FLAGS` bit `0x1`; wow-re -/// `melee-impact-timing.md`, byte-census landed 2026-09-07). +/// [[victim+0x110]+0x224],1` / `0x62455a jne 0x624689` — `UNIT_DYNAMIC_FLAGS` bit `0x1`). /// /// `0x624530` bails **before every consequence it owns** — blood, wound flinch, impact sounds and /// the floating damage number alike (`0x624689` is the bare epilogue, and the number's call at @@ -75,13 +73,13 @@ pub(crate) struct SwingImpact { pub(crate) text_only: bool, /// The `$AH0`–`$AH3` digit when a creature natural-weapon tag fired this dispatch — it /// selects the attacker's `CreatureSoundData.CustomAttack` sound column and latches - /// `SWINGNOHITSOUND`, replacing the generic weapon-impact sound (`0x6247d0` §f, decision + /// `SWINGNOHITSOUND`, replacing the generic weapon-impact sound (`0x6247d0`, decision /// 0525). `None` for `$CAH` (character attack-hit), the receive-time unresolved-attacker /// fallback, and flushes. pub(crate) natural: Option, /// **The world point of the tag that fired this dispatch** — the reference's `edi = /// [ebx+0x10]`, carried from `0x624862` through both weapon-sound legs and pushed at - /// `0x6248ef`/`0x624950` (wow-re `anim-event-position-law.md` §3). `None` for a flush and for + /// `0x6248ef`/`0x624950`. `None` for a flush and for /// the receive-time fallback, where no tag fired at all and the reference has only the unit. pub(crate) pos: Option, } diff --git a/crates/benilla-app/src/creature_anim/lod.rs b/crates/benilla-app/src/creature_anim/lod.rs index 856fe94f..56d4cf2d 100644 --- a/crates/benilla-app/src/creature_anim/lod.rs +++ b/crates/benilla-app/src/creature_anim/lod.rs @@ -5,7 +5,7 @@ //! sampling off-view). //! //! 0448 shipped this as a modernization against a reference that "has NO view cull on unit -//! skeletons" — a verdict wow-re REFUTED on 2026-08-13 (`outdoor-object-pass-election.md`: every +//! skeletons" — a verdict that is wrong (`0x683dd0`: every //! scene object, units included, is frustum/horizon/room-elected each frame, and a pass-2 unit //! is neither drawn, nor animated, nor event-ticked — except creatures flagged `MORE_AUDIBLE`, //! re-linked for tick only). So this gate turned out to be the *faithful* direction, not a @@ -393,9 +393,9 @@ mod tests { /// The gate laws in one flow (0448, events re-lawed by 1482): an off-frustum rig parks /// (joints repointed at the park entity, bones frozen) while its seek clock keeps advancing — - /// and its event keyframes FALL SILENT, because the reference's pass-2 walk never ticks an - /// unflagged model (the 0448 "off-screen swings must not go silent" law rested on the verdict - /// wow-re refuted on 2026-08-13; the flagged exception is the next test's). A woken rig and a + /// and its event keyframes FALL SILENT, because the reference's pass-2 walk (`0x683dd0`) never + /// ticks an unflagged model (the 0448 "off-screen swings must not go silent" law rested on the + /// wrong "no view cull" verdict; the flagged exception is the next test's). A woken rig and a /// never-parked twin sample the SAME pose — the absolute-clock snap, not resume-from-freeze. #[test] fn parked_rig_keeps_its_clock_falls_silent_and_wakes_to_the_absolute_pose() { diff --git a/crates/benilla-app/src/creature_anim/net.rs b/crates/benilla-app/src/creature_anim/net.rs index 1813bb08..4734d960 100644 --- a/crates/benilla-app/src/creature_anim/net.rs +++ b/crates/benilla-app/src/creature_anim/net.rs @@ -259,8 +259,7 @@ fn ai_reaction( /// anim starts NOW; the victim feedback (blood/flinch/text/impact sounds) defers to the swing /// clip's attack-hit keyframe (`creature_anim::impact`, the client's `0x6247d0` router) — EXCEPT /// the center combat text, which the client fires **synchronously at packet parse** -/// (`0x6255b0 → 0x629d30 → 0x703f50`, one call stack — §5-verified, wow-re -/// `combat-text-update-emission-law.md`; decision 0580's fold-back). +/// (`0x6255b0 → 0x629d30 → 0x703f50`, one call stack; decision 0580's fold-back). fn attacker_state( mut s: AttackerState, index: &GuidIndex, @@ -330,7 +329,7 @@ fn attacker_state( // The **observed attacker auto-draws melee** — the ref's SECOND melee draw, independent // of the attack-start one, and the reason a swing is never delivered in the wrong stance: // `0x625829 cmp [attacker+0xd40],1; jne` → `SetSheatheState(1, bInstant=1, bFireEvent=1)` - // at `0x62583a`, byte-read here, tabulated in wow-re `sheath-policy.md` §1. It sits + // at `0x62583a`, byte-read here. It sits // immediately after the attacker resolve and **before** any hit-info handling, so even a // swing whose animation is suppressed (`HitInfo & 0x10000`) still draws. Nothing else in // the policy can do this job: the per-animation reconcile's melee force is gated to @@ -421,12 +420,11 @@ fn play_spell_visual( } } -/// `SMSG_ENVIRONMENTALDAMAGELOG`'s consequence (wow-re `sound/scratch/uisound-tables.md`: reader +/// `SMSG_ENVIRONMENTALDAMAGELOG`'s consequence (reader /// `0x624fcc` inside `0x624f30`): the EnvironmentalDamage.dbc 6-slot table picks the damage type's /// SpellVisualKit — fall's is the DustCloud_Land puff — played on the victim through the ordinary /// discrete kit play (`0x60edf0`), the same leg the kit-push opcode rides. The pain vocal's exact -/// trigger is a dispatched wow-re §5 (in flight) — it folds in as its own edge when the verdict -/// lands. +/// trigger is open — it folds in as its own edge once pinned. fn environmental_damage_log( e: EnvironmentalDamageLog, index: &GuidIndex, diff --git a/crates/benilla-app/src/creature_anim/select.rs b/crates/benilla-app/src/creature_anim/select.rs index 404eb5be..a7d3e051 100644 --- a/crates/benilla-app/src/creature_anim/select.rs +++ b/crates/benilla-app/src/creature_anim/select.rs @@ -1,6 +1,7 @@ -//! The pure animation-selection logic (RF-0057/0073 tables, movement/Special state, gait/swing/ready -//! picks, playback-rate math) — kept in its own file as it carries the bulk of the unit-tested selector -//! logic, separate from the Bevy driver systems in [`super::driver`]. +//! The pure animation-selection logic (the `0x5fd8b0` selector's and decision 0073's tables, +//! movement/Special state, gait/swing/ready picks, playback-rate math) — kept in its own file as it +//! carries the bulk of the unit-tested selector logic, separate from the Bevy driver systems in +//! [`super::driver`]. use benilla_assets::AnimClip; use bevy::prelude::*; @@ -8,11 +9,11 @@ use bevy::prelude::*; use crate::net::{RemoteMotion, Spline}; /// Fallback walk speed (yd/s) for a unit whose movement block didn't carry one — vanilla's default -/// creature walk speed. The run boundary is 2× this (`RecomputeBaseAnim`, RF-0057). +/// creature walk speed. The run boundary is 2× this (`RecomputeBaseAnim`, the `0x5fd224` compare). pub(super) const DEFAULT_WALK_SPEED: f32 = 2.5; /// Fast-run cutoff (yd/s): at or above it the unit plays the sprint animation (id 143) ahead of Run -/// (`[0x80c484]` in the real selector, RF-0057). +/// (`[0x80c484]` in the real selector). const FAST_RUN_SPEED: f32 = 11.0; /// Below this ground speed (yd/s) a streamed mover counts as standing still — guards a near-zero residual. @@ -25,29 +26,27 @@ pub(super) const DEATH: u16 = 1; /// ShuffleLeft (11) / ShuffleRight (12) — the **turn-in-place foot-shuffle**, picked below when /// the unit turns without translating. Named because their *lifecycle* is unlike any other gait: /// once armed they are held to their own clip window rather than released when the turn ends -/// (decision 1655, wow-re `object-layer/scratch/turn-shuffle-lifecycle.md`), and the driver has -/// to be able to say so. +/// (decision 1655; the clip-end watchdog `0x719370`), and the driver has to be able to say so. pub(super) const SHUFFLE_LEFT: u16 = 11; /// See [`SHUFFLE_LEFT`]. pub(super) const SHUFFLE_RIGHT: u16 = 12; /// StealthWalk (119) — the **prowl creep**, and with [`STEALTH_STAND`] the entire difference -/// stealth makes to a body's *pose*. Byte-verified (wow-re `rf57-movement-anim-select.md`, the -/// 2026-07-18 §5): both ids are gated by the same descriptor bit — `[[unit+0x110]+0x213] & 2`, -/// `UNIT_FIELD_BYTES_1` byte 3's CREEP flag ([`crate::net::ObjectStore`]'s `unit_is_stealthed`) — -/// 119 from the core cascade at `0x5fd1d3`, tested **after** the backward branch and **before the -/// whole speed tail**, so a prowling unit never plays Run or Sprint however fast it travels. +/// stealth makes to a body's *pose*. Both ids are gated by the same descriptor bit — +/// `[[unit+0x110]+0x213] & 2`, `UNIT_FIELD_BYTES_1` byte 3's CREEP flag +/// ([`crate::net::ObjectStore`]'s `unit_is_stealthed`) — 119 from the core cascade at `0x5fd1d3`, +/// tested **after** the backward branch and **before the whole speed tail**, so a prowling unit +/// never plays Run or Sprint however fast it travels. /// /// No aura, no visual kit, no spell id is involved. Stealth's *translucency* is its aura state /// kit's CharProc-14 (decision 0806) and its *pose* is this flag: two independent mechanisms off /// one server byte, which is exactly why the body could go translucent and still stand bolt -/// upright. (The same flag's other three read sites are nameplate/marker suppression — it drives -/// no body render at all; wow-re `ghost-death-visuals.md`.) +/// upright. (The same flag's other three read sites are nameplate/marker suppression, among them +/// `0x6070a0` and `0x60f600` — it drives no body render at all.) /// /// **119 is NOT rate-scaled.** The client's locomotion-rate whitelist `0x5fee80` is exactly -/// `{4,5,11,12,13,37,38,39,42,43,44,45,135,143,187}` (byte-verified, §5-agreed; wow-re -/// `rf57b-rate-jump-standstate.md`) and 119 is absent — the creep cycle plays at 1× whatever the -/// live speed, so [`playback_rate`] leaves it alone. +/// `{4,5,11,12,13,37,38,39,42,43,44,45,135,143,187}` and 119 is absent — the creep cycle plays at +/// 1× whatever the live speed, so [`playback_rate`] leaves it alone. pub(super) const STEALTH_WALK: u16 = 119; /// StealthStand (120) — the prowl idle, from the **last** resolver in the chain (the fallback idle @@ -56,17 +55,16 @@ pub(super) const STEALTH_WALK: u16 = 119; /// shuffle, the combat Ready idle, loot — outranks it. See [`STEALTH_WALK`] for the shared gate. pub(super) const STEALTH_STAND: u16 = 120; -/// Mount (91) — the rider's seated pose while a mount model is attached (decision 0441; -/// byte-verified, wow-re `mount-composition.md` B1): held **unconditionally** — moving, turning, -/// airborne. The client arms it outside the base selector (once at attach by `0x607a00`, then -/// re-forced on every `PlayAnimation` by `0x5fe2f0`'s mounted branch — there is NO mount leg in -/// the `0x5fd8b0` chain); benilla pins the gait slot instead, the same rendered result. The -/// locomotion the selector would pick plays on the mount model (same `0x5fd100` thresholds), -/// via the mount child's own driver pass. Variations roll as any base loop (HumanMale authors 3). +/// Mount (91) — the rider's seated pose while a mount model is attached (decision 0441): held +/// **unconditionally** — moving, turning, airborne. The client arms it outside the base selector +/// (once at attach by `0x607a00`, then re-forced on every `PlayAnimation` by `0x5fe2f0`'s mounted +/// branch — there is NO mount leg in the `0x5fd8b0` chain); benilla pins the gait slot instead, the +/// same rendered result. The locomotion the selector would pick plays on the mount model (same +/// `0x5fd100` thresholds), via the mount child's own driver pass. Variations roll as any base loop +/// (HumanMale authors 3). pub(super) const MOUNT: u16 = 91; -/// Loot (50) — the kneel-and-rummage held while a unit's loot window is open. Byte-verified end -/// to end (wow-re `loot-anim-leg.md`, the 2026-07-18 §5 + the 08-21 §5 trio; decisions 0515 / +/// Loot (50) — the kneel-and-rummage held while a unit's loot window is open (decisions 0515 / /// 1471 / 1477): the `0x5fd8b0` chain's loot leg `0x5fd260` → 0x32 = 50, chain order locomotion → /// **LOOT** → standState → combat/channel (movement outranks by position — the leg itself reads no /// movement state), leg gates `[+0xdc]==0` (never mounted) + the `[+0xd58]&0x40` enable. @@ -96,22 +94,22 @@ pub(super) const LOOT: u16 = 50; pub(super) const UNIT_FLAG_LOOTING: u32 = 0x400; /// `UNIT_FIELD_FLAGS` bit `0x1000_0000` — must be CLEAR for a remote unit's loot kneel -/// (`0x612710`, byte-verified; the §5's one INFERRED residue is this bit's Blizzard *name*, not +/// (`0x612710`; its one inferred residue is this bit's Blizzard *name*, not /// its role). vmangos never sets it on players, so the gate is inert on our wire — carried for /// the faithful predicate shape. pub(super) const UNIT_FLAG_LOOT_SUPPRESS: u32 = 0x1000_0000; /// MountSpecial (94) — the mounted flourish (the horse rears; decision 0441 P2). Plays on the -/// MOUNT model as a one-shot (§5-verified, wow-re `mount-composition.md`: MountSpecial routes to -/// the mount via the same `0x5fe2f0` op4 target as its locomotion), never on the rider — the -/// rider holds [`MOUNT`] throughout. Fired by the space-bar gate (self, locally at send time) -/// and by `SMSG_MOUNTSPECIAL_ANIM` (observed riders; our own echo is dropped in the net drain). +/// MOUNT model as a one-shot (MountSpecial routes to the mount via the same `0x5fe2f0` op4 target +/// as its locomotion), never on the rider — the rider holds [`MOUNT`] throughout. Fired by the +/// space-bar gate (self, locally at send time) and by `SMSG_MOUNTSPECIAL_ANIM` (observed riders; +/// our own echo is dropped in the net drain). pub(crate) const MOUNT_SPECIAL: u16 = 94; /// The three ids whose arm takes the **base-animation lock** — `0x5fdba0`'s tail keyed on the id -/// actually armed, byte table `0x5fdd90` (wow-re `base-anim-lock-knockdown.md` §2). While one of -/// these holds bone 0, `PlayAnimation` refuses every base request outright, which is what lets a -/// stunned victim's `Knockdown` play out over the root's own `Stand` recompute. +/// actually armed, byte table `0x5fdd90`. While one of these holds bone 0, `PlayAnimation` refuses +/// every base request outright, which is what lets a stunned victim's `Knockdown` play out over the +/// root's own `Stand` recompute. /// /// They are the clips with a definite start and end pose — the ones a re-pick mid-flight would /// leave the body wrong: knocked flat, or halfway off the ground. @@ -122,8 +120,8 @@ pub(crate) const LIFT_OFF: u16 = 192; pub(crate) const LAND: u16 = 200; /// Movement direction/mode flag bits, matching the client's CMovement `MOVEMENTFLAGS` (cached at -/// `unit+0x9e8`; VERIFIED wow-5875-re RF-0057 + the jump §5 cross-check). The selector tests these -/// exactly as the binary does, so a streamed unit can eventually drop the server's raw `u32` straight in. +/// `unit+0x9e8`). The selector tests these exactly as the binary does, so a streamed unit can +/// eventually drop the server's raw `u32` straight in. pub(crate) mod move_flags { pub const FORWARD: u32 = 0x1; pub const BACKWARD: u32 = 0x2; @@ -139,9 +137,8 @@ pub(crate) mod move_flags { pub const WALK_MODE: u32 = 0x100; /// MOVEFLAG_LEVITATING — **the free-flight bit, and it works by SUPPRESSION.** It is the very /// first test in the client's per-frame swim decision `0x6030c0` (`0x6030d2 test ah,4` → bail to - /// `0x6031fa`; VERIFIED, wow-re `collision/scratch/swim-transition.md`): set, and the whole - /// water/depth decision is skipped — neither the ENTER arm nor the STOP arm runs, so liquid can - /// no longer latch *or unlatch* [`SWIMMING`]. + /// `0x6031fa`): set, and the whole water/depth decision is skipped — neither the ENTER arm nor + /// the STOP arm runs, so liquid can no longer latch *or unlatch* [`SWIMMING`]. /// /// That suppression is the entirety of GM flight. vmangos's `.cheat fly` sends /// `LEVITATING | SWIMMING | MOVED | FLYING` (`Player::SetFly`): `SWIMMING` puts the mover in the @@ -152,13 +149,13 @@ pub(crate) mod move_flags { pub const LEVITATING: u32 = 0x400; /// JUMPING/FALLING — set for the whole airborne arc; drives the jump Special state. pub const FALLING: u32 = 0x2000; - /// MOVEFLAG_FALLINGFAR — the arc has become a **far fall** (`0x633220`/`0x633240`; wow-re - /// `land-anim-height-gate.md`, legs corrected by decision 0179): a *jump* (launch vz ≠ 0) - /// latches once it descends 1/9 yd below its launch height; a *step-off fall* (launch vz = 0) - /// latches at 500 ms airborne (≈ 2.41 yd of free fall — a fence hop never latches, a - /// wagon-height drop latches just before the floor). Cleared only with FALLING at StopFalling - /// (`0x7c6290`). Mid-air it swaps the pose to Fall(40) and it opens the landing-anim gate; a - /// flat jump never descends below its takeoff, so its hang stays Jump(38). + /// MOVEFLAG_FALLINGFAR — the arc has become a **far fall** (`0x633220`/`0x633240`; legs + /// corrected by decision 0179): a *jump* (launch vz ≠ 0) latches once it descends 1/9 yd below + /// its launch height; a *step-off fall* (launch vz = 0) latches at 500 ms airborne (≈ 2.41 yd + /// of free fall — a fence hop never latches, a wagon-height drop latches just before the + /// floor). Cleared only with FALLING at StopFalling (`0x7c6290`). Mid-air it swaps the pose to + /// Fall(40) and it opens the landing-anim gate; a flat jump never descends below its takeoff, + /// so its hang stays Jump(38). pub const FALLING_FAR: u32 = 0x4000; /// MOVEFLAG_ROOT — the server rooted this mover (death, until release). The controller MUST /// carry it in the root-apply ack's MovementInfo (vmangos `HandleMoveRootAck:715` KICKS a @@ -178,19 +175,18 @@ pub(crate) mod move_flags { /// MOVEFLAG_SAFE_FALL — **feather fall** (Slow Fall, Levitate; decision 0866). It has exactly /// one effect: the fall-velocity query `0x7c5d20` picks its terminal clamp on this bit /// (`0x7c5d23 test [ecx+0x40],0x20000000`) — 7.0 yd/s `[0x87d898]` instead of the ordinary - /// 60.148 `[0x87d894]`. wow-re's ledger labels it "in-water; selects swim gravity", which is the - /// bit's *shape* read without the server-side name: vmangos sets it only from + /// 60.148 `[0x87d894]`. A bytes-only reading labels it "in-water; selects swim gravity", which + /// is the bit's *shape* read without the server-side name: vmangos sets it only from /// `SPELL_AURA_FEATHER_FALL`, and swimming is the separate [`SWIMMING`] (0x200000). pub const SAFE_FALL: u32 = 0x2000_0000; - /// MOVEFLAG_HOVER — the body rests 1.0 yd above the ground (Levitate; decision 0866). VERIFIED - /// as hover rather than a wade bit in wow-re `system/collision/collision.md`: the WALK resolver - /// `0x6367b0` gates a `[0x7ff9d8]` = +1.0-yd surface offset on it, and the step-down reach - /// widens by the same yard (`0x633e35`). + /// MOVEFLAG_HOVER — the body rests 1.0 yd above the ground (Levitate; decision 0866). Hover + /// rather than a wade bit: the WALK resolver `0x6367b0` gates a `[0x7ff9d8]` = +1.0-yd surface + /// offset on it, and the step-down reach widens by the same yard (`0x633e35`). pub const HOVER: u32 = 0x4000_0000; /// The bits a **server-authored move packet owns**, and the only ones it may write — the - /// reference's flag *merge* mask, VERIFIED at `0x618c30 @0x618deb` - /// (`new = old ^ ((old ^ wire) & 0x75a07dff)`; wow-re `self-addressed-move.md`, decision 0725). + /// reference's flag *merge* mask, at `0x618c30 @0x618deb` + /// (`new = old ^ ((old ^ wire) & 0x75a07dff)`; decision 0725). /// Applying a `MSG_MOVE_*` is not an assignment: bits outside this mask are kept from local /// state, because they are the client's own (`0x618c30` also holds a second mask, /// `0x75a01dff`, for a caller arm that passes a non-zero 4th arg — the plain state family @@ -209,7 +205,7 @@ pub(crate) mod move_flags { /// assignment. One law, both movers: our own (`player::wire_in`'s self-addressed pose) and every /// watched one (`net::motion::remote::apply_move`), because the reference runs this merge inside /// the one scheduler both go through and the mask is arm-invariant across all thirty relay - /// opcodes (wow-re `collision/scratch/movement-relay-family-map.md`, decision 2064). + /// opcodes (`0x618f20` always takes the wide arm; decision 2064). /// /// The omission that bites is pinned by test on the self lane: [`ON_TRANSPORT`] sits **outside** /// the mask, so a server-authored pose can relocate a rider but never board or deboard them. @@ -224,11 +220,9 @@ pub(crate) mod move_flags { /// must pass before any physics runs on it. `CMovement::Update 0x616de0` opens its substep loop /// with `0x616e20 test dword [esi+0x40],0x20ff; je 0x616f49` (none set ⇒ finalize, having /// integrated nothing), and the movement manager tests the same mask before it ever calls the - /// integrator (`0x6166f5`) — the "idle gate" decision 0059 named by address. wow-re states the - /// consequence for a watched player outright: *"for a **STANDING remote** the integrator does - /// NOT run … a flag-less unit is not even in the mover list"* - /// (`collision/scratch/remote-air-facing.md` §A4; the loop gate is in `collision/collision.md` - /// and `scratch/spec-driver-A.md`/`-B.md`). + /// integrator (`0x6166f5`) — the "idle gate" decision 0059 named by address. The consequence + /// for a watched player: for a **STANDING remote** the integrator does NOT run, and a flag-less + /// unit is not even in the mover list (`0x618940`). /// /// So a mover with none of these bits keeps the pose its last packet wrote, verbatim, until a /// packet or a flag moves it — which is what decision 1545 makes benilla do. The bits: the four @@ -248,13 +242,13 @@ pub(crate) mod move_flags { FORWARD | BACKWARD | STRAFE_LEFT | STRAFE_RIGHT | TURN_LEFT | TURN_RIGHT | SWIMMING; /// The stationary-cast pin's "moving" test — the client's `[9e8] & 0x20000f` in the `0x5fde80` - /// cast-override gate (wow-re `spell-visual-apply.md` §2.1, VERIFIED): the direction bits plus - /// swim, and NOTHING else. The turn bits are absent — a caster turning in place (keys or a - /// mouselook body-step) keeps the full-body pin. Distinct from [`ROUTE_COMMITTED_MOVE`]'s - /// `0x20003f` (the one-shot route test at `0x5fe6dc` — a different byte site, a different - /// mask); using that mask here made the pin flap at mouse-event cadence — the transient - /// chase-step TURN flags demoted the hold to shuffle+overlay on every mouse-delta frame and - /// re-pinned on every quiet one (decision 0491, the frostbolt right-drag jitter). + /// cast-override gate: the direction bits plus swim, and NOTHING else. The turn bits are absent + /// — a caster turning in place (keys or a mouselook body-step) keeps the full-body pin. + /// Distinct from [`ROUTE_COMMITTED_MOVE`]'s `0x20003f` (the one-shot route test at `0x5fe6dc` — + /// a different byte site, a different mask); using that mask here made the pin flap at + /// mouse-event cadence — the transient chase-step TURN flags demoted the hold to + /// shuffle+overlay on every mouse-delta frame and re-pinned on every quiet one (decision 0491, + /// the frostbolt right-drag jitter). pub const CAST_PIN_MOVE: u32 = ANY_MOVE | SWIMMING; } @@ -279,14 +273,13 @@ pub(crate) fn ease_strafe_yaw(current_yaw: f32, aim: f32, offset: f32, dt: f32) } /// The strafe **body-heading offset** (radians) a unit's rendered root yaw sits from its aim — the -/// client's display-facing strafe blend (wow-5875-re `body-facing-pipeline.md` §3, `0x607ed0` tail): -/// ±π/2 for a pure strafe, ±π/4 when forward/back is also held (`flags & 3`). Yaw is left-positive -/// (WoW orientation and our Bevy yaw alike), so strafe-left is `+` — **mirrored while backpedaling** -/// (the client's sign fold `((flags>>1)&2) == (flags&2)`): a back-left diagonal faces the body -/// forward-right and backpedals along the movement line, legs never crossing. `0` when not strafing -/// (both strafe bits set cancel — no lateral motion results, unlike the client's left-bit-only fold, -/// an unobservable edge). The [`super::twist::BodyTwist`] counter-twist then walks the upper body -/// back toward the aim. +/// client's display-facing strafe blend (`0x607ed0` tail): ±π/2 for a pure strafe, ±π/4 when +/// forward/back is also held (`flags & 3`). Yaw is left-positive (WoW orientation and our Bevy yaw +/// alike), so strafe-left is `+` — **mirrored while backpedaling** (the client's sign fold +/// `((flags>>1)&2) == (flags&2)`): a back-left diagonal faces the body forward-right and backpedals +/// along the movement line, legs never crossing. `0` when not strafing (both strafe bits set cancel +/// — no lateral motion results, unlike the client's left-bit-only fold, an unobservable edge). The +/// [`super::twist::BodyTwist`] counter-twist then walks the upper body back toward the aim. pub(crate) fn strafe_body_offset(flags: u32) -> f32 { use std::f32::consts::{FRAC_PI_2, FRAC_PI_4}; let left = flags & move_flags::STRAFE_LEFT != 0; @@ -305,7 +298,7 @@ pub(crate) fn strafe_body_offset(flags: u32) -> f32 { } /// The **swim body pitch** — the one render law, for every mover that has a reported pitch -/// (TU-A, decision 0464 §1; `0x60a110`→`0x710620`, the Euler-ZYX builder). +/// (decision 0464 §1; `0x60a110`→`0x710620`, the Euler-ZYX builder). /// /// The reference composes `T·S·Rz(facing)·Ry(2π − pitch)`: yaw about WoW +Z, pitch about WoW +Y /// from `CMovement+0x20`, +pitch = nose up. That is this expression, worked through the basis @@ -317,21 +310,19 @@ pub(crate) fn strafe_body_offset(flags: u32) -> f32 { /// /// The facing it reads is `+0xc94`, the **render** facing rather than the raw movement one /// (`+0x1c`); for a swimmer the two coincide, because SWIMMING is on the display-facing SNAP list -/// (`mov [esi+0xc94],[esi+0xc98]`, wow-re `body-facing-pipeline.md`) — the same gate our callers +/// (`mov [esi+0xc94],[esi+0xc98]` in the `0x607ed0` tail) — the same gate our callers /// apply before handing a yaw in. /// /// The gate is `SWIMMING` **and** a forward/back bit: strafe-only, idle, and grounded all render /// **level** (the ground path). It is `CMovement+0x40 & 3`, read at `0x60857d` off the very word /// the create-block apply and every relayed `MSG_MOVE_*` merge into under `0x75a07dff` — **not** /// a `CM2Model+0x3c & 3` dirty gate, which is what an earlier reading of this law named and which -/// a wow-re §5 has since scanned for exhaustively and found does not exist (`0x710620` is an -/// unconditional 16-dword copy). Recorded in wow-re -/// `system/collision/scratch/create-block-swim-pitch.md`. +/// does not exist (`0x710620` is an unconditional 16-dword copy). /// /// One function rather than one per pose owner: the law has three consumers — our own avatar /// ([`crate::player::body_pose`]), an observed relay mover /// ([`crate::net::motion::extrapolate_remote_units`]), and the create-block seed -/// ([`crate::net::objects`]) — and TU-A is explicit that all of them present the *same* +/// ([`crate::net::objects`]) — and in the reference all of them present the *same* /// pitch. Held apart, they drift: the remote lane shipped in July 2026 with no test and no trace /// of its own, so "does an observed swimmer tilt?" had no answer short of the director's eye. pub(crate) fn swim_body_rotation(yaw: f32, flags: u32, pitch: f32) -> Quat { @@ -344,8 +335,9 @@ pub(crate) fn swim_body_rotation(yaw: f32, flags: u32, pitch: f32) -> Quat { } } -/// The per-frame movement descriptor that drives a unit's animation (wow-5875-re RF-0057). The player -/// controller fills it for our avatar from input; a streamed unit's is derived from its [`Spline`]. +/// The per-frame movement descriptor that drives a unit's animation (what the selector `0x5fd8b0` +/// reads). The player controller fills it for our avatar from input; a streamed unit's is derived +/// from its [`Spline`]. #[derive(Component, Clone, Copy, Default)] pub(crate) struct MovementState { /// Live movement speed (yd/s) — `0` while standing. The rate-scaler's numerator and the @@ -371,27 +363,25 @@ pub(crate) struct MovementState { pub(crate) stealthed: bool, /// Riding a **flying** server spline (the taxi flight): the client's selector reads the active /// CMovement's spline flags (`[[unit+0x118]+0xa4]+0x18`) and plays Fly 135 when the wire Flying - /// bit (0x200) is set — RF-0057 `0x5fd19c`. Stamped by [`unify`] from the entity's live + /// bit (0x200) is set — `0x5fd19c`. Stamped by [`unify`] from the entity's live /// [`Spline`] each frame (the same read-through the client does); the controller's stored /// component leaves it `false`. pub(crate) flying: bool, } /// A transition-bracketed animation state — a one-shot **enter** → **loop** → one-shot **exit**, as -/// opposed to the directly-cross-faded gaits. The ids are from the verified selector + the HumanMale.m2 -/// sequence table (wow-5875-re; decisions 0047/0049). +/// opposed to the directly-cross-faded gaits. The ids are from the reference selector + the +/// HumanMale.m2 sequence table (decisions 0047/0049). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(super) enum Special { /// Airborne from a **jump** (the arc launched upward): JumpStart(37) → Jump(38) hang. - /// **VERIFIED** (the swim-hop §5, wow-re `swim-jump-anim-law.md`): `0x60e480` case 0xbb - /// (MSG_MOVE_JUMP) arms **37 directly**, local and observed alike, and 38 follows only by - /// 37's own 833 ms window elapsing — a short arc (the ~0.24 s deep swim hop) never shows - /// 38, which the Entering→`oneshot_finished` machine reproduces. (rf57b's dispatcher - /// divergence is settled: `0x5fc3f0`'s events 0x25/0x26 → 38 is the separate - /// emote/anim-event dispatcher, not the jump path.) FALLINGFAR - /// latching mid-arc hands off to [`Special::Fall`]; the landing is [`jump_land_pick`]. A - /// **step-off fall** never enters here — its gait freezes (the base selector's keep-current, - /// via the takeoff-frozen flags/speed) until FALLINGFAR latches. + /// `0x60e480` case 0xbb (MSG_MOVE_JUMP) arms **37 directly**, local and observed alike, and 38 + /// follows only by 37's own 833 ms window elapsing — a short arc (the ~0.24 s deep swim hop) + /// never shows 38, which the Entering→`oneshot_finished` machine reproduces. (`0x5fc3f0`'s + /// events 0x25/0x26 → 38 is the separate emote/anim-event dispatcher, not the jump path.) + /// FALLINGFAR latching mid-arc hands off to [`Special::Fall`]; the landing is + /// [`jump_land_pick`]. A **step-off fall** never enters here — its gait freezes (the base + /// selector's keep-current, via the takeoff-frozen flags/speed) until FALLINGFAR latches. Jump, /// Airborne and **fallen far** (FALLINGFAR latched): the Fall(40) loop, entered *directly* — /// the client plays 40 the tick the flag latches (`0x602c40`), no enter one-shot — so @@ -449,13 +439,13 @@ impl Special { } } -/// The jump-landing one-shot, from the flags at touchdown — the client's land dispatcher `0x602c60` -/// (wow-5875-re rf57b §2), transcribed: swimming → no landing anim (the recompute picks the swim -/// gait); stopped (`flags & 0xf == 0`) → JumpEnd **39**; moving — unless BACKWARD (`0x2`) or -/// WALK-mode (`0x100`) — → JumpLandRun **187**; a **backpedaling or walking** landing plays **no -/// one-shot at all**, dropping straight into the gait (the reference backpedals the instant it -/// touches down; 187 is a forward-run footplant and never plays backward — the "forward run flash -/// after a jump-then-hold-S" bug). `None` = no landing clip, go straight to `Mode::Gait`. +/// The jump-landing one-shot, from the flags at touchdown — the client's land dispatcher +/// `0x602c60`, transcribed: swimming → no landing anim (the recompute picks the swim gait); stopped +/// (`flags & 0xf == 0`) → JumpEnd **39**; moving — unless BACKWARD (`0x2`) or WALK-mode (`0x100`) — +/// → JumpLandRun **187**; a **backpedaling or walking** landing plays **no one-shot at all**, +/// dropping straight into the gait (the reference backpedals the instant it touches down; 187 is a +/// forward-run footplant and never plays backward — the "forward run flash after a +/// jump-then-hold-S" bug). `None` = no landing clip, go straight to `Mode::Gait`. /// /// **A ROOTED arc end is not a landing** (decision 0880). A root or a stun caught mid-air ends the /// fall where the body hangs — `SetRoot 0x7c7340`'s `StopFalling` clears FALLING with no ground @@ -505,7 +495,7 @@ pub(super) enum Mode { /// While `under` is airborne the one-shot obeys the client's airborne-freeze: a finished clip /// clamps and holds its last frame, flag changes are keep-current no-ops, and only a Special /// *edge* exits — the FALLINGFAR latch's Fall (played ONCE, at the latch edge - /// `0x61a820` — 0864's per-tick re-assert was §5-refuted, decision 0868; a clip armed + /// `0x61a820` — 0864's per-tick re-assert was refuted, decision 0868; a clip armed /// after the latch holds like any other) and the `0x602c60` land pick at touchdown. Grounded /// (`under == None`) it returns to Gait when it finishes; a new one restarts it — and any /// movement-flag change since the **base** was last armed @@ -520,11 +510,11 @@ pub(super) enum Mode { } /// The gait an idle/moving unit should play in [`Mode::Gait`], most-specific id first with fallbacks so a -/// model lacking the ideal clip steps down rather than snapping (RF-0057 core, minus the Special states — -/// jump and the sit/sleep/kneel poses are handled by the state machine). `walk_speed` is the unit's own -/// walk speed (the run boundary is 2× it). `ready` is the engaged combat idle (decision 0073), played -/// only when standing on the ground — locomotion outranks it, and a swimming engaged unit treads -/// water (the client's own gate order). +/// model lacking the ideal clip steps down rather than snapping (the `0x5fd8b0` chain's core, minus +/// the Special states — jump and the sit/sleep/kneel poses are handled by the state machine). +/// `walk_speed` is the unit's own walk speed (the run boundary is 2× it). `ready` is the engaged +/// combat idle (decision 0073), played only when standing on the ground — locomotion outranks it, +/// and a swimming engaged unit treads water (the client's own gate order). pub(super) fn gait_candidates( state: &MovementState, walk_speed: f32, @@ -533,11 +523,11 @@ pub(super) fn gait_candidates( ) -> &'static [u16] { use move_flags::*; let f = state.flags; - // Swimming (RF-0057 core, `[9e8] & 0x200000`) — the VERIFIED `0x5fd100` cascade (the swim §5's - // TU-E, wow-re `swim-mechanism.md`): **TURN > STRAFE > BACKWARD > FORWARD**, ids turn→41, - // strafeL(0x4)→43 / strafeR(0x8)→44 (SwimLeft/SwimRight — names byte-read from - // AnimationData.dbc), back→45, fwd→42, idle→41. So a turning swimmer treads water whatever its - // travel bits, and a strafe diagonal (fwd+strafe, back+strafe) plays the side-stroke, not 42/45. + // Swimming (`[9e8] & 0x200000`) — the `0x5fd100` cascade: **TURN > STRAFE > BACKWARD > + // FORWARD**, ids turn→41, strafeL(0x4)→43 / strafeR(0x8)→44 (SwimLeft/SwimRight — names + // byte-read from AnimationData.dbc), back→45, fwd→42, idle→41. So a turning swimmer treads + // water whatever its travel bits, and a strafe diagonal (fwd+strafe, back+strafe) plays the + // side-stroke, not 42/45. if f & SWIMMING != 0 { return if f & (TURN_LEFT | TURN_RIGHT) != 0 { &[41, 0] @@ -553,7 +543,7 @@ pub(super) fn gait_candidates( &[41, 0] }; } - // A flying server spline — the taxi ride (RF-0057 `0x5fd19c`, between the swim block and the + // A flying server spline — the taxi ride (`0x5fd19c`, between the swim block and the // backward test in the byte cascade): the spline's wire Flying bit selects Fly 135 outright, // before the backward and speed branches — a 32 yd/s taxi plays Fly, never Sprint 143. The // one-step fallback mirrors AnimationData (Fly → 0 Stand); every shipped taxi mount authors @@ -561,11 +551,11 @@ pub(super) fn gait_candidates( if state.flying { return &[135, 0]; } - // Backward dominates strafe (RF-0057 `[9e8] & 2` → WalkBackwards 13). + // Backward dominates strafe (`0x5fd1bc`, `[9e8] & 2` → WalkBackwards 13). if f & BACKWARD != 0 { return &[13, 4, 0]; } - // Stealthed and moving — the prowl (RF-0057 `0x5fd1d3`, sitting between the backward branch and + // Stealthed and moving — the prowl (`0x5fd1d3`, sitting between the backward branch and // the speed tail; [`STEALTH_WALK`]). The gate outranks the ENTIRE speed tail, so a stealthed // unit creeps at any speed, while a *backpedaling* one plays WalkBackwards — backward is tested // first. The Walk 4 fallback is `AnimationData.dbc`'s own Fallback for row 119, which is also @@ -575,8 +565,8 @@ pub(super) fn gait_candidates( if state.stealthed && f & ANY_MOVE != 0 { return &[STEALTH_WALK, 4, 0]; } - // Ground gaits by live speed (RF-0057 core): fast-run ≥ 11, run > 2× walk, else walk. - // (Strafe currently routes here to Run/Walk; the dedicated-shuffle question is under §5 verification.) + // Ground gaits by live speed (`0x5fd202`/`0x5fd224`): fast-run ≥ 11, run > 2× walk, else walk. + // (Strafe currently routes here to Run/Walk; whether it has a dedicated shuffle is open.) if f & ANY_MOVE != 0 { let s = state.speed; return if s >= FAST_RUN_SPEED { @@ -587,7 +577,7 @@ pub(super) fn gait_candidates( &[4, 0] }; } - // Turning in place (the turn keys with no translation): the foot-shuffle (RF-0057 `0x5fd3f0` → + // Turning in place (the turn keys with no translation): the foot-shuffle (`0x5fd3f0` → // ShuffleLeft 11 / ShuffleRight 12). Only reached when not moving — moving with a turn curves the // run path and plays the gait above. if f & TURN_LEFT != 0 { @@ -615,10 +605,10 @@ pub(super) fn gait_candidates( // The Hold twins 109/110 ARE reachable (decision 1544): `0x5fd460`'s own jump table writes // only 105/106/111/112, but a finished Load is promoted to its Hold by the completion // dispatch (`0x5fc3f0` slot 11/12/15), and the caller passes whichever of the pair - // currently owns the pose. 0994 left them out on §J4.1's absence proof — that the - // dispatcher is never reached for a bow id — which wow-re's §5 has since refuted. - // Each Hold falls back to its own Load first: a model that authors the pull but not the - // hold should freeze at full draw, not drop to ReadyUnarmed. + // currently owns the pose. 0994 left them out on an absence proof — that the dispatcher is + // never reached for a bow id — which is false: the completion dispatcher has a second, + // deferred fire site (`0x7075af`). Each Hold falls back to its own Load first: a model that + // authors the pull but not the hold should freeze at full draw, not drop to ReadyUnarmed. return match l { 105 => &[105, 25, 0], 106 => &[106, 25, 0], @@ -645,18 +635,17 @@ pub(super) fn gait_candidates( } } -/// The local auto-repeat **standing idle** id — the client's `0x5fd460` → LUT `0x5fd530` -/// (byte-verified, wow-re `ranged-shot-anim.md`): the RANGED-slot item's subclass picks a held -/// Load/Hold clip — Bow → LoadBow 105 · Gun/Crossbow → LoadRifle 106 · Thrown → LoadThrown 112 · -/// Wand → HoldThrown 111 · anything else → ReadyUnarmed 25. **Not** ReadyBow/AttackBow: no code -/// in the client plays those rows. +/// The local auto-repeat **standing idle** id — the client's `0x5fd460` → LUT `0x5fd530`: the +/// RANGED-slot item's subclass picks a held Load/Hold clip — Bow → LoadBow 105 · Gun/Crossbow → +/// LoadRifle 106 · Thrown → LoadThrown 112 · Wand → HoldThrown 111 · anything else → +/// ReadyUnarmed 25. **Not** ReadyBow/AttackBow: no code in the client plays those rows. /// -/// (This doc used to carry `ranged-shot-anim.md` §(a)'s "the per-shot caster fire animation is a -/// verified NEGATIVE — the shot shows only the missile". That was REFUTED twice over: by the -/// weapon-visual merge, which plays the fire clip off the weapon's own substitute visual -/// (decision 0370/0986), and by the completion dispatch below (decision 1544). It is gone rather -/// than hedged — a stale negative in a doc is how a session concludes the missing animation is -/// correct, which is exactly what happened to bug B307.) +/// (This doc used to carry the claim "the per-shot caster fire animation is a verified NEGATIVE — +/// the shot shows only the missile". That was REFUTED twice over: by the weapon-visual merge, which +/// plays the fire clip off the weapon's own substitute visual (decision 0370/0986), and by the +/// completion dispatch below (decision 1544). It is gone rather than hedged — a stale negative in a +/// doc is how a session concludes the missing animation is correct, which is exactly what happened +/// to bug B307.) pub(super) fn ranged_load_anim(ranged: Option<(u8, u8)>) -> u16 { match ranged { Some((2, 2)) => 105, // Bow → LoadBow @@ -679,7 +668,7 @@ pub(super) fn is_ranged_load(id: u16) -> bool { /// → HoldThrown 111. The wand's HoldThrown 111 is already the hold and re-arms itself, and /// anything else (ReadyUnarmed 25) holds nothing and stays put. /// -/// **The promotion is UNCONDITIONAL** (wow-re §5, decision 1544): the `[+0xd24]` ranged-prop and +/// **The promotion is UNCONDITIONAL** (decision 1544): the `[+0xd24]` ranged-prop and /// `[+0xd58] & 0x600` test at `0x5fc5bc` belongs to slot **13** — the Hold's own re-arm — not to /// the Load's slot 11, which is the bare `mov eax,0x6d ; push eax ; call 0x5fe2f0` at `0x5fc5e9`. /// Reading that gate onto the Load is the mistake that would leave a shooter frozen at full draw. @@ -696,8 +685,7 @@ pub(super) fn ranged_hold_anim(load: u16) -> u16 { } } -/// Does the drawn ranged Load idle own this unit's standing pose? Byte-verified, wow-re -/// `shooter-stop-law.md` §J6 claim 1 (§5, four independent pairs + byte arbitration): +/// Does the drawn ranged Load idle own this unit's standing pose? /// /// `0x5fd460` — tier 9 of `ComputeAnimation 0x5fd8b0`, and the **only** writer of 105/106/111/112 /// image-wide — claims on exactly two tests and nothing else: @@ -710,15 +698,15 @@ pub(super) fn ranged_hold_anim(load: u16) -> u16 { /// `0x200`'s only writer image-wide is the LOCAL cast-send `0x6e593b`, gated `AttributesEx2 & /// 0x20` — so entry means *this client's player is actively auto-repeating*. **`0x400` /// ([`super::RangedHold`]) is never tested in this function.** It appears only in `0x5fc3f0`'s -/// sustain gates (`test ah,0x6` for 109, `test ah,0x4` for 110/111/112), which §J4 shows never +/// sustain gates (`test ah,0x6` for 109, `test ah,0x4` for 110/111/112), which never /// run. Folding it into this gate is what let one Serpent Sting / Multi-Shot — any ranged-slot /// spell whose visual sets the hold bit — leave the shooter aiming a drawn bow with nothing able /// to clear it (director-reported 2026-08-05). /// /// A REMOTE shooter never runs the local cast-send, so it never enters here. It is not simply -/// Stand, though (§J6 claim 2, CORRECTED): it plays **LoadBow(105) once** at the volley's single -/// `SMSG_SPELL_START`, through the PrecastKit — our [`super::CastHold`] path — then AttackBow per -/// GO over its ordinary idle. Never a sustained aim pose. +/// Stand, though: it plays **LoadBow(105) once** at the volley's single `SMSG_SPELL_START` +/// (`0x6e7901`), through the PrecastKit — our [`super::CastHold`] path — then AttackBow per GO over +/// its ordinary idle. Never a sustained aim pose. /// /// The caller applies the tier order: locomotion (tier 4) outranks tier 9, so a moving unit is /// never in the idle whatever these bits say. @@ -754,7 +742,7 @@ pub(super) fn defense_anim(victim_state: u32, main: Option<(u8, u8)>) -> Option< /// The **play-time unarmed-special substitution** — the third weapon substitution in the client, /// and the only one that lives inside `PlayAnimation` itself rather than in a selector -/// (`0x5fe2f0` @ `0x5fe3cc`–`0x5fe3e9`, byte-verified: wow-re `disarm-weapon-gate-law.md` §7). +/// (`0x5fe2f0` @ `0x5fe3cc`–`0x5fe3e9`). /// A requested **Special1H(57) / Special2H(58)** — the weapon-remapped spin a spell kit asks for, /// Eviscerate's among them — becomes **SpecialUnarmed(118)** when `GetWeapon(0, 0)` *and* /// `GetWeapon(1, 0)` are both NULL, i.e. both hands are empty to the combat reading. @@ -777,8 +765,7 @@ pub(super) fn is_swing_id(id: u16) -> bool { matches!(id, 16..=19 | 85 | 87 | 88 | 117) } -/// The client's COMBAT-anim classifier (`0x5fcc10` — wow-re `combat-anim-fastpath.md` §3, the -/// exhaustive byte-decoded table, cross-confirmed against `anim-composition-model.md`): the gate +/// The client's COMBAT-anim classifier (`0x5fcc10` — the exhaustive byte-decoded table): the gate /// on `PlayAnimation`'s combat fast-path (decision 0406). A combat clip requested while another /// combat clip is playing is NOT armed — the current clip's rate doubles and the request defers. /// Members: the swings (16–19 main, 85/86 dagger, 87/88/117 off), the specials (57/58/118 — @@ -789,13 +776,12 @@ pub(super) fn is_combat_anim(id: u16) -> bool { matches!(id, 10 | 16..=24 | 30 | 36 | 57..=59 | 85..=88 | 95 | 117 | 118) } -/// The client's **CAST** classifier (`0x5fcbb0` — byte-decoded in wow-re `oneshot-lifecycle.md` -/// §7, previously unlabelled): the spell-cast release anims `{2, 32, 33, 53, 54}`. Together with -/// [`is_combat_anim`] it is what the **transplant** predicate (`0x5feae0`) tests on the *currently -/// armed bone-0 clip*: a locomotion request over one of these does not replace it — the clip moves -/// up onto the key-bone at its live play position ([`OneShotRoute`]'s two slots, decision 0878). -/// NOT the ReadySpell holds (51/52) — those are their own set (`0x5fde40`), and a jump over a -/// standing hold really does take the whole body. +/// The client's **CAST** classifier (`0x5fcbb0`): the spell-cast release anims +/// `{2, 32, 33, 53, 54}`. Together with [`is_combat_anim`] it is what the **transplant** predicate +/// (`0x5feae0`) tests on the *currently armed bone-0 clip*: a locomotion request over one of these +/// does not replace it — the clip moves up onto the key-bone at its live play position +/// ([`OneShotRoute`]'s two slots, decision 0878). NOT the ReadySpell holds (51/52) — those are +/// their own set (`0x5fde40`), and a jump over a standing hold really does take the whole body. pub(super) fn is_cast_anim(id: u16) -> bool { matches!(id, 2 | 32 | 33 | 53 | 54) } @@ -854,8 +840,8 @@ pub(super) fn swing_anim_off(wielded: Option<(u8, u8)>) -> u16 { } } -/// The draw/stow clip for one weapon slot, by the item's **sheathe-type** byte — the byte-verified -/// pick (wow-re `sheath-anim-pick.md`, 8 sites, byte-identical): `(1 << (type & 0x1f)) & 0x88` → +/// The draw/stow clip for one weapon slot, by the item's **sheathe-type** byte — the client's +/// pick (8 byte-identical sites, `0x611930`–`0x611cc6`): `(1 << (type & 0x1f)) & 0x88` → /// HipSheath(90) for types {3, 7}, Sheath(89) for everything else (back-mounts, shields, staves). /// The slot's own byte is the *only* input — mainhand, offhand, shield and ranged all run the same /// test, each on its own record. @@ -867,8 +853,7 @@ pub(super) fn sheath_clip(sheath_type: u8) -> u16 { } } -/// The ranged-handling anims exempt from the `&0x10` force-stow while ranged-drawn — -/// **byte-verified** (wow-re `ranged-sheath-exempt-autorepeat.md`, the 2026-07-15 §5): the +/// The ranged-handling anims exempt from the `&0x10` force-stow while ranged-drawn — the /// exemption is the predicate `0x5fe180` (sole caller `0x5fe04c`, the **CUR==2 path only**; /// index table `0x5fe1a8`, targets `0x5fe1a0`), returning true for exactly these nine ids — /// the ranged Load/Hold/Attack family. **ReadyThrown 108 is NOT exempt** and genuinely stows; @@ -885,8 +870,7 @@ const SHEATH_RANGED_EXEMPT: [u16; 9] = [46, 49, 105, 106, 107, 109, 110, 111, 11 /// 1. flag `&4` → stow (casts, swim, mount, sit-chair, loot); /// 2. mounted → stow — the persistent mounted draw-block (`0x5fdfd9`: the reconcile forces /// state 0 on every recompute while a mount model is attached, so weapons sit at their sheath -/// points and the manual toggle can never stick — wow-re `sheath-policy.md` §3, VERIFIED; -/// dismount does NOT restore the pre-mount state); +/// points and the manual toggle can never stick; dismount does NOT restore the pre-mount state); /// 3. flag `&0x10` → stow (emotes, unarmed attacks, sit-ground/kneel, bow/rifle/thrown shots) — /// except the three ranged-handling anims while ranged-drawn; /// 4. engaged **or** flag `&0x20` → draw melee (armed attacks, the Ready idles, fishing) — only @@ -927,20 +911,19 @@ pub(super) fn reconcile_sheath( } /// Whether a **looping base arm** keeps the deterministic HEAD variation instead of rolling a -/// fresh one — the client's re-zero gate on the arm helper (`0x5fdba0`, wow-re -/// `loop-replay-fidget.md` §5b): every base (re-)arm carries `variationIdx = −1` (a weighted -/// roll) **unless** the unit has an auto-attack target, is holding a cast/channel, or the -/// *outgoing* clip was a combat/cast/ready id — then it is forced to `0` (the head). This is why -/// a relaxed unit "looks around" on each Stand re-arm while a fighting one holds one steady idle. -/// The outgoing-id families here approximate the client's four classifier calls -/// (`0x5fcc10`/`0x5fcbb0`/`0x5fde40`/`0x5fde60`, exact id sets unpinned): the melee swings -/// (16–19), the ready stances (25–29, the 0111-cited set), and the spell ready/cast poses -/// (51–54) — engagement and the cast hold carry the main weight regardless (decision 0123). +/// fresh one — the client's re-zero gate on the arm helper (`0x5fdba0`): every base (re-)arm +/// carries `variationIdx = −1` (a weighted roll) **unless** the unit has an auto-attack target, is +/// holding a cast/channel, or the *outgoing* clip was a combat/cast/ready id — then it is forced to +/// `0` (the head). This is why a relaxed unit "looks around" on each Stand re-arm while a fighting +/// one holds one steady idle. The outgoing-id families here approximate the client's four +/// classifier calls (`0x5fcc10`/`0x5fcbb0`/`0x5fde40`/`0x5fde60`, exact id sets unpinned): the +/// melee swings (16–19), the ready stances (25–29, the 0111-cited set), and the spell ready/cast +/// poses (51–54) — engagement and the cast hold carry the main weight regardless (decision 0123). pub(super) fn arm_forces_head(engaged: bool, casting: bool, outgoing: u16) -> bool { engaged || casting || matches!(outgoing, 16..=19 | 25..=29 | 51..=54) } -/// The victim wound-flinch id (`0x60ea70`, decision 0111 §5.3 — byte-verified): decided **solely** +/// The victim wound-flinch id (`0x60ea70`, decision 0111 — byte-verified): decided **solely** /// by `(severity, engaged)`. Crit (`HitInfo & 0x80`) → CombatCritical; the victim engaged in /// melee (its auto-attack-target GUID set, `[unit+0xc48]`) → CombatWound; else StandWound. pub(super) fn wound_anim(hit_info: u32, engaged: bool) -> u16 { @@ -955,7 +938,7 @@ pub(super) fn wound_anim(hit_info: u32, engaged: bool) -> u16 { /// Whether the wound overlay covers the **full body** (the client forces op4's key-bone to `-1` = /// bone 0) or stays **masked** to the upper-body subtree — the two byte-decoded mechanisms -/// (decision 0111 §5.2, `0x60eae8` / `0x60eb9a`): +/// (decision 0111, `0x60eae8` / `0x60eb9a`): /// /// - **(A), all ids:** the victim's current bone-0 pose is a combat-ready stance {25–29} — a /// weapon-drawn victim standing between its own swings flinches full-body. `base_anim` is the @@ -963,9 +946,9 @@ pub(super) fn wound_anim(hit_info: u32, engaged: bool) -> u16 { /// `[block0+0xf8]`), so mid-swing the base is the swing — not ready — and the flinch masks. /// - **(B), StandWound(8) only:** genuinely stationary — the client's `[+0x118]+0x40 & 0x20200f` /// (move/jump/swim; note the keyboard-turn bits `0x30` are **not** in the mask) — **and not -/// mounted** (the secondary-blend note's `[unit+0xdc]==0` companion clause; a mounted rider's -/// flinch never replaces the seat pose, decision 0441). The transport-substate companion is -/// still a state benilla doesn't model. +/// mounted** (`[unit+0xdc]==0`, `0x60eb9a`→`0x60ebea`; a mounted rider's flinch never replaces +/// the seat pose, decision 0441). The transport-substate companion is still a state benilla +/// doesn't model. /// /// Everything else is masked: the legs keep the base animation untouched. pub(super) fn wound_full_body(id: u16, base_anim: u16, flags: u32, mounted: bool) -> bool { @@ -1019,12 +1002,12 @@ pub(super) fn ready_anim(main: Option<(u8, u8)>) -> u16 { } } -/// The Special state a unit is in this frame, if any. Airborne splits three ways (wow-re -/// `land-anim-height-gate.md` + rf57b §2): FALLINGFAR latched → **Fall** (the 40 loop); else a -/// **jump** arc (launched upward, `jump_arc`) → Jump (the 37/38 bracket); else a **step-off fall** -/// → `None` — the base selector's keep-current freeze: the gait keeps playing off the -/// takeoff-frozen flags/speed until FALLINGFAR latches or the unit lands. Standing with a -/// transition-able stand-state (sit/sleep/kneel) → Pose; movement suppresses a pose. +/// The Special state a unit is in this frame, if any. Airborne splits three ways: FALLINGFAR +/// latched → **Fall** (the 40 loop, `0x602c40`); else a **jump** arc (launched upward, `jump_arc`) +/// → Jump (the 37/38 bracket); else a **step-off fall** → `None` — the base selector's keep-current +/// freeze (`0x5fd8e8`): the gait keeps playing off the takeoff-frozen flags/speed until FALLINGFAR +/// latches or the unit lands. Standing with a transition-able stand-state (sit/sleep/kneel) → Pose; +/// movement suppresses a pose. pub(super) fn current_special(mv: &MovementState, jump_arc: bool) -> Option { if mv.flags & move_flags::FALLING != 0 { if mv.flags & move_flags::FALLING_FAR != 0 { @@ -1041,8 +1024,8 @@ pub(super) fn current_special(mv: &MovementState, jump_arc: bool) -> Option bool { matches!(id, 2 | 8..=10 | 14..=36 | 46..=49 | 51..=90 | 105..=113 | 117..=118 | 122..=138 | 185..=186 | 195) } -/// The **COMBAT** membership (wow-re `0x5fcc10`) — the set whose airborne test can route to the mask +/// The **COMBAT** membership (`0x5fcc10`) — the set whose airborne test can route to the mask /// (a mid-jump swing). Byte-decoded memberships: **17 ∈**, and **66/68/80 ∉** (the emotes never mask /// on airborne alone). Every swing id (16–19/85/87/88/117) is in it; no emote id is — and no CAST id /// (32/33/51–54) either, which is why a jump-in-place cast routes FULL-BODY and replaces the hang @@ -1074,7 +1057,7 @@ fn is_combat(id: u16) -> bool { matches!(id, 10 | 16..=24 | 30 | 36 | 57..=59 | 85..=88 | 95 | 117 | 118) } -/// The **forced-full-body** carve-outs (wow-re §3): Death-class `{1,6,131,132}` (`0x5fda90`) and the +/// The **forced-full-body** carve-outs: Death-class `{1,6,131,132}` (`0x5fda90`) and the /// sit-transition ids `{57,58,118}` (`0x5fec60`) route to bone 0 regardless of state. Kept faithful /// though benilla never feeds these through the one-shot path. fn is_forced_full_body(id: u16) -> bool { @@ -1082,7 +1065,7 @@ fn is_forced_full_body(id: u16) -> bool { } /// Route a requested one-shot (swing/emote id) by the unit's **live state**, per play — the client's -/// `esi` decision `0x5fe6c8..0x5fe74d` (decision 0087, wow-re §3/§5): masked onto the SpineLow overlay +/// `esi` decision `0x5fe6c8..0x5fe74d` (decision 0087): masked onto the SpineLow overlay /// when the lower body is committed — **moving/turning/swimming** (`[9e8] & 0x20003f`), a **non-Stand /// stand-state** (seated/sleep/kneel/chair, `standState ≠ 0`), or a **combat** id while **airborne** /// (`activeCMovement+0x40 & 0x2000`); **full-body** on bone 0 when standing idle (none of those). The @@ -1102,14 +1085,14 @@ pub(super) fn route_oneshot(id: u16, flags: u32, stand_state: u8) -> OneShotRout } } -/// `AnimationData` ids whose playback rate the client scales by movement speed (wow-5875-re `0x5fee80`): +/// `AnimationData` ids whose playback rate the client scales by movement speed (`0x5fee80`): /// the locomotion gaits. An id outside this set plays at rate 1×. The **same** table is the /// LOCOMOTION membership the transplant predicates key on ([`is_locomotion`]). const RATE_SCALED: &[u16] = &[4, 5, 11, 12, 13, 37, 38, 39, 42, 43, 44, 45, 135, 143, 187]; /// The client's **LOCOMOTION** membership (`0x5fee80` — the very table the rate scaler uses): /// what the transplant predicates test as "the clip being *requested* is a base locomotion clip" -/// (`0x5feae0`/`0x5fe912`, wow-re `oneshot-lifecycle.md` §3a). Note Fall(40) and SwimIdle(41) are +/// (`0x5feae0`/`0x5fe912`). Note Fall(40) and SwimIdle(41) are /// **not** members — a FALLINGFAR latch mid-cast replaces the cast on bone 0 rather than /// transplanting it, exactly as the bytes order it. pub(super) fn is_locomotion(id: u16) -> bool { @@ -1136,8 +1119,7 @@ pub(super) fn gait_is_locomotion(state: &MovementState, walk_speed: f32) -> bool } /// The playback rate for a clip given the unit's live speed and its rendered model scale — the -/// client's `0x5fe2f0` divide, VERIFIED byte-for-byte and §5 cross-checked (wow-5875-re -/// `anim-rate-divisor.md`, the canonical note; `0x5fe4be..0x5fe550`): +/// client's `0x5fe2f0` divide (`0x5fe4be..0x5fe550`): /// /// ```text /// 5fe508 call 0x711a20 ; DIVISOR = ‖row0(M+0xbc)‖ · M2Sequence[reqId].moveSpeed @@ -1162,7 +1144,7 @@ pub(super) fn gait_is_locomotion(state: &MovementState, walk_speed: f32) -> bool /// folded the DBC scale in — `entities::attach`), and `+0x9c` is a mount's own /// `CreatureDisplayInfo` column, composed under the rider's. Our unit transform and mount-child /// transform carry exactly those, so `transform.scale.x × host` reproduces the product (decision -/// 0910). Two exactness notes from the cross-check: +/// 0910). Two exactness notes: /// /// - The client takes the **L2 length of row 0** of the model's world matrix, not an `abs()`. For a /// *uniform* scale — which every unit of ours is (`Vec3::splat`) — the two are identical at any @@ -1225,7 +1207,7 @@ pub(super) fn unify( modes: Option<&crate::net::UnitMoveModes>, ) -> MovementState { // The flying-spline fact is read through to the live `Spline` on EVERY leg — the client's - // selector reads the active CMovement's spline flags at select time (RF-0057 `0x5fd19c`), + // selector reads the active CMovement's spline flags at select time (`0x5fd19c`), // so a self ride (MovementState leg) and a remote taxi (spline leg) both fly. let flying = spline.is_some_and(|s| !s.grounded); if let Some(m) = movement { diff --git a/crates/benilla-app/src/creature_anim/select/tests.rs b/crates/benilla-app/src/creature_anim/select/tests.rs index 0b88eab4..e0179fc5 100644 --- a/crates/benilla-app/src/creature_anim/select/tests.rs +++ b/crates/benilla-app/src/creature_anim/select/tests.rs @@ -126,9 +126,9 @@ fn backward_is_walkbackwards() { #[test] fn swimming_back_forward_strafe_and_idle() { - // The RF-0057 swim row (`0x5fd137`…): fwd/back/turn/strafe → 42/45/41/43-44. SwimLeft 43 / + // The swim row (`0x5fd137`…): fwd/back/turn/strafe → 42/45/41/43-44. SwimLeft 43 / // SwimRight 44 byte-read from AnimationData.dbc; the combined-bit cascade below is the - // VERIFIED TU-E order (wow-re `swim-mechanism.md`). + // `0x5fd100` order. let back = MovementState { speed: 4.0, flags: move_flags::SWIMMING | move_flags::BACKWARD, @@ -153,7 +153,7 @@ fn swimming_back_forward_strafe_and_idle() { ..Default::default() }; assert_eq!(gait_candidates(&right, 2.5, None, None), &[44, 42, 41, 0]); - // The VERIFIED `0x5fd100` cascade (TU-E): TURN > STRAFE > BACKWARD > FORWARD. A strafe + // The `0x5fd100` cascade: TURN > STRAFE > BACKWARD > FORWARD. A strafe // diagonal plays the side-stroke — strafe outranks both fwd and back… let diag = MovementState { speed: 4.0, @@ -198,9 +198,9 @@ fn swimming_back_forward_strafe_and_idle() { #[test] fn airborne_splits_jump_fall_and_gait_freeze() { - // The three-way airborne split (wow-re land-anim-height-gate): a jump arc plays the 37/38 - // bracket; FALLINGFAR latched → the Fall(40) loop (whatever the arc's origin); a step-off - // fall below the latch is NO special — the gait freezes through it (keep-current). + // The three-way airborne split: a jump arc plays the 37/38 bracket; FALLINGFAR latched → the + // Fall(40) loop (`0x602c40`, whatever the arc's origin); a step-off fall below the latch is NO + // special — the gait freezes through it (keep-current). let s = MovementState { flags: move_flags::FORWARD | move_flags::FALLING, ..Default::default() @@ -234,14 +234,14 @@ fn a_pose_yields_to_movement_but_a_jump_landing_plays_out() { #[test] fn jump_sequence_ids() { - // The §5/asset-verified sequence: JumpStart 37 → Jump 38 → the landing pick (below). + // The asset-verified sequence: JumpStart 37 (`0x60e480`) → Jump 38 → the landing pick (below). assert_eq!(Special::Jump.enter(), 37); assert_eq!(Special::Jump.loop_id(), 38); } #[test] fn jump_land_pick_is_the_0x602c60_rule() { - // The land dispatcher `0x602c60` (wow-re rf57b §2): stopped → JumpEnd 39; moving forward or + // The land dispatcher `0x602c60`: stopped → JumpEnd 39; moving forward or // strafing → JumpLandRun 187; backpedaling or walk-mode → NO landing clip (the recompute drops // straight into the gait — a jump-then-hold-S backpedals the instant it touches down, never // flashing the forward-run footplant); swimming → no clip (the swim gait takes over). @@ -388,7 +388,7 @@ fn reconcile_priority_is_stow_over_draw() { #[test] fn reconcile_mounted_is_a_persistent_draw_block() { - // Mounted forces stow on every recompute (decision 0441, wow-re sheath-policy §3): it beats + // Mounted forces stow on every recompute (decision 0441, `0x5fdfd9`): it beats // the engaged draw, the &0x20 draw, and the remote server-byte pull-through alike — a // volunteered drawn byte can never re-arm a rider. assert_eq!(reconcile_sheath(1, 0, 0, true, true, 1, true), Some(0)); @@ -466,7 +466,7 @@ fn a_degenerate_model_scale_falls_through_to_unit_rate() { } /// The SIGN of `moveSpeed` is load-bearing, and the `abs()` belongs to the scale ALONE (decision -/// 0912, §5-verified). A backwards gait is authored negative — `RidingKodo.m2` seq 14 is +/// 0912). A backwards gait is authored negative — `RidingKodo.m2` seq 14 is /// WalkBackwards at **−2.5**, byte-read here with `benilla-extract m2seq` — and Guard A is a strict /// `divisor > 0`, so the reference leaves that clip at a flat 1×. A model authoring NO /// WalkBackwards falls back to forward Walk (+2.5) and *is* speed-scaled. Pinned because @@ -514,13 +514,13 @@ fn ranged_load_idle_selects_by_weapon_and_ranks_below_ready() { ))); } -/// The drawn ranged idle is claimed by **two tests and no others** ([`ranged_idle_gate`], -/// byte-verified — wow-re `shooter-stop-law.md` §J6 claim 1): `0x5fd460` reads the ranged sheath -/// (`cmp [+0xd40],2`) and the local auto-repeat bit (`test ah,0x2` = `0x200`). The any-caster -/// weapon-visual hold `0x400` is **never tested in that function**; it appears only in -/// `0x5fc3f0`'s Hold self-loop gates, which are never reached for a bow id. Admitting it here is -/// the defect the director reported on 2026-08-05 ("they keep aiming like they are going to shoot -/// at something"): one Serpent Sting sets `0x400`, and no volley end ever clears it. +/// The drawn ranged idle is claimed by **two tests and no others** ([`ranged_idle_gate`]): +/// `0x5fd460` reads the ranged sheath (`cmp [+0xd40],2`) and the local auto-repeat bit +/// (`test ah,0x2` = `0x200`). The any-caster weapon-visual hold `0x400` is **never tested in that +/// function**; it appears only in `0x5fc3f0`'s Hold self-loop gates, which are never reached for a +/// bow id. Admitting it here is the defect the director reported on 2026-08-05 ("they keep aiming +/// like they are going to shoot at something"): one Serpent Sting sets `0x400`, and no volley end +/// ever clears it. #[test] fn the_ranged_idle_is_entered_by_the_auto_repeat_bit_and_the_ranged_sheath_alone() { // The whole claim of `0x5fd460`: the local `0x200`, with the ranged sheath. @@ -540,8 +540,8 @@ fn the_ranged_idle_is_entered_by_the_auto_repeat_bit_and_the_ranged_sheath_alone /// in the cycle authored as a LOOP and therefore the one that can sit between shots. /// /// This replaces 0994's `is_ranged_fire` law test, which asserted the opposite mechanism: that a -/// fire clip's completion must NOT recompute the base. wow-re's §5 refuted the absence proof it -/// rested on — the completion dispatcher has a second, deferred fire site — so the fire clips +/// fire clip's completion must NOT recompute the base. The absence proof it rested on is false — +/// the completion dispatcher has a second, deferred fire site (`0x7075af`) — so the fire clips /// recompute like every other one-shot and the promotion below is what holds the pose instead. #[test] fn each_ranged_load_promotes_to_its_weapon_familys_hold() { @@ -680,8 +680,8 @@ fn route_land_row8_picks_land_clip_from_touchdown_input() { #[test] fn route_classifier_memberships_are_the_decoded_bytes() { - // The load-bearing byte-decoded memberships (wow-re §3): 17 ∈ COMBAT, 66/68/80 ∉ COMBAT; - // 17/66/68/80 ∈ CLASS_A. All swing ids are COMBAT; no emote id is. + // The load-bearing byte-decoded memberships (`0x5fcc10`, `0x5fed90`): 17 ∈ COMBAT, 66/68/80 ∉ + // COMBAT; 17/66/68/80 ∈ CLASS_A. All swing ids are COMBAT; no emote id is. assert!(is_combat(17)); for id in [66, 68, 80] { assert!(!is_combat(id), "emote {id} must not be COMBAT"); @@ -789,7 +789,7 @@ fn base_arm_head_force_is_the_combat_carveout() { #[test] fn wound_id_by_severity_then_engagement() { - // Decision 0111 §5.3: crit outranks engagement; engagement decides 9 vs 8. + // Decision 0111: crit outranks engagement; engagement decides 9 vs 8. assert_eq!(wound_anim(0x2 | 0x80, false), 10); assert_eq!(wound_anim(0x2 | 0x80, true), 10); assert_eq!(wound_anim(0x2, true), 9); @@ -864,7 +864,7 @@ fn swing_ids_cover_both_hands() { #[test] fn a_flying_spline_is_fly_before_backward_and_speed() { - // RF-0057 `0x5fd19c`: the fly branch sits between the swim block and the backward test — + // `0x5fd19c`: the fly branch sits between the swim block and the backward test — // a 32 yd/s taxi plays Fly 135, never Sprint 143 (the ≥11 branch) and never WalkBackwards. let fly = MovementState { flying: true, @@ -971,7 +971,7 @@ fn the_granted_modes_fold_into_the_flags_word_on_every_leg_but_our_own() { ); } -/// The prowl creep outranks the WHOLE speed tail (RF-0057 `0x5fd1d3` precedes `0x5fd202`): a +/// The prowl creep outranks the WHOLE speed tail (`0x5fd1d3` precedes `0x5fd202`): a /// stealthed unit plays 119 at a dead crawl and at sprint speed alike — never Walk, Run or Sprint. #[test] fn the_prowl_creeps_at_every_speed() { diff --git a/crates/benilla-app/src/creature_anim/sheath.rs b/crates/benilla-app/src/creature_anim/sheath.rs index 1161911f..def1aa47 100644 --- a/crates/benilla-app/src/creature_anim/sheath.rs +++ b/crates/benilla-app/src/creature_anim/sheath.rs @@ -97,9 +97,8 @@ const SHEATH_OVERLAY_WEIGHT: f32 = 8.0; /// sender), the attack-start auto-draw, the stand-state stow rider — and `drive_animations` is /// the sole executor: the idempotency refusal, the commit to the client-side cache, the /// `CMSG_SETSHEATHED` volunteer for the local player (`bFireEvent = 1`), and the ceremony-vs-snap -/// visual all live in that one place. Byte-verified across all 24 client call sites (wow-re -/// `sheath-policy.md`): **only the manual `ToggleSheath` passes `bInstant = 0`** — every reactive -/// trigger and the server-field apply snap. +/// visual all live in that one place. Across all 24 client call sites, **only the manual +/// `ToggleSheath` passes `bInstant = 0`** — every reactive trigger and the server-field apply snap. #[derive(Message, Clone, Copy)] pub(crate) struct SheathRequest { pub(crate) entity: Entity, @@ -111,8 +110,8 @@ pub(crate) struct SheathRequest { /// The manual `ToggleSheath` **cycle** — the state a Z press asks for next, or `None` where the /// ref makes no `SetSheatheState` call at all. Byte-read off `0x5eb642`–`0x5eb6a8` (the four -/// ToggleSheath call sites tabulated in wow-re `sheath-policy.md` §1; the dispatch is a -/// `sub 0; je` / `dec; je` / `dec; jne` walk over the committed state `[unit+0xd40]`): +/// ToggleSheath call sites; the dispatch is a `sub 0; je` / `dec; je` / `dec; jne` walk over the +/// committed state `[unit+0xd40]`): /// /// ```text /// CUR 0 mainhand or offhand worn -> 1 melee (0x5eb6a0: push 1, 0, 1) diff --git a/crates/benilla-app/src/creature_anim/spell_visual.rs b/crates/benilla-app/src/creature_anim/spell_visual.rs index ac2e4933..9da64e24 100644 --- a/crates/benilla-app/src/creature_anim/spell_visual.rs +++ b/crates/benilla-app/src/creature_anim/spell_visual.rs @@ -32,8 +32,8 @@ const ERROR_CUBE: &str = "Spells\\ErrorCube.mdx"; /// The reserved [`SpellKitFx`] reap key for the lootable-corpse effect — NOT a spell id (no 5875 /// spell id is anywhere near `u32::MAX`; the client keys these nodes by attach tag + node flag -/// instead of a spell, wow-re `loot-corpse-effect.md`). One key suffices: a unit carries at most -/// one loot effect (the client's flag8 dedup), which [`arm_loot_fx`]'s edge cache guarantees. +/// instead of a spell — `0x61fa10`). One key suffices: a unit carries at most one loot effect (the +/// client's flag8 dedup), which [`arm_loot_fx`]'s edge cache guarantees. const LOOT_FX_KEY: u32 = u32::MAX; /// The M2 attachment the three engine-spawned effects we ship hang from — the loot sparkle, the @@ -41,25 +41,25 @@ const LOOT_FX_KEY: u32 = u32::MAX; /// fallback, so a model lacking the point lands on the unit base — exactly the reference's /// root fallback (`0x61fb4f`–`0x61fb5f`: `HasAttachment(tag)` on `[unit+0xd8]`, else `0x13`). /// -/// **It is a table value, not an engine-wide constant** (wow-re `mount-composition.md` Q4b.1, §5 -/// 2026-08-03 — the 14-entry `DAT_0080c968`, dumped and joined to the `0x8617b8` name table). It -/// happens to be `0x13` for indices **4** Loot Art, **5** Unit Level Up and **6** Mount Poof — -/// our three — and for the PetLoyalty/Meeting-Stone/Reputation rows; it is `0x11` for the two -/// breath effects and Inebriated Bubbles, and the sentinel `0x25` (spawn refused outright) for -/// the two footstep sprays. A fourth consumer must read the table row, not reuse this. +/// **It is a table value, not an engine-wide constant** (the 14-entry `DAT_0080c968`, dumped and +/// joined to the `0x8617b8` name table). It happens to be `0x13` for indices **4** Loot Art, **5** +/// Unit Level Up and **6** Mount Poof — our three — and for the PetLoyalty/Meeting-Stone/Reputation +/// rows; it is `0x11` for the two breath effects and Inebriated Bubbles, and the sentinel `0x25` +/// (spawn refused outright) for the two footstep sprays. A fourth consumer must read the table row, +/// not reuse this. const HARDCODED_FX_ATTACH: u16 = 0x13; /// The engine-spawned level-up effect's baked lookup name (the client's string `0x8618e0`, -/// matched by the boot name-resolve `0x61f5b0`; wow-re `levelup-ding.md` — decision 0305). +/// matched by the boot name-resolve `0x61f5b0`; decision 0305). const LEVEL_UP_EFFECT: &str = "HARDCODED Unit Level Up"; /// The mount transition's cloud — hardcoded index **6**, shipped as `SpellVisualEffectName` row -/// 1185 → `Spells\DruidMorph_Impact_Base.mdx`, i.e. literally the druid-morph puff (wow-re -/// `mount-composition.md` Q4b, decision 0927). +/// 1185 → `Spells\DruidMorph_Impact_Base.mdx`, i.e. literally the druid-morph puff (`0x5ffa50` +/// pushes index 6 to `0x61fae0`; decision 0927). const MOUNT_POOF_EFFECT: &str = "HARDCODED Mount Poof"; /// The meeting-stone join — hardcoded index **0xc** (`0x861808`), spawned on the local player by -/// the `SMSG 0x295` status-1 arm with the same `Effect_C` shape as the mount poof (wow-re -/// `meeting-stone-status.md` §8; decision 1974). +/// the `SMSG 0x295` status-1 arm `0x4ca2c7` with the same `Effect_C` shape as the mount poof +/// (decision 1974). const MEETING_STONE_JOIN_EFFECT: &str = "HARDCODED Meeting Stone Join"; /// The meeting-stone join visual for `entity`, or `None` without client data or the row — @@ -102,17 +102,17 @@ pub(crate) enum SpellKitSound { /// unit's tracked hold loop, if any (the client kills the effect's sound at `0x614150`). StopHold { entity: Entity }, /// Ring this kit at a bare **world point**, with no owning unit — the kit-sound leg's - /// `extra`-override arm (wow-re `kit-sound-leg.md`: "position = `extra` if non-NULL else the - /// unit's own position", `60f49e`–`60f4bc`). Its one caller is the missile's ground arrival, - /// where `0x61d870` passes the landing point: the bomb's boom belongs where it landed, not at - /// the thrower. Always a one-shot — a LOOPING sound here would need a tracked node with an - /// owner, and none of the six shipped ground-arrival kits carries one (census below). + /// `extra`-override arm (position = `extra` if non-NULL else the unit's own position, + /// `60f49e`–`60f4bc`). Its one caller is the missile's ground arrival, where `0x61d870` passes + /// the landing point: the bomb's boom belongs where it landed, not at the thrower. Always a + /// one-shot — a LOOPING sound here would need a tracked node with an owner, and none of the six + /// shipped ground-arrival kits carries one (census below). PlayAt { pos: Vec3, kit_sound: u32 }, /// Stop exactly `kit_sound`'s channels on this unit — the aura-drop reap of a LOOPING - /// **state**-kit sound (wow-re `kit-sound-leg.md`: a looping kit sound rides a tracked, - /// spell-id-tagged CEffect on the unit's list, and `0x614150` stops it with a 0.15 s fade - /// when the aura leaves). Kit-scoped so an unrelated hold loop on the same unit survives; - /// a one-shot kit is never tracked, so for it this is a no-op. + /// **state**-kit sound (a looping kit sound rides a tracked, spell-id-tagged CEffect on the + /// unit's list, and `0x614150` stops it with a 0.15 s fade when the aura leaves). Kit-scoped so + /// an unrelated hold loop on the same unit survives; a one-shot kit is never tracked, so for it + /// this is a no-op. StopKit { entity: Entity, kit_sound: u32 }, } @@ -121,13 +121,12 @@ pub(crate) enum SpellKitSound { /// [`SpellKitSound`]'s two play arms: the reference reaches both through the same leg of the same /// one-time node arm pass, one field apart. /// -/// **Exactly once per `PlaySpellVisualKit` call, and always** (wow-re -/// `spell/scratch/camera-shake-producers.md`, §5 2026-09-02). The primary site is `0x620e11`, -/// inside the first-created effect node's one-time arm pass `0x620be0`, gated on that node's -/// flags snapshot carrying bit `0x10` — which `0x60edf0` sets iff `kit+0x38 != 0` and every later -/// spawner clears via `(flags & ~0x11) | 0x400`, so exactly the FIRST node carries it. `0x60f4e6` -/// in the kit tail is the **no-node fallback**, gated on "nothing was created" rather than on a -/// flag. There is no stage gate and no cancel path: a shake outlives its source and plays out. +/// **Exactly once per `PlaySpellVisualKit` call, and always**. The primary site is `0x620e11`, +/// inside the first-created effect node's one-time arm pass `0x620be0`, gated on that node's flags +/// snapshot carrying bit `0x10` — which `0x60edf0` sets iff `kit+0x38 != 0` and every later spawner +/// clears via `(flags & ~0x11) | 0x400`, so exactly the FIRST node carries it. `0x60f4e6` in the +/// kit tail is the **no-node fallback**, gated on "nothing was created" rather than on a flag. +/// There is no stage gate and no cancel path: a shake outlives its source and plays out. #[derive(Message, Clone, Copy, Debug)] pub(crate) enum SpellKitShake { /// Shake at the playing unit's own world position — the ordinary kit play. @@ -139,12 +138,11 @@ pub(crate) enum SpellKitShake { } /// A persistent effect instance's **lifetime class** — which owner's reap can kill it. -/// Byte-verified (wow-re `state-kit-aura-lifecycle.md`, §5 2026-07-14): the client's reap walk -/// `0x614150(spellId, force)` discriminates by node flags — every SPELL_GO / SPELL_FAILED reap -/// passes `force=0`, which **spares** stage-2 nodes (flag `0x1000`); only the aura-remove path -/// (`0x612320 → 0x5ff290`) passes `force=1` and takes them. So a cast's GO releasing its precast -/// never sweeps the same spell's aura-state models (re-eat while the food buff holds: the GO -/// must not take the bread with it). This enum is that discriminator, named. +/// The client's reap walk `0x614150(spellId, force)` discriminates by node flags — every SPELL_GO / +/// SPELL_FAILED reap passes `force=0`, which **spares** stage-2 nodes (flag `0x1000`); only the +/// aura-remove path (`0x612320 → 0x5ff290`) passes `force=1` and takes them. So a cast's GO +/// releasing its precast never sweeps the same spell's aura-state models (re-eat while the food +/// buff holds: the GO must not take the bread with it). This enum is that discriminator, named. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum FxClass { /// Cast-lifecycle instances: precast/channel holds (and every self-terminating flash, whose @@ -157,13 +155,13 @@ pub(crate) enum FxClass { /// Which of the reference's five kit **stages** an instance's model runs its *animation lifecycle* /// as — the discriminator behind `PlaySpellVisualKit`'s per-stage completion-callback table -/// (`0x60edf0`'s jump table `0x60f4f8`; wow-re `ceffect-anim-lifecycle.md` §2, §5 trio 2026-08-02). +/// (`0x60edf0`'s jump table `0x60f4f8`). /// /// Distinct from [`FxClass`], which answers a different question — *whose reap can kill this* (the /// `0x614150` force census). The two are not derivable from each other: a channel kit and an /// aura-state kit are both stage 2 but reaped by different owners, while a precast and a channel /// are both [`FxClass::Hold`] and run *different* animation lifecycles. So the stage rides its own -/// field, set at each writer from the caller table (`spell-visual-apply.md` §1.6). +/// field, set at each writer from the stage its caller passes to `0x60edf0`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum FxStage { /// Stages 0/1 (kit push, cast release, impact) — callback `0x5fbf50`: destroy at the first @@ -186,9 +184,8 @@ pub(crate) enum FxStage { /// The id is not decoration. It is half the reference's **same-slot replace** key: every /// `CEffect::AddEffect` (`0x61fdd0`) first calls `0x6208e0(owner, rec, tag)`, which walks the /// owner's `+0xb4` list and **destroys** every node carrying the same record id at the same tag -/// (wow-re `kit30-effect-slot.md` §4, VERIFIED `[0x6208e0, 0x62092c]`; decision 2057). Keying on -/// the path instead would be a different rule — two `SpellVisualEffectName` rows may name one -/// `.mdx`, and the reference lets those coexist. +/// (`[0x6208e0, 0x62092c]`; decision 2057). Keying on the path instead would be a different rule — +/// two `SpellVisualEffectName` rows may name one `.mdx`, and the reference lets those coexist. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct FxSlot { /// The M2 attachment id to hang from ([`benilla_formats::KIT_SLOT_TAGS`]), or @@ -202,9 +199,9 @@ pub(crate) struct FxSlot { } /// A spell-visual kit's attach-point **effect model** edge (kit fields 3–11 → -/// `SpellVisualEffectName` `.mdx` — decision 0099 phase 3; slot loop byte-pinned in wow-re -/// `spell-visual-apply.md` §1.3). Written here, consumed by `crate::entities::spell_fx` (the -/// bone-rider home). One message type so the consumer sees edges **in emission order** — a GO's +/// `SpellVisualEffectName` `.mdx` — decision 0099 phase 3; the slot loop's tag pushes +/// `0x60f00d`–`0x60f309`). Written here, consumed by `crate::entities::spell_fx` (the bone-rider +/// home). One message type so the consumer sees edges **in emission order** — a GO's /// reap-then-begin never races. #[derive(Message, Clone)] pub(crate) enum SpellKitFx { @@ -283,12 +280,12 @@ pub(crate) struct MissileSpawn { /// point, exactly ONE projectile flies at it instead (the client's `0x6e8a50` empty-hit-array /// arm — `0x6e8aa2 test al,0x60; setne cl` latches "SOURCE or DEST location present", /// `0x6e8bd4` then calls the spawner once, `0x60a5b6`–`0x60a5c9` copies `targets+0x3c` into - /// the aim point and `0x60a5ec` forces the owning unit slot to −1; wow-re - /// `spell-go-dest-effect.md` §3). This is what a pure ground cast — Flare, a bomb thrown at - /// empty dirt — actually shows travelling. Its arrival is [`super::CastEventKind::GroundImpact`], - /// never the unit hand-off. Named divergence: the reference latches on `flags & 0x60` and then - /// reads the DEST vector regardless, so a SOURCE-only cast aims at whatever the zero-initialised - /// DEST slot holds; we carry only a real DEST (`flags & 0x40`) and launch nothing without one. + /// the aim point and `0x60a5ec` forces the owning unit slot to −1). This is what a pure ground + /// cast — Flare, a bomb thrown at empty dirt — actually shows travelling. Its arrival is + /// [`super::CastEventKind::GroundImpact`], never the unit hand-off. Named divergence: the + /// reference latches on `flags & 0x60` and then reads the DEST vector regardless, so a + /// SOURCE-only cast aims at whatever the zero-initialised DEST slot holds; we carry only a real + /// DEST (`flags & 0x40`) and launch nothing without one. pub(crate) ground_aim: Option, /// The caster's ranged-weapon fallback `SpellVisual` id, resolved at GO time — rides the /// flight and returns in [`super::CastEventKind::Impact`] so a basic shot's impact kit @@ -330,7 +327,7 @@ pub(super) fn load_spell_visuals(mut commands: Commands, assets: Option { displays: Option>, @@ -485,10 +482,10 @@ fn resolve_kit_traced( /// The in-flight cast's **strike sound** for the `$TRD` anim event (`0x62faa0`): the handler /// resolves the spell's visual to its 16-dword `SpellVisual.dbc` row (`0x60d450` → /// `DAT_00c0d738[visualId]`) and plays that row's **dword 14** (`[row+0x38]`) positioned at the -/// unit — wow-re `sound/scratch/gather-sound-anim-events.md`, decision 0562. Mining's visual 93 -/// carries 1143 "Mining Impact" here (the pick clang — client-side complete, no server state); -/// the smithing crafts' 395 the same hammer; Herb's 91 carries 1142 but its anim never fires -/// `$TRD`. No ranged-weapon fallback (`$TRD` rides work/craft anims, never a ranged shot's). +/// unit (decision 0562). Mining's visual 93 carries 1143 "Mining Impact" here (the pick clang — +/// client-side complete, no server state); the smithing crafts' 395 the same hammer; Herb's 91 +/// carries 1142 but its anim never fires `$TRD`. No ranged-weapon fallback (`$TRD` rides work/craft +/// anims, never a ranged shot's). pub(crate) fn held_strike_sound( spells: &crate::ui_action::Spells, visuals: &SpellVisualCatalog, @@ -568,10 +565,10 @@ struct KitOut<'a, 'w1, 'w2, 'w3, 'w4, 'w5, 'w6, 'w7, 'w8> { /// - `effects`: whether the models spawn here at all. `false` for the impact hand-off's STATE /// stage — its models belong to the aura's life and [`arm_aura_state_fx`] owns them. /// - `sound`: whether the kit's sound rings — **ungated and positional at the unit, every -/// stage** (wow-re `kit-sound-leg.md`, decision 0852: `0x60edf0`'s sound leg carries no self -/// test; the old "state sound is self-only" reading was `0x5ff43e`'s gate on a *different* -/// aura-apply cue, and `0x5fa6d0` appears nowhere in the kit play). The flag exists for -/// plays that deliberately hand the sound to another owner (none today). +/// stage** (decision 0852: `0x60edf0`'s sound leg carries no self test; the old "state sound is +/// self-only" reading was `0x5ff43e`'s gate on a *different* aura-apply cue, and `0x5fa6d0` +/// appears nowhere in the kit play). The flag exists for plays that deliberately hand the sound +/// to another owner (none today). #[derive(Clone, Copy)] struct KitPlay { persistent: bool, @@ -585,10 +582,9 @@ struct KitPlay { /// /// Stage 2 is **foreclosed from the play site**: `0x60f387 jne` diverts it around the sole /// `0x60f3c5 call 0x5fe2f0` that ever hands a kit's `AnimID` to the animation primitive, and - /// its leg leaves the block with `jmp 0x60f3ca` (VERIFIED, wow-re - /// `state-kit-anim-and-stun-pose.md` §1). So a state kit neither plays its anim nor reaches - /// the `[8,10]` wound test (the half decision 2063 already had): the id is only ever the - /// right-hand side of a comparison against what the unit is already playing, and a mismatch + /// its leg leaves the block with `jmp 0x60f3ca`. So a state kit neither plays its anim nor + /// reaches the `[8,10]` wound test (the half decision 2063 already had): the id is only ever + /// the right-hand side of a comparison against what the unit is already playing, and a mismatch /// spends itself on a base recompute ([`BaseAnimRecompute`]). /// /// The other exclusion at `0x60f3a0` — stage 4 with a zero cast time — never reaches this @@ -698,13 +694,12 @@ struct ImpactPlay { /// kit plays its anim + sound only: its **effect models live for the aura's life**, owned by /// [`arm_aura_state_fx`]'s slot watcher (a single instance, begun when the aura lands in the /// slots and reaped when it leaves — the same packet burst as the GO, so there is no visible -/// gap). The state sound rings **ungated and positional** (wow-re `kit-sound-leg.md`, decision -/// 0852: `0x60edf0`'s sound leg has no self test — the old self-only reading belonged to a -/// different aura-apply cue); the same-frame duplicate against the aura watcher's ADD-edge play -/// collapses in the sound router. Named approximation: the client's small stage-2 gate -/// `0x61dc20` (37 B, content unpinned) is not modeled. `weapon_visual` = the CASTER's ranged -/// fallback visual (already resolved — the caster may be gone by a missile arrival), through -/// which a basic shot's impact kit resolves. +/// gap). The state sound rings **ungated and positional** (decision 0852: `0x60edf0`'s sound leg +/// has no self test — the old self-only reading belonged to a different aura-apply cue); the +/// same-frame duplicate against the aura watcher's ADD-edge play collapses in the sound router. +/// Named approximation: the client's small stage-2 gate `0x61dc20` (37 B, content unpinned) is not +/// modeled. `weapon_visual` = the CASTER's ranged fallback visual (already resolved — the caster +/// may be gone by a missile arrival), through which a basic shot's impact kit resolves. /// /// [`ImpactPlay::flinch`] is the **spell impact body twitch** (decision 2058): the client lays /// the victim's wound flinch at `severity = 0` beside the impact kit — the instant-hit loop @@ -898,14 +893,12 @@ pub(super) fn route_cast_visuals( |s| s.precast, || weapon_src.caster(ev.entity), ) { - // The `0x400` weapon-visual hold, ANY caster (wow-re - // `ranged-sheath-exempt-autorepeat.md` §Q4): a RANGED spell's visual play - // sets it (`0x60d020`, sole caller inside PlaySpellVisual); any other - // visual play clears it (the stale-visual cleanup `0x6ec39e` — re-set only - // when the new visual is ranged). It is what keeps a REMOTE shooter (an NPC - // archer, another hunter) in the drawn Load/Hold idle between shots — the - // local `0x200` ([`super::AutoRepeatArmed`]) never exists off the local - // cast-send. + // The `0x400` weapon-visual hold, ANY caster: a RANGED spell's visual play sets + // it (`0x60d020`, sole caller inside PlaySpellVisual); any other visual play + // clears it (the stale-visual cleanup `0x6ec39e` — re-set only when the new + // visual is ranged). It is what keeps a REMOTE shooter (an NPC archer, another + // hunter) in the drawn Load/Hold idle between shots — the local `0x200` + // ([`super::AutoRepeatArmed`]) never exists off the local cast-send. let ranged = spells .catalog .get(ev.spell_id) @@ -970,9 +963,9 @@ pub(super) fn route_cast_visuals( }); // A ranged-slot spell's GO **re-draws ranged before its cast-kit play** — the // kit-play path's internal snap (`0x60f34c`, gate `Attributes & 0x2`, stages - // {0, 4} — wow-re `ranged-sheath-exempt-autorepeat.md` Q3): SPELL_GO makes no - // direct SetSheatheState call, but every auto-repeat shot re-snaps state 2 - // through its play. This is what re-draws a bow an emote lowered mid-volley. + // {0, 4}): SPELL_GO makes no direct SetSheatheState call, but every auto-repeat + // shot re-snaps state 2 through its play. This is what re-draws a bow an emote + // lowered mid-volley. if spells .catalog .get(ev.spell_id) @@ -986,8 +979,8 @@ pub(super) fn route_cast_visuals( } // The cast kit (the release flash) — a discrete play, effects self-terminating // after their model's own clip span (the client's stage-0/1 completion callback). - // For a basic shot this is the fire clip itself (the ranged fallback: Throw's - // AttackThrown, Auto Shot's AttackBow — wow-re `throw-ranged-attack-anim.md`). + // For a basic shot this is the fire clip itself (the ranged fallback `0x60d450`: + // Throw's AttackThrown, Auto Shot's AttackBow). if let Some(kit) = resolve_kit_traced( "cast", ev.entity, @@ -1106,7 +1099,7 @@ pub(super) fn route_cast_visuals( // gate `field 6 == 0` (no missile owns the arrival) ∧ `field 12 ≠ 0`. NOT gated on the // hit list (a pure ground cast's lists are empty and the burst still plays — the // captured Flamestrike shape); fired here at the GO, never waiting on the dynobj - // create (wow-re trap #6: the burst precedes the object). + // create (the burst precedes the object). if let Some(dest) = go.dest { if let Some(stages) = stages { if stages.missile_gate == 0 && stages.area_effect != 0 { @@ -1297,17 +1290,16 @@ pub(super) fn route_cast_visuals( /// still plays the state kit as its short impact-time flash (the client's `0x61dc50` order — /// impact, then state), which simply overlaps the persistent instance this watcher owns. /// -/// The trigger and reap are byte-verified (wow-re `state-kit-aura-lifecycle.md`, §5 2026-07-14): -/// the aura watcher `0x604d00 → 0x6123f0 → 0x5ff350` reads `SpellVisual` field 4 and plays the -/// kit at stage 2 (`0x5ff4c2: push 2`); the remove path `0x612320 → 0x5ff290` reaps with -/// `0x614150(spellId, force=1)`. The ADD edge's kit sound rings **ungated and positional** (wow-re -/// `kit-sound-leg.md`, decision 0852 — its §5 corrected the earlier "SELF-gated" reading: the -/// `0x5fa6d0` gate at `0x5ff43e` covers only a separate spellRec-driven aura-apply cue, and both -/// branches fall through to the ungated kit play at `0x5ff4c6`); a looping kit sound is tracked -/// and stops with the aura ([`SpellKitSound::StopKit`]). The body anim is NOT code-suppressed -/// (`0x60edf0`'s tail plays `kit+0x08` unconditionally) — this watcher plays effects + sound, -/// with a **named residual** for a state kit carrying a real anim: that ADD-edge replay isn't -/// built (no live kit demonstrates it; build it from the verdict when one shows). +/// The trigger and reap: the aura watcher `0x604d00 → 0x6123f0 → 0x5ff350` reads `SpellVisual` +/// field 4 and plays the kit at stage 2 (`0x5ff4c2: push 2`); the remove path `0x612320 → 0x5ff290` +/// reaps with `0x614150(spellId, force=1)`. The ADD edge's kit sound rings **ungated and +/// positional** (decision 0852 — not SELF-gated: the `0x5fa6d0` gate at `0x5ff43e` covers only a +/// separate spellRec-driven aura-apply cue, and both branches fall through to the ungated kit play +/// at `0x5ff4c6`); a looping kit sound is tracked and stops with the aura +/// ([`SpellKitSound::StopKit`]). The body anim is NOT code-suppressed (`0x60edf0`'s tail plays +/// `kit+0x08` unconditionally) — this watcher plays effects + sound, with a **named residual** for +/// a state kit carrying a real anim: that ADD-edge replay isn't built (no live kit demonstrates it; +/// build it from the reference when one shows). /// /// `armed` tracks exactly the (unit, spell) pairs this watcher began, so the REMOVE edge never /// reaps another owner's persistent instances (a channel hold whose spell also rides an aura). @@ -1325,8 +1317,8 @@ pub(super) fn route_cast_visuals( /// visual is one proc-14 CharProc), so an effects-only test dropped it and the character showed /// nothing. /// The reference's one aura watch: `UNIT_FIELD_AURA` + `UNIT_FIELD_AURAFLAGS` as a single -/// registration — `0x604d00`, registered `604226 mov edx,0xa4` / `604221 push 0xd8` for class 3 -/// (wow-re `object-layer.md`): field offset `0xa4` = dword 41 past the 6 OBJECT fields = +/// registration — `0x604d00`, registered `604226 mov edx,0xa4` / `604221 push 0xd8` for class 3: +/// field offset `0xa4` = dword 41 past the 6 OBJECT fields = /// `FIELD_UNIT_AURA`, length `0xd8` bytes = 54 dwords = the 48 slot ids and the 6 packed flag words. const AURA_WATCH_BASE: u16 = benilla_protocol::field::FIELD_UNIT_AURA; const AURA_WATCH_SPAN: u16 = 54; @@ -1458,10 +1450,9 @@ pub(crate) fn arm_aura_state_fx( armed.retain(|e, _| units.contains(*e)); } -/// Arm/reap the **lootable sparkle** on both things that can wear it (wow-re -/// `loot-corpse-effect.md` + `corpse-decal-and-loot-sparkle.md` §Q2, both §5 cross-checked): -/// a DEAD unit carrying `UNIT_DYNFLAG_LOOTABLE`, **and a corpse object carrying -/// `CORPSE_FIELD_DYNAMIC_FLAGS` bit 0** (decision 1723 — a battleground body's insignia). Each +/// Arm/reap the **lootable sparkle** on both things that can wear it: a DEAD unit carrying +/// `UNIT_DYNFLAG_LOOTABLE`, **and a corpse object carrying `CORPSE_FIELD_DYNAMIC_FLAGS` bit 0** +/// (watcher `0x5d6de0`; decision 1723 — a battleground body's insignia). Each /// wears the `SpellVisualEffectName` row named /// `"HARDCODED Loot Art"` (5875: `Particles\LootFX.mdl` — a golden flare + three star-twinkle /// emitters; cadence/size/color/blend all authored in the asset, the client sets none of them) @@ -1517,10 +1508,10 @@ pub(super) fn arm_loot_fx( due.into_iter().filter_map(|e| units.get(e).ok()).collect() }; for (entity, store, net) in scan { - // The **corpse object**'s own sparkle (wow-re `corpse-decal-and-loot-sparkle.md` §Q2, §5 - // cross-checked): `0x5d6de0` watches `CORPSE_FIELD_DYNAMIC_FLAGS`, and bit 0 rising calls - // `0x5d6e30` → the same `"HARDCODED Loot Art"` row at the same attachment `0x13`. Same - // asset, same tag — so it shares this body rather than growing a second one. + // The **corpse object**'s own sparkle: `0x5d6de0` watches `CORPSE_FIELD_DYNAMIC_FLAGS`, and + // bit 0 rising calls `0x5d6e30` → the same `"HARDCODED Loot Art"` row at the same + // attachment `0x13`. Same asset, same tag — so it shares this body rather than growing a + // second one. let lootable = if net.kind == benilla_protocol::EntityKind::Corpse { store.0.corpse_lootable() } else { @@ -1539,8 +1530,8 @@ pub(super) fn arm_loot_fx( persistent: true, class: FxClass::Hold, // Not a kit stage at all (this is `SpawnHardcodedEffect`, not - // `PlaySpellVisualKit`), but wow-re `loot-corpse-effect.md`'s recorded law for the - // loot art is "its own first sequence, LOOPING" — which is precisely what + // `PlaySpellVisualKit`), but the reference's law for the loot art is "its own first + // sequence, LOOPING" (the `0x600640` re-arm) — which is precisely what // [`FxStage::Relive`] renders, and unlike a bare loop-flag arm it holds even if the // art model's sequence were clamp-flagged. // **The one place the two paths differ, and it is a real divergence.** The unit's @@ -1575,7 +1566,7 @@ pub(super) fn arm_loot_fx( armed.retain(|e| units.contains(*e)); } -/// The engine-spawned **ding** (decision 0305, byte-verified — wow-re `levelup-ding.md`): a +/// The engine-spawned **ding** (decision 0305): a /// `UNIT_FIELD_LEVEL` **change** on any streamed unit (the client's descriptor change-watcher /// `CMirrorHandler 0x6045b0` → `SpawnHardcodedEffect(5)`) spawns [`LEVEL_UP_EFFECT`] /// (`Spells\LevelUp\LevelUp.mdl`) at the base attach — the ding visual is DECOUPLED from @@ -1631,9 +1622,9 @@ pub(super) fn arm_level_up_fx( /// boot name-match table to `"HARDCODED Mount Poof"` → row 1185 → `Spells\DruidMorph_Impact_Base` /// — the druid-morph cloud, which is exactly the "cloud shapeshift animation" of the report. /// Byte-for-byte the same spawn shape as [`arm_level_up_fx`]'s index 5, so it is the same edge -/// here (wow-re `mount-composition.md` Q4b, §5 2026-08-03; decision 0927). +/// here (decision 0927). /// -/// The three properties that decide the shape of this arm, all VERIFIED there: +/// The three properties that decide the shape of this arm: /// - **Mount side only.** The build *and the entire allocation* sit behind `5ffa87 je 0x5ffade` /// on the **NEW** field value. `N→0` jumps clean past it: **no poof on dismount.** `0→N` and /// `N→N′` both spawn one. @@ -1689,15 +1680,15 @@ pub(super) fn arm_mount_poof_fx( } } -/// The one-slot **pending-morph latch** — the reference's `[unit+0xd54]`, a `SpellRec*` (wow-re -/// `shapeshift-morph-cloud.md`, §5 trio 2026-08-21): armed by `0x5ff0c0` on every aura **add -/// AND remove** whose spell passes [`is_morph_spell`]; consumed and cleared by the DISPLAYID +/// The one-slot **pending-morph latch** — the reference's `[unit+0xd54]`, a `SpellRec*`: armed by +/// `0x5ff0c0` on every aura **add AND remove** whose spell passes [`is_morph_spell`]; consumed and +/// cleared by the DISPLAYID /// rebuild `0x60abe0`, which — after draining every attached effect and rebuilding the model — /// **replays the latched spell's IMPACT kit** (stage 1, `0x6ec1e0` → `[SpellVisual+0xc]` → /// `0x60edf0` at `0x60ad67`, cleared at `0x60ad6c`). That replay IS the druid-morph cloud the /// player sees, both directions: shift-in's SPELL_GO impact instance dies in the drain and the /// replay restores it in sync with the swap; shift-out has no GO at all (the cancel is -/// client-side, `shapeshift-plaincast-toggle.md`) — the aura-REMOVE handler's last call +/// client-side, `CancelAura 0x6e7040`) — the aura-REMOVE handler's last call /// (`0x6123ad`) re-arms the latch, and the aura fields precede DISPLAYID in the applier's /// ascending field order, so the demorph revert replays the same kit. A swap with no latch — a /// GM morph, a revive — plays nothing. One entry per unit (a one-slot field); entries die with @@ -1756,8 +1747,8 @@ pub(super) fn arm_morph_latch( latch.0.retain(|e, _| units.contains(*e)); } -/// The rebuild's **impact-kit replay** — the tail of the reference's `0x60abe0` (wow-re -/// `shapeshift-morph-cloud.md`): a display swap that finds the latch armed plays the latched +/// The rebuild's **impact-kit replay** — the tail of the reference's `0x60abe0`: a display swap +/// that finds the latch armed plays the latched /// spell's impact kit as an ordinary discrete kit play ([`KitPlay::DISCRETE`] — full /// `PlaySpellVisualKit`: effects, sound, anim, CharProcs) and clears the latch; a swap with no /// latch plays nothing. Runs off [`crate::entities::DisplaySwapped`], which the swap writes as diff --git a/crates/benilla-app/src/creature_anim/spell_visual/tests.rs b/crates/benilla-app/src/creature_anim/spell_visual/tests.rs index b8aec7fd..269d02cf 100644 --- a/crates/benilla-app/src/creature_anim/spell_visual/tests.rs +++ b/crates/benilla-app/src/creature_anim/spell_visual/tests.rs @@ -847,12 +847,11 @@ fn missile_spawn_defers_iff_the_cast_kit_animates() { ); } -/// The **location fallback** and its arrival, end to end through the router (wow-re -/// `spell-go-dest-effect.md` §3 + `spell-visual-lifecycle.md` §Q4): a Speed>0 GO whose hit and -/// miss lists are empty but which carries a ground point spawns one projectile aimed at the -/// point — the flight a pure ground cast (Flare, a bomb thrown at empty dirt) shows — and that -/// projectile's ground arrival rings `SpellVisual` field 13's kit sound **at the landing point**, -/// not at the caster. +/// The **location fallback** and its arrival, end to end through the router (`0x6e8a50`'s empty-hit +/// arm, then the arrival `0x61d870`): a Speed>0 GO whose hit and miss lists are empty but which +/// carries a ground point spawns one projectile aimed at the point — the flight a pure ground cast +/// (Flare, a bomb thrown at empty dirt) shows — and that projectile's ground arrival rings +/// `SpellVisual` field 13's kit sound **at the landing point**, not at the caster. #[test] fn a_targetless_dest_go_spawns_a_ground_missile_whose_arrival_sounds_at_the_point() { const GROUND: u32 = 1543; // Flare's shape: speed>0, dest-targeted, empty hit list @@ -1009,10 +1008,9 @@ fn a_despawned_subject_never_panics_the_router() { } } -/// The `0x400` weapon-visual hold (wow-re `ranged-sheath-exempt-autorepeat.md` §Q4): a RANGED -/// spell's visual play inserts [`RangedHold`] on ANY caster — what keeps a remote shooter in -/// the drawn Load/Hold idle between shots — and a non-ranged visual play clears it (the -/// client's stale-visual cleanup `0x6ec39e`). +/// The `0x400` weapon-visual hold (set by `0x60d020`): a RANGED spell's visual play inserts +/// [`RangedHold`] on ANY caster — what keeps a remote shooter in the drawn Load/Hold idle between +/// shots — and a non-ranged visual play clears it (the client's stale-visual cleanup `0x6ec39e`). #[test] fn ranged_visual_play_arms_the_any_caster_hold_and_a_non_ranged_play_clears_it() { let mut app = app(); @@ -1564,12 +1562,12 @@ fn a_shooter_with_no_ranged_weapon_resolves_no_clip_at_all() { ); } -/// **A state kit's animation id is a comparison, never a play** (decision 2085; VERIFIED wow-re -/// `state-kit-anim-and-stun-pose.md` §1): `0x60edf0`'s tail has exactly one site that hands a -/// kit's `AnimID` to the animation primitive (`0x60f3c5 call 0x5fe2f0`) and `0x60f387 jne` -/// diverts stage 2 around it. Both `SpellVisual` field-4 consumers hardcode stage 2 — the aura -/// watcher and this one, the impact hand-off `0x61dced` — so the state kit's id is only ever the -/// right-hand side of `0x60f390`'s compare, spent on a base recompute. +/// **A state kit's animation id is a comparison, never a play** (decision 2085): `0x60edf0`'s tail +/// has exactly one site that hands a kit's `AnimID` to the animation primitive +/// (`0x60f3c5 call 0x5fe2f0`) and `0x60f387 jne` diverts stage 2 around it. Both `SpellVisual` +/// field-4 consumers hardcode stage 2 — the aura watcher and this one, the impact hand-off +/// `0x61dced` — so the state kit's id is only ever the right-hand side of `0x60f390`'s compare, +/// spent on a base recompute. /// /// The subject is the real Silithus chain: a Dredge Striker's **Charge** (22911 → visual 3783) /// plays `Knockdown`(121) from impact kit 348, and state kit 349 names `Stun`(14). Before this, diff --git a/crates/benilla-app/src/creature_anim/twist.rs b/crates/benilla-app/src/creature_anim/twist.rs index b2397622..c6d8666f 100644 --- a/crates/benilla-app/src/creature_anim/twist.rs +++ b/crates/benilla-app/src/creature_anim/twist.rs @@ -1,5 +1,5 @@ -//! The **display-facing counter-twist** — the client's strafe/look body pose (wow-5875-re -//! `body-facing-pipeline.md` §3, the `0x607ed0` tail → the `0x711f10` bone channels). +//! The **display-facing counter-twist** — the client's strafe/look body pose (the `0x607ed0` tail → +//! the `0x711f10` bone channels). //! //! A unit's rendered root yaw can sit *offset* from its aim — a strafe turns the root toward the //! slide (±90° pure, ±45° diagonal) while the aim (camera, server orientation) holds. The client @@ -71,7 +71,7 @@ pub(crate) fn wrap_pi(angle: f32) -> f32 { /// /// The binary carries a full-share branch too (`0x6103a0`: local player AND a live click-to-move /// action), but `[0xc4d888]` is the click-to-move action type and `0xc` = disabled is its normal -/// in-world value (VERIFIED, wow-re `b947e5aa`) — so half is the effective share for everyone in +/// in-world value — so half is the effective share for everyone in /// ordinary play, exactly as the director's reference eye called it when the full-share variant /// was tried and rejected (decision 0104). Full share would fire only during click-to-move, which /// benilla doesn't have. @@ -84,8 +84,8 @@ fn twist_shares(gap: f32) -> (f32, f32) { /// The shares actually ARMED on a unit — [`twist_shares`] with the **mounted spine gate** applied. /// -/// The SpineLow channel is armed only on an UNMOUNTED unit: `CGUnit+0xdc == 0` (wow-re -/// `body-facing-pipeline.md` Q3, alongside the per-model capability bit `+0xd58 & 0x80` we model as +/// The SpineLow channel is armed only on an UNMOUNTED unit: `CGUnit+0xdc == 0` (in the `0x607ed0` +/// tail, alongside the per-model capability bit `+0xd58 & 0x80` we model as /// "the skeleton authors the key bone"). `+0xdc` is the **mount model** pointer — the same field /// `0x614cd0` passes to `0x7106c0` as "model `[+0xd8]` or mount `[+0xdc]`". The HEAD channel /// (`+0xd58 & 0x100`) carries no mount gate. So a strafing rider counter-twists its head alone @@ -98,7 +98,7 @@ fn twist_shares(gap: f32) -> (f32, f32) { /// back to its animated base, which is the reference's disarm (`0x711f10(4, 0, 0x80)`); skipping /// would freeze our last twist into any frame the clip does not re-key. /// -/// (`+0xdc`'s semantics are flagged INFERRED in that note. It is taken here because the mechanism +/// (`+0xdc`'s semantics are inferred. It is taken here because the mechanism /// PREDICTS the reference: the director reported the mounted over-wobble before this gate was /// found, and it is the gate that accounts for it.) fn armed_shares(gap: f32, mounted: bool) -> (f32, f32) { diff --git a/crates/benilla-app/src/cursor.rs b/crates/benilla-app/src/cursor.rs index a65affd9..6c385139 100644 --- a/crates/benilla-app/src/cursor.rs +++ b/crates/benilla-app/src/cursor.rs @@ -1,10 +1,11 @@ //! The mouse cursor — the real client's `Interface\Cursor\*.blp` set, shown as a crisp **hardware //! (OS-composited) cursor** so it has zero lag, matching the reference 1.12 client (whose cursor is //! lag-free on this same machine). Which cursor shows is the targeting classifier's call -//! ([`crate::target::WorldCursor`], the wow-re cursor RE's decision tree): Point by default, the sword over an -//! attackable unit, the speech bubble / pouch / trainer / taxi over service NPCs, loot/skin over -//! corpses — each with its grayed `Unable*` twin out of range. The whole set preloads at startup -//! (18 tiny BLPs); a stem missing from the archives falls back toward the base cursor, then Point. +//! ([`crate::target::WorldCursor`], the reference's decision tree at `0x4828d0`): Point by +//! default, the sword over an attackable unit, the speech bubble / pouch / trainer / taxi over +//! service NPCs, loot/skin over corpses — each with its grayed `Unable*` twin out of range. The +//! whole set preloads at startup (18 tiny BLPs); a stem missing from the archives falls back toward +//! the base cursor, then Point. //! //! macOS needs native AppKit. winit drives the cursor through the legacy cursor-rect API //! (`addCursorRect`/`resetCursorRects`), which a continuously-redrawing Metal view doesn't honor on @@ -16,11 +17,11 @@ //! platforms use winit's `CursorIcon::Custom` (which works there), swapped on mode change; their //! hide-on-look goes through `CursorOptions.visible`, handled in `player::control`. //! -//! **The held cursor payload** (decision 0216 §5, wow-re cursor-system.md §1 VERIFIED): while -//! `UiScript::cursor_payload()` holds a payload with a resolved icon, the HARDWARE cursor becomes -//! that icon instead of the classified mode — the real client composites the item's `Interface\ -//! Icons\…` art into its drag bitmap and uploads it via the same `SetHardwareCursor` path the mode -//! art uses. Both platform `drive` fns below check the held payload FIRST, each frame, and fall +//! **The held cursor payload** (decision 0216 §5): while `UiScript::cursor_payload()` holds a +//! payload with a resolved icon, the HARDWARE cursor becomes that icon instead of the classified +//! mode — the real client composites the item's `Interface\Icons\…` art into its drag bitmap +//! (`0x523840`) and uploads it via the same `SetHardwareCursor` path the mode art uses +//! (`0x523790`). Both platform `drive` fns below check the held payload FIRST, each frame, and fall //! back to the mode cursor when nothing is held or its icon fails to decode; each caches its built //! cursor per icon path ([`other::PayloadCursorImages`]/[`macos::PayloadCursors`]) — repeated //! pickups of the same icon never re-decode. The icons are 64×64 (unlike the mode BLPs, already @@ -105,8 +106,8 @@ fn box_downsample_32(w: u32, h: u32, rgba: &[u8]) -> Vec { /// Decode a payload icon and box-downsample it to a 32×32 hardware-cursor-ready RGBA8 buffer /// ([`box_downsample_32`]), alpha forced fully opaque — the client's own drag-bitmap composite -/// (byte-verified, wow-re cursor-dragdrop-payload.md: 64×64 → 2×2 box filter → 32×32, alpha -/// written 0xFF; folded back by 0218). `None` on a missing/undecodable icon. +/// (`0x523840`: 64×64 → 2×2 box filter → 32×32, alpha written 0xFF; folded back by 0218). `None` +/// on a missing/undecodable icon. fn decode_payload_cursor_rgba( assets: &mut benilla_assets::WorldAssets, path: &str, @@ -145,8 +146,8 @@ const CURSOR_STEMS: &[&str] = &[ "Skin", "UnableSkin", "Repair", // the repair-mode base cursor (never grayed — the shipped UnableRepair is unreachable) - // The data-driven GameObject cursors (decision 0236, wow-re cursor-system §4): a mailbox's Mail, - // a lock's Mine / GatherHerbs (grayed out of reach), a picked lock's PickLock (never grayed). + // The data-driven GameObject cursors (decision 0236, `0x5f8760`): a mailbox's Mail, a lock's + // Mine / GatherHerbs (grayed out of reach), a picked lock's PickLock (never grayed). "Mail", "UnableMail", "Mine", @@ -154,7 +155,7 @@ const CURSOR_STEMS: &[&str] = &[ "GatherHerbs", "UnableGatherHerbs", "PickLock", - // The spell-targeting pair (wow-re cursor-system.md §5): Cast(2) / UnableCast(22). Cast is + // The spell-targeting pair (`0x4820f0`): Cast(2) / UnableCast(22). Cast is // the armed-enchant-pick overlay's mode; the grayed twin preloads with it (the overlay's // valid/invalid split is its named refinement). "Cast", @@ -166,9 +167,9 @@ fn cursor_path(stem: &str) -> String { format!("Interface\\Cursor\\{stem}.blp") } -/// The **displayed** cursor — the client's single sticky mode global `0xbe2c2c` (wow-re -/// cursor-system.md §1/§7). Read by the platform drivers instead of [`crate::target::WorldCursor`], -/// which is only ever *one* of its two writers. +/// The **displayed** cursor — the client's single sticky mode global `0xbe2c2c`. Read by the +/// platform drivers instead of [`crate::target::WorldCursor`], which is only ever *one* of its two +/// writers. #[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)] pub(crate) struct DisplayedCursor(pub(crate) crate::target::WorldCursor); @@ -229,10 +230,10 @@ fn drive_displayed_cursor( return; } - // **The BASE mode is not a constant** (`0xbe2c4c`, wow-re cursor-system.md §7: *"it is - // independently mutable — e.g. a spell-cancel flow parks it at Cast(2)"*), and that is the - // piece B208 kept missing. `ResetCursor` restores *the value of this cell*, not a hardcoded - // Point — so what a bag slot's `ResetCursor()` shows depends entirely on what is parked here. + // **The BASE mode is not a constant** (`0xbe2c4c`: it is independently mutable — e.g. a + // spell-cancel flow parks it at Cast(2)), and that is the piece B208 kept missing. + // `ResetCursor` restores *the value of this cell*, not a hardcoded Point — so what a bag slot's + // `ResetCursor()` shows depends entirely on what is parked here. // // While a spell awaits its click, the base is **Cast(2)**. The director watched the reference // do exactly this: with Feed Pet armed the cursor is blue over *"pretty much anything UI @@ -250,8 +251,8 @@ fn drive_displayed_cursor( // reference. let repair = script.as_ref().is_some_and(|s| s.repair_mode()); let base = if repair { - // `ShowRepairCursor` parks the base at Repair for as long as it holds (wow-re - // repair-machinery.md); it is the explicit modal, so it wins the cell. + // `ShowRepairCursor 0x4fbcc0` parks the base at Repair for as long as it holds; it is the + // explicit modal, so it wins the cell. WorldCursor { kind: CursorKind::Repair, unable: false, @@ -614,8 +615,7 @@ mod macos { } } // **A cinematic hides the pointer too**, and on this platform it can only be done here: - // the reference's `0x58b590(0)` at StartCinematic / `(1)` at End (wow-re - // `ui/scratch/cinematic-camera-law.md` §3.4). `crate::cinematic` writes + // the reference's `0x58b590(0)` at StartCinematic / `(1)` at End. `crate::cinematic` writes // `CursorOptions.visible`, which is the right lever everywhere else and is *inert on // macOS* for the reason this module exists — winit's cursor-rect route does not survive a // continuously-redrawing Metal view. So the fly-by's pointer is hidden by joining the @@ -814,9 +814,9 @@ mod tests { ); } - /// **The base mode is Cast while a spell awaits its click** (wow-re cursor-system.md §7: the - /// base cell `0xbe2c4c` "is independently mutable — e.g. a spell-cancel flow parks it at - /// Cast(2)"), and `ResetCursor` restores *that value*, never a hardcoded Point. + /// **The base mode is Cast while a spell awaits its click** (the base cell `0xbe2c4c` is + /// independently mutable — e.g. a spell-cancel flow parks it at Cast(2)), and `ResetCursor` + /// restores *that value*, never a hardcoded Point. /// /// This is what the director watched the reference do with Feed Pet armed: **blue over the /// interface at large, grey only out in the world**. Both fall out of the one cell — the world diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index 72870728..71b73ea7 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -15,11 +15,9 @@ //! **latches** (flag bit1 at its register site) says so with [`Registered::latched`]; a //! structural test insists every row has a reader somewhere in the source. //! -//! **The reference column has one source**: wow-re's -//! `system/cvar/scratch/registered-defaults-census.md` and the regenerable manifest beside it, -//! `re/cvar/cvar-register-sites.tsv` — all 214 of the reference's `CVar::Register` sites with -//! name, help, flags, default string, callback, category and record global. A new row looks -//! its answer up there rather than re-deriving it. +//! **The reference column has one source**: all 214 of the reference's `CVar::Register` sites, +//! with name, help, flags, default string, callback, category and record global. A new row +//! looks its answer up there rather than re-deriving it. //! //! - **The change callback is a Bevy observer** ([`CvarChanged`]). An accepted move of a row's //! applied value is triggered as one event, and the subsystem that owns the knob observes it @@ -85,8 +83,8 @@ pub(crate) struct Registered { pub(crate) reference: Reference, /// The reference registers it with flag bit1 (`rec+0x1c & 0x2`, decision 2303): a write is /// staged in [`Row::pending`] and applied only at the latch boundary - /// ([`Cvars::commit_latched`]). Read off `re/cvar/cvar-register-sites.tsv`'s `flags` column - /// (2 or 3); a test pins the set. + /// ([`Cvars::commit_latched`]). Read off each register site's `flags` argument (2 or 3); a test + /// pins the set. pub(crate) latched: bool, } @@ -110,8 +108,7 @@ impl Registered { /// /// - **Not what the reference install's `Config.wtf` says.** That file is one player's saved /// *diff*: `SaveConfig 0x63d980` writes only what has moved off its default, so a line's mere -/// presence is proof the registered default is something *else* (wow-re -/// `cvar/scratch/graphics-cost-cvar-census.md` §10 — the trap it exists to close). +/// presence is proof the registered default is something *else*. /// - **Not, by itself, what a fresh install ends up running at.** `hwDetect` rewrites sixteen /// video CVars out of `VideoHardware.dbc` before the first frame, and the `useUiScale`-OFF leg /// computes a UI scale of its own. Where the client's own boot code overrides the registered @@ -218,8 +215,8 @@ pub(crate) fn registered_pairs() -> impl Iterator `"1"` // `0x82e748`, mask bit `0x10`. It is NOT a show gate like the three above — `ShouldShowName // 0x6070a0` consults only bits `0x1/0x2/0x4` — it gates ONE LINE of the player stack, the a5 - // `"\n<%s>"` guild decoration at `0x609085` (wow-re `object-layer/scratch/overhead-name.md` - // Q4 point 3 + the registrar table). Its fifth sibling `UnitNamePlayerPVPTitle` (bit `0x20`, - // also `"1"`) has no row: a4's rank prefix needs a faction side `ui_unit` cannot resolve for - // an arbitrary player, so there is no reader and 1134 §4 says no key. + // `"\n<%s>"` guild decoration at `0x609085`. Its fifth sibling `UnitNamePlayerPVPTitle` (bit + // `0x20`, also `"1"`) has no row: a4's rank prefix needs a faction side `ui_unit` cannot + // resolve for an arbitrary player, so there is no reader and 1134 §4 says no key. same("UnitNamePlayerGuild", "1"), // The two V-plate toggles over `VPlateMode` — the engine bitmask `[0xc4da34]`'s bit 0 and - // bit 3. 1.12 registers NO nameplate CVar (wow-re, VERIFIED — the bitmask is a plain runtime + // bit 3. 1.12 registers NO nameplate CVar (the bitmask is a plain runtime // global, persisted FrameXML-side as the `RegisterForSave`'d `NAMEPLATES_ON` / // `FRIENDNAMEPLATES_ON`), so these take the LATER-era engine's names: the `autoLootDefault` // posture, where benilla's persistence IS the CVar store (0954) and a setting with no 1.12 @@ -537,8 +530,7 @@ pub(crate) const REGISTERED: &[Registered] = &[ // reads only `SmallCull`. So its own slider **reads Medium at boot**, which is this row. // Mid-1804 this was filed as a deviation against "0" on two true facts that are not the // answer: `OptionsFrame.lua:430`'s Defaults ladder yields 0, and `frillDensity` registers 16. - // A partly-verified mechanism is not the mechanism (wow-re - // `cvar/scratch/registered-defaults-census.md`, the §5 round 1804 dispatched). + // A partly-verified mechanism is not the mechanism (1804). // // What IS still divergent is the grass, and that is a *mapping* difference rather than a // default: our stop 1 scatters ×2 (32) where the reference's boot `frillDensity` is 16 @@ -550,8 +542,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("WorldDetail", "1"), // The SAME knob in the reference's own unit (2151), and the CVar 1.12 actually registers for // it: `0x68862e` passes name `0x8423d8` `"frillDensity"`, default string `0x864644` `"16"`, - // help "Terrain frill density", flags `1`, callback `0x688de0`, record `[0xc7f2f4]` (wow-re - // `re/cvar/cvar-register-sites.tsv` row 185). The value is **cells visited per chunk**: the + // help "Terrain frill density", flags `1`, callback `0x688de0`, record `[0xc7f2f4]`. The value + // is **cells visited per chunk**: the // callback clamps `[1, 256]` and hands the number to `0x6725a0` → `[0xc7b494]`, which bounds // the detail-doodad scatter loop at `0x6bfcfb`/`0x6bff1c`. Our scatter is the byte-exact port // of that loop, so `frillDensity` is not a new dial — it is the unit @@ -589,9 +581,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ // // Eight rows, registered by the reference in ONE place — `0x626d00`, a loop over the // `{cvarName, defaultValue}` pairs at `0x8629e0` skipping the NULL/empty names, then one - // unrolled call for the death range (wow-re `object-layer/scratch/combat-log-chat-law.md` - // §5.2). They read as the CVar record's **float** (`+0x24`), unlike the periodic gate below, - // which reads the int. + // unrolled call for the death range. They read as the CVar record's **float** (`+0x24`), unlike + // the periodic gate below, which reads the int. // // **They are why a damage meter's range slider does something.** `BigWigs/Plugins/Range.lua` // and `DPSMate/DPSMate_DataBuilder.lua` both read and write all eight; unregistered, every @@ -648,9 +639,9 @@ pub(crate) const REGISTERED: &[Registered] = &[ // `assistAttack` (`0x48fc50`, record `[0xb4d8f8]`) — `/assist`'s opt-in second leg: select the // basis unit's target AND open the swing on it. Three references image-wide, two of them the // shared assist tails; `CanAssist 0x6066f0` is verified NOT on the path. The `"0"` default is - // the one wow-re had to correct against itself — its first pass read `"3"` off the *next* - // registration's default (`minimapZoom`), the `mov ds:` adjacency trap — so it is worth saying - // plainly here: stock `/assist` selects and does not swing. + // easy to misread: the *next* registration's default (`minimapZoom`) is `"3"`, and reading it + // off instead is the `mov ds:` adjacency trap — so it is worth saying plainly here: stock + // `/assist` selects and does not swing. same("assistAttack", "0"), // ── Mouse-look, per axis: the two CVars whose absence RAISED in the stock window ──────────── // @@ -684,26 +675,25 @@ pub(crate) const REGISTERED: &[Registered] = &[ // **The spelling is FrameXML's, not the binary's**, and that is deliberate: `WoW.exe` holds // `mouseSpeed` (capital S, register site `0x402c7b`) while `UIOptionsFrame.lua`'s slider // writes `cvar = "mousespeed"`. The reference reconciles them by looking CVars up - // case-insensitively (`SStrCmpI`, wow-re `cvar/cvar.md`), and so do we (the registry and + // case-insensitively (`SStrCmpI` in `CVar::Lookup 0x63de30`), and so do we (the registry and // every observer lowercase first), so both spellings answer. We take the one the interface uses, because // that is the one an addon will type. // - // **The VALUE agrees and the MECHANISM does not** (wow-re - // `cvar/scratch/registered-defaults-census.md` §, 1804). The reference's default is not a + // **The VALUE agrees and the MECHANISM does not** (1804). The reference's default is not a // literal at all: it is `sprintf("%1.1f", SPI_GETMOUSESPEED × 0.1)`, which is `"1.0"` on a - // stock Windows host — so "1" is the right number. But its record has **zero readers**: the - // slider drives the *operating system's* pointer speed through `SPI_SETMOUSESPEED` (clamped - // [0.1, 2.0]), not an in-engine gain. benilla will not reach out and repoint the OS mouse, so - // ours is a multiplier over our own per-pixel rate — the same dial, the same range, the same - // resting value, a different thing underneath. Recorded here rather than filed as a deviation - // because the default is the question this table answers, and the default matches. + // stock Windows host — so "1" is the right number. But its record `[0x882704]` has **zero + // readers**: the slider drives the *operating system's* pointer speed through + // `SPI_SETMOUSESPEED` (callback `0x402ec0`, clamped [0.1, 2.0]), not an in-engine gain. + // benilla will not reach out and repoint the OS mouse, so ours is a multiplier over our own + // per-pixel rate — the same dial, the same range, the same resting value, a different thing + // underneath. Recorded here rather than filed as a deviation because the default is the + // question this table answers, and the default matches. same("mousespeed", "1"), // Max Camera Distance (1140): 1.12's `cameraDistanceMaxFactor` (its MAX_FOLLOW_DIST slider, // 1..2 step 0.1) over `cameraDistanceMax`'s 15 yd base. **"1", the reference's registrar - // value** (wow-re `ui/scratch/follow-camera.md`: "cameraDistanceMax 15.0, - // cameraDistanceMaxFactor 1.0") — so the shipped ceiling is 15 yd, not the 30 this row - // registered from 1140 until 1804. `ZoomLimit`'s doc carries why that changed; the slider - // still reaches 2. + // value** (`cameraDistanceMax` "15.0" `0x84fbd0`, `cameraDistanceMaxFactor` "1.0" `0x82e92c`) + // — so the shipped ceiling is 15 yd, not the 30 this row registered from 1140 until 1804. + // `ZoomLimit`'s doc carries why that changed; the slider still reaches 2. same("cameraDistanceMaxFactor", "1"), // Camera Following Style (1493, re-pinned by 1502): 1.12's `cameraSmoothStyle` — the // auto-return that swings the camera back behind the character. Registered "1" = Smart, which @@ -727,12 +717,11 @@ pub(crate) const REGISTERED: &[Registered] = &[ // **The four 1.12 camera-option toggles** (decision 2149) — the `UIOptionsFrame` checkboxes // FOLLOW_TERRAIN / HEAD_BOB / SMART_PIVOT / WATER_COLLISION, all four of which sat on the // unbacked-CVar census with a byte-level spec and no feature until now. Defaults are the - // registrar's own (`re/cvar/cvar-register-sites.tsv`), and two of them are **"1"** — which is - // why building them was not cosmetic: benilla was the divergence on those, not the reference. + // registrar's own, and two of them are **"1"** — which is why building them was not cosmetic: + // benilla was the divergence on those, not the reference. // - // `cameraPivot` `[0xbe10a4]` "1" (`0x50bda3`) — smart pivot. Mechanism: wow-re - // `ui/scratch/camera-cvar-gates.md` §3 (gate `0x510690`, routing `0x50fee0`, release - // `0x5107f0`); ours is `player::camera_dynamics::SmartPivot`. + // `cameraPivot` `[0xbe10a4]` "1" (`0x50bda3`) — smart pivot. Mechanism: gate `0x510690`, + // routing `0x50fee0`, release `0x5107f0`; ours is `player::camera_dynamics::SmartPivot`. same("cameraPivot", "1"), // Its two drag-shape thresholds, both read by that routing (`0x50fff5`/`0x510004`) and both // in RADIANS of camera rotation — the deltas they are compared against are already scaled by @@ -759,8 +748,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("cameraWaterCollision", "1"), // `cameraTerrainTilt` `[0xbe0fd4]` **"0"** (`0x50bcfd`) — Follow Terrain, and the one of the // four that ships OFF, so building it changed nothing until a player ticks the box. Mechanism: - // wow-re `camera-cvar-kernels.md` §2 (the ahead-probe and the five-step staircase) and - // `camera-smooth-style.md` §9 (the arm); ours is `player::camera_dynamics::TerrainTilt`. + // the ahead-probe and the five-step staircase (`0x50d900`) and the arm (`0x50dbc0`); ours is + // `player::camera_dynamics::TerrainTilt`. same("cameraTerrainTilt", "0"), // The ground channel's rate, deg/s (`[0xbe0fc0]`) and the duration bound its `Factor` scales // (`[0xbe1050]`/`[0xbe1054]`, seconds). The floor always binds — `20° / 7.5°/s` is 2.67 s @@ -769,8 +758,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("cameraTerrainTiltTimeMin", "3"), same("cameraTerrainTiltTimeMax", "10"), // `cameraBobbing` `[0xbe10c0]` **"0"** (`0x50b76d`) — head bob, the fourth of the four and the - // second that ships OFF. Mechanism: wow-re `camera-cvar-kernels.md` §4 and - // `camera-cvar-gates.md` §2; ours is `player::camera_dynamics::HeadBob`. + // second that ships OFF. Mechanism: the kernel `0x511920` and its gate `0x5105e0`; ours is + // `player::camera_dynamics::HeadBob`. same("cameraBobbing", "0"), // Its four numeric siblings. The two amplitudes are in the CVar's own units — the kernel // scales both by 1/36 (`[0x7ff9d0]`) to reach yards. `cameraBobbingSmoothSpeed` is the odd one @@ -788,9 +777,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ // // **Byte-read since 1804** — register site `0x48fc34`, default string `"0"`, record // `[0xb4d904]`, and a whole-image census finds that record has **no engine reader at all**: - // this CVar is FrameXML's alone, which is exactly the shape this row was built for (wow-re - // `cvar/scratch/registered-defaults-census.md`). It used to concede "behaviour-derived, not - // byte-read"; that hedge is retired. + // this CVar is FrameXML's alone, which is exactly the shape this row was built for. It used to + // concede "behaviour-derived, not byte-read"; that hedge is retired. same("statusBarText", "0"), // Enhanced Tooltips (B230): 1.12's `UberTooltips`, the *Enhanced Tooltips* checkbox // (`UIOptionsFrame.lua:15`, `USE_UBERTOOLTIPS`). **No host knob** — its consumers are Lua, and @@ -804,13 +792,13 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("UberTooltips", "1"), // The two chat-bubble switches (1139): 1.12's own registrar CVars over the bubble gate, // which held them as `const bool` from 0598 until this window had a page for them. Both are - // the binary's own (registrar `0x603280` — wow-re `object-layer/scratch/chat-bubble.md`: + // the binary's own (registrar `0x603280`: // `ChatBubbles` "1", `ChatBubblesParty` "0"); the party half shipped ON from 0598 to 1804 on // the director's `/p` ask, and is a click away on the Chat page. same("ChatBubbles", "1"), same("ChatBubblesParty", "0"), // **The two text filters** (2077) — 1.12's own pair, and both are real features rather than - // vestigial switches, which is what the wow-re §5 round behind `text-filter-law.md` settled. + // vestigial switches. // Registered `"1"` each, byte-read: `0x402e68` (`profanityFilter`, name `0x82e7f4`, callback // `0x403570`) and `0x402e8e` (`spamFilter`, name `0x82e7d4`, callback `0x4035b0`), both pushing // the shared `"1"` literal `0x82e748`, both category 4. @@ -825,8 +813,7 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("spamFilter", "1"), // **The loading-screen tip of the day** (2077) — 1.12's own pair, both registered lazily by // `CGlueMgr::EnterWorld` on its way to the config flush (`0x46b633` `gameTip` `"0"`, - // `0x46b658` `showGameTips` `"1"`, both category 5, neither with a callback or a help string; - // wow-re `system/loadingscreen/scratch/game-tip-of-the-day.md`). + // `0x46b658` `showGameTips` `"1"`, both category 5, neither with a callback or a help string). // // `gameTip` is not a preference — it is the **cursor**, and it holds the NEXT row rather than // the one on screen, which is why the reference's own `Config.wtf` reads `SET gameTip "34"` @@ -837,8 +824,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ same("showGameTips", "1"), // *Detailed Loot Information* (1589, the Chat page) — 1.12's `showLootSpam`, whose subject is // group LOOT ROLLS (its own tooltip: "Uncheck this to hide individual loot roll messages and - // only show the winner"). Registered `"1"`, **byte-read**: wow-re's census of `0xb4e2bc` - // (`lootroll-chat-and-lifecycle.md` §4) has the register site at `0x48fd1c`, name `0x8430a0`, + // only show the winner"). Registered `"1"`, **byte-read**: a census of `0xb4e2bc` has the + // register site at `0x48fd1c`, name `0x8430a0`, // default string `0x82e748` = "1", **category** 5 — and exactly four references to the global, // one writer and three readers, all in the roll-line composers. The knob is // [`crate::ui_loot::LootConfig::show_loot_spam`], welded to that default below. @@ -847,23 +834,23 @@ pub(crate) const REGISTERED: &[Registered] = &[ // help string says what it does: "Receive notification when guild members log on/off". // // Registered **`"0"`** — this is one of the few rows that ships a feature OFF, and it is - // byte-read rather than chosen: the register site `0x5e24c7` pushes default `0x82e570` = "0" - // (§5, wow-re `system/object-layer/scratch/guild-signon-cvar-gate.md`). A stock 1.12 client is + // byte-read rather than chosen: the register site `0x5e24c7` pushes default `0x82e570` = "0". + // A stock 1.12 client is // silent when a guildmate logs in, and a whole-image census of the record global `0xc4d3c4` // finds exactly two readers, both inside `SMSG_GUILD_EVENT`'s handler. The knob is // [`crate::ui_guild::GuildMemberNotify`]; the other three conjuncts of the line's display // condition live on `ui_guild::net::event`. same("guildMemberNotify", "0"), - // The minimap's two zoom indices (1131). Byte-verified 1.12 CVars, both registered `"3"` - // (wow-re, at the `RegisterCVar 0x63db90` argument slot). No options row drives these — the + // The minimap's two zoom indices (1131). 1.12 CVars, both registered `"3"` (`0x48fc6c`, + // `0x48fc88`, at the `RegisterCVar 0x63db90` argument slot). No options row drives these — the // +/- buttons on the minimap do, through `Minimap:SetZoom`, exactly as in the reference, where // `set_zoom` writes the live index and `CVar::Set`s the CVar in one breath. The knob is // [`crate::minimap::MinimapZoom`], the widget's live index is seeded from it at UI load. same("minimapZoom", "3"), same("minimapInsideZoom", "3"), // The addon version gate (decision 1292): 1.12's own `checkAddonVersion`, the *Load out of - // date AddOns* checkbox INVERTED. Registrar default "1" = check enforced = box unticked — - // byte-verified (wow-re `addon-version-gate.md` §1.1: the key appears in Config.wtf exactly + // date AddOns* checkbox INVERTED. Registrar default "1" = check enforced = box unticked + // (`0x402c3b`; the key appears in Config.wtf exactly // while force-load is on and vanishes when it is turned off, `SaveConfig 0x63d980`'s // skip-default rule). No host knob: its consumers are the load walk (via the persisted value, // [`Cvars::addon_version_check`]) and the gate's live per-query read in the VM. @@ -874,8 +861,7 @@ pub(crate) const REGISTERED: &[Registered] = &[ // real selector — `0x63a3c4` compares the live value case-insensitively against `"OpenGl"` // (`0x842a5c`) and `GxDevCreate` builds `CGxDeviceD3d` on anything else — but no shipped // `WTF` overrides it, so the stock client is always D3D9 and the whole GL arm is dead code - // image-wide (wow-re states this from a dozen nodes; `models/scratch/part-additive-combine.md` - // §"the gxApi selector" is the decoded compare). + // image-wide. // // **Here it DESCRIBES, it does not steer** — the `gxColorBits`/`gxDepthBits` posture. benilla // renders through wgpu, which has no D3D9 backend to name and no chooser to offer: the backend @@ -907,8 +893,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ // FrameXML). The knob is [`crate::video::VideoConfig::vsync`], which the window's // `present_mode` follows. // - // Default "1" is BEHAVIOUR-derived, not byte-read: 1.12's registrar value for this var is not - // pinned in wow-re, and "1" is what benilla actually ships — the primary window is born at + // Default "1" is BEHAVIOUR-derived, not byte-read: 1.12's registrar value for this var is + // unconfirmed, and "1" is what benilla actually ships — the primary window is born at // `PresentMode::default()` (Fifo), and a test in `video.rs` welds the two together. // // Two knowing departures from the reference row, both stated on [`crate::video`]: its @@ -932,9 +918,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ // Departs from the reference row's `gxRestart = 1` exactly like `gxVSync` above: ours applies // on the click. // Byte-read `"0"` at register site `0x63a889` — **for enUS**. Three defaults in this binary - // are locale-conditional and this is one: `gxWindow` and `gxMaximize` register `"1"` on zhCN, - // `AutoInteract` `"1"` on koKR (wow-re `cvar/scratch/registered-defaults-census.md`, which - // caught its own instrument publishing a single arm mid-round). benilla is enUS-only, so `"0"` + // are locale-conditional and this is one: `gxWindow` and `gxMaximize` (`0x63a8e0`) register + // `"1"` on zhCN, `AutoInteract` (`0x603390`) `"1"` on koKR. benilla is enUS-only, so `"0"` // is the answer here; the note exists so the next reader does not take a locale-conditional // default for an unconditional one. same("gxWindow", "0").latched(), @@ -992,8 +977,7 @@ pub(crate) const REGISTERED: &[Registered] = &[ // 193-row table that field only ever holds 1 (144 rows) or 2 (49 rows), and the three rows the // fallback match can reach all hold 1 — so on any GPU the 2004-era table does not list, which // is every machine this client runs on now, the registered string is "1". A 1 is genuinely no - // multisampling on both of its backends, not a one-sample mode. (wow-re §5 cross-check, - // 2026-08-26, `system/console/scratch/gxmultisample-default.md`; decision 1629.) + // multisampling on both of its backends, not a one-sample mode. (Decision 1629.) // // Latched means a change is PENDING until the next launch — the reference's own callback // echoes "set pending gxRestart" — so this row persists and `GetCVar` answers it, while the @@ -1069,8 +1053,8 @@ pub(crate) const REGISTERED: &[Registered] = &[ // channels, or the storm/fog blend, so no server-visible behaviour rides it. // // The reference registers **`"2"`** at `0x67b806` (flags 0, callback `0x67b870`, name string - // `0x8685ac`) — wow-re `cvar/scratch/graphics-cost-cvar-census.md` §4, whose §10 table also - // lists this row among the twelve the reference install's `Config.wtf` moves off its default. + // `0x8685ac`), and this row is among the twelve the reference install's `Config.wtf` moves off + // its default. deviates( "weatherDensity", "3", @@ -1091,15 +1075,15 @@ pub(crate) const REGISTERED: &[Registered] = &[ // (`byte[dev+0x20b]` = `CGxFormat +0x07` = `gxWindow`), which is every mode benilla has. So the // reader here is not a ramp upload: it is [`crate::ui_gamma::DisplayGamma`], the same curve // applied to the same values one stage later, inside the pass that already owns the composited - // image's single decode. wow-re `ffxeffects/scratch/whole-frame-grade-verdict.md` §(a) for the - // curve and `ui/scratch/video-options-verbs.md` §3 for the verbs. + // image's single decode. // // 1.0 is the identity ramp — load-bearing rather than tidy: at the default this client's // output is what it was before the setting existed, so no visual golden moves. // // **Written `"1.000000"` rather than `"1.0"` or `"1"`, and that is not cosmetic.** Every value - // this key ever receives comes through `SetGamma`, whose `SStrPrintf(buf, 0x10, "%f", …)` is - // six decimals — so the row that Restore Defaults produces is `"1.000000"`, and a default + // this key ever receives comes through `SetGamma 0x4891f0`, whose + // `SStrPrintf(buf, 0x10, "%f", …)` is six decimals — so the row that Restore Defaults produces + // is `"1.000000"`, and a default // string in any other spelling would make it compare *moved* and write a `config.toml` line // holding the default value. The slider rows dodge this with their own trailing-zero strip // (`OptionsSlider_OnValueChanged`); a row whose store is an engine verb cannot, because the @@ -2314,7 +2298,7 @@ mod tests { d["showLootSpam"] != 0.0, LootConfig::default().show_loot_spam ); - // …and the one row that ships a feature OFF, byte-read at `0x5e24c7` (§5). + // …and the one row that ships a feature OFF, byte-read at `0x5e24c7`. assert_eq!( d["guildMemberNotify"] != 0.0, crate::ui_guild::GuildMemberNotify::default().0 @@ -3184,7 +3168,7 @@ mod tests { /// /// **`realmName` defaults EMPTY** — empty rather than a guess: the value is written from the /// session's real realm by `set_realm_name`, so the default only ever describes a client that - /// has not connected. wow-re records `"Last realm connected to"` beside the registration, but + /// has not connected. `0x85d684` holds `"Last realm connected to"` beside the registration, but /// that reads like the CVar's HELP text rather than its value, and nothing here needs it /// resolved — `""` is what `Ace/AceState.lua:27`'s `ace.trim(GetCVar("realmName"))` handles /// cleanly, and inventing a realm name would be worse than admitting we have none yet. @@ -3546,10 +3530,10 @@ mod tests { } } - /// The eight rows the reference latches that benilla registers, and no other — read off - /// `re/cvar/cvar-register-sites.tsv` (`flags` 2 or 3): the sound-init row, and the `gx*` - /// block `RestartGx` commits. `trilinear`/`anisotropic`/`farclip` register with `flags = 1` - /// there and apply live, so they are deliberately not here. + /// The eight rows the reference latches that benilla registers, and no other — read off their + /// register sites (`flags` 2 or 3): the sound-init row, and the `gx*` block `RestartGx` + /// commits. `trilinear`/`anisotropic`/`farclip` register with `flags = 1` and apply live, so + /// they are deliberately not here. #[test] fn the_latched_rows_are_the_references_own() { let mut latched: Vec<&str> = REGISTERED diff --git a/crates/benilla-app/src/death/mod.rs b/crates/benilla-app/src/death/mod.rs index 68720661..62fcf1d1 100644 --- a/crates/benilla-app/src/death/mod.rs +++ b/crates/benilla-app/src/death/mod.rs @@ -65,8 +65,8 @@ pub(crate) struct DeathNet { pub(crate) corpse: Option, /// A pending resurrection offer; cleared when answered or when the popup times out. pub(crate) resurrect: Option, - /// Bumped on every `SMSG_RESURRECT_REQUEST` — the offer announces per MESSAGE (wow-re - /// death-ui.md §8: handler `0x5e7bc0` signals per arrival through `0x5ded50`), not per + /// Bumped on every `SMSG_RESURRECT_REQUEST` — the offer announces per MESSAGE (handler + /// `0x5e7bc0` signals per arrival through `0x5ded50`), not per /// `Some`-edge. `resurrect` is only cleared by an Accept/Decline, and the stock UI can hide /// the popup without either (PLAYER_UNGHOST's `StaticPopup_Hide` runs no `OnCancel`), so an /// edge latch there swallowed every later offer for the session. @@ -97,9 +97,9 @@ impl DeathNet { /// The reference raises this dialog **client-side, on the click**: `0x5f0130`'s bit-5 arm /// (`0x5df730`) sends no packet at all — it caches the healer's guid and fires /// `CONFIRM_XP_LOSS`, and `CMSG_SPIRIT_HEALER_ACTIVATE` belongs to the Lua Accept - /// (`AcceptXPLoss`), which calls the same function with a ZERO guid (wow-re - /// `interact-dead-fork-and-npc-service-ladder.md` §C). vmangos also *pushes* the question as - /// `SMSG_SPIRIT_HEALER_CONFIRM` off its gossip route, and benilla honours both roads in. + /// (`AcceptXPLoss`), which calls the same function with a ZERO guid. vmangos also *pushes* the + /// question as `SMSG_SPIRIT_HEALER_CONFIRM` off its gossip route, and benilla honours both + /// roads in. /// /// The generation bump is what makes a re-ask re-show a cancelled dialog: the confirm /// announces per MESSAGE, not per `Some`-edge (decision 1068). @@ -112,8 +112,8 @@ impl DeathNet { /// The feed's memory, in **two scopes** (decisions 1290/1291): the body's own state machine is /// world memory and survives a `/reload`; what this VM has been *told* dies with the VM. /// -/// The reference draws the same line by construction — its death mirror is engine-side (wow-re -/// death-ui.md §1), so a `ReloadUI` neither re-arms the release window nor loses it, while the +/// The reference draws the same line by construction — its death mirror is engine-side +/// (`[0xb4e340]`), so a `ReloadUI` neither re-arms the release window nor loses it, while the /// rebuilt UI repaints from the surviving state. Ours mirrors that: `mirror`/`died_at` stay put /// across a VM replacement, and the announce latches reset with the VM so the fresh frame tree /// hears the events the old one consumed. @@ -123,8 +123,8 @@ struct DeathFeedState { /// arms and clears the release window. `None` before a world session's first snapshot. mirror: Option<(u64, bool, bool)>, /// **World-scoped.** `Time::elapsed_secs_f64` at the death edge — the anchor of the - /// client-side release window (byte-VERIFIED, wow-re death-ui.md §1: the wire carries only - /// the PLAYER_FIELD_BYTES timer BIT; the client arms `now + 360000 ms` at its own + /// client-side release window (the wire carries only the PLAYER_FIELD_BYTES timer BIT; the + /// client arms `now + 360000 ms` (`0x5e9a30`) at its own /// alive→dead mirror-edge — so a login-while-dead re-arms at login exactly like ours, and /// expiry reads 0, never negative). Deliberately NOT per-VM: a `/reload` must not restart /// the six minutes — the reference's engine-side window keeps running through one. @@ -144,7 +144,7 @@ struct DeathAnnounced { /// CORPSE_IN_RANGE / CORPSE_OUT_OF_RANGE / CORPSE_IN_INSTANCE edge memory (decision 0308 §5). corpse_range: Option, /// The [`DeathNet::reclaim_generation`] this latch last saw — a fresh `0x269` re-fires the - /// range events through the latch (the client's own re-announce, death-ui.md §4), so the + /// range events through the latch (the client's own re-announce, `0x4962d0`), so the /// RECOVER_CORPSE popup re-shows with the new delay. reclaim_generation: u32, /// The [`DeathNet::resurrect_generation`] this VM was last told about (`RESURRECT_REQUEST` @@ -171,14 +171,14 @@ enum CorpseRange { /// mirror both sides know; never on the wire. const RELEASE_WINDOW_SECS: f64 = 360.0; -/// The corpse-range dialog radius — byte-VERIFIED **40.0 yd, inclusive** (wow-re death-ui.md §3: -/// the client compares d² ≤ `[0xb4e2ac]` = 1600.0 per frame against the corpse-QUERY cache, never +/// The corpse-range dialog radius — **40.0 yd, inclusive** (the client compares +/// d² ≤ `[0xb4e2ac]` = 1600.0 per frame against the corpse-QUERY cache, never /// the streamed corpse object; edge-latched, no hysteresis). vmangos's `CORPSE_RECLAIM_RADIUS 39` /// comment "equal client dialog show radius" is off by one against the client's own constant — /// the server reclaim gate simply sits 1 yd inside the dialog radius. Squared yards. const CORPSE_RANGE_SQ: f32 = 40.0 * 40.0; -/// The spirit-healer dialog range — byte-VERIFIED (wow-re death-ui.md §7: CheckSpiritHealerDist +/// The spirit-healer dialog range (CheckSpiritHealerDist /// compares d² ≤ `[0xc4c28c]` = 30.864 = 5.5556², the service interaction range). Squared yards. const SPIRIT_HEALER_RANGE_SQ: f32 = 5.5556 * 5.5556; @@ -297,7 +297,7 @@ fn feed_death( // the map markers. The server's own "corpse gone" push is LOOTER-gated (vmangos // Map.cpp:3617-3629: the unprompted u8(0) goes out only when a PvP looter exists), so // a PvE res never gets one; the real client re-runs its corpse query off the same - // ghost-bit edge (wow-re death-ui.md §5's watcher), and post-res the player→corpse + // ghost-bit edge (the watcher `0x5ee990`), and post-res the player→corpse // binding is gone (SpawnCorpseBones unbinds), so the answer clears the cache. // Director-reported: the map tombstone survived a spirit-healer res without this. let _ = net.0.send(ClientCommand::CorpseQuery); @@ -378,7 +378,7 @@ fn feed_death( // coord transform, so the Bevy-space compare is the yard compare. let range = if ghost { death_net.corpse.map(|cp| { - // The client's own law (death-ui.md §3-4): within 40 yd (inclusive) of the QUERY + // The client's own law (`0x492130`, `0x4920f0`): within 40 yd (inclusive) of the QUERY // cache position, on the display map → IN_RANGE when displayMapId == corpseMapId, // else IN_INSTANCE (standing at a dungeon's ghost entrance, corpse inside). let on_display_map = map @@ -420,7 +420,7 @@ fn feed_death( const SPELL_EFFECT_SELF_RESURRECT: u32 = 94; /// What a `UseSoulstone()` would spend right now — the client's own two-legged fork -/// (`Script::UseSoulstone 0x48ad70`, wow-re death-ui.md §10.4), resolved app-side because neither +/// (`Script::UseSoulstone 0x48ad70`), resolved app-side because neither /// leg's inputs (the spell catalog, the item cache) are bound in the script VM. #[derive(Clone, Debug, PartialEq)] pub(crate) enum SelfRes { @@ -451,7 +451,7 @@ impl SelfRes { } /// **`HasSoulstone()`'s whole answer**, and `UseSoulstone()`'s routing — one resolver, because the -/// binary is one predicate the two bindings share (wow-re death-ui.md §10.1/§10.4). +/// binary is one predicate the two bindings share (`0x48ac80`, `0x48ad70`). /// /// Three gates in the client's own order, and the order is the performance story as much as the /// fidelity one: @@ -523,9 +523,9 @@ fn resolve_self_res( /// `GlobalStrings.lua`** (decision 2045). That is the reference's own key: `GetResSicknessDuration` /// (`0x51a3a0`) computes a millisecond duration off `Spell.dbc` 15007 × `SpellDuration.dbc` and /// hands it to the shared formatter `0x52fa50(ms, "GENERIC", 0, 1)`, whose minute arm is -/// `GENERIC_MIN[_P1]` with `ms / 60000` (wow-re `ui/scratch/death-ui.md` §9). Every value this -/// table can produce — 1 to 10 minutes — lands in that arm, so the day/hour/second arms have no -/// case here and the ladder is not reproduced. +/// `GENERIC_MIN[_P1]` with `ms / 60000`. Every value this table can produce — 1 to 10 minutes — +/// lands in that arm, so the day/hour/second arms have no case here and the ladder is not +/// reproduced. /// /// **What benilla still models differently, deliberately:** the *number* comes from vmangos's /// server-side table rather than from `Spell.dbc`'s `Base + PerLevel·level`, because the aura the @@ -545,9 +545,9 @@ fn sickness_duration(level: u32, get: &dyn Fn(&str) -> Option) -> Option )) } -/// Drive the ghost-world look (decision 0308 §7, all byte-VERIFIED): the FFXDeath screen pass -/// gates on `PLAYER_FLAGS_GHOST` — instant on release, instant off at resurrect (wow-re -/// death-pass.md: the flag's CMirrorHandler watcher is the single driver of the FFX pass swap, +/// Drive the ghost-world look (decision 0308 §7): the FFXDeath screen pass gates on +/// `PLAYER_FLAGS_GHOST` — instant on release, instant off at resurrect (the flag's CMirrorHandler +/// watcher `0x5ee990` is the single driver of the FFX pass swap, /// the death light profile, and the ghost ambience — one watcher, several consumers; ours splits /// the consumers across their owning systems off the same derived flag). fn drive_death_look( @@ -590,8 +590,8 @@ fn drain_death( targeting: crate::spell::cast_target::CastTargeting, mut ladder: crate::spell::CastLadder, mut ui_errors: ResMut, - // The soulstone leg is a real arm-290 caller in the reference's own census (wow-re - // `bind-confirm-law.md`: 290 ← UseSoulstone · UseInventoryItem · UseAction · UseContainerItem), + // The soulstone leg is a real arm-290 caller in the reference (290 = `0x5d8d00` ← + // UseSoulstone · UseInventoryItem · UseAction · UseContainerItem), // so it goes through the shared send carrying the same bind gate every other item use does. mut gate: crate::ui_bind_confirm::BindGate, ) { @@ -913,7 +913,7 @@ mod self_res_tests { } } - /// **The dead gate comes first** (wow-re death-ui.md §10.1): `HasSoulstone` reads + /// **The dead gate comes first** (`0x48acba`): `HasSoulstone` reads /// `UNIT_FIELD_HEALTH` and answers nil while it is positive. A living player holding a /// standing self-res spell gets nothing — and neither does a released **ghost**, whose health /// the server sets to exactly 1 (`BuildPlayerRepop`). That is what confines the answer to the @@ -965,7 +965,7 @@ mod self_res_tests { ); } - /// An id the catalog cannot resolve is **"UNKNOWN"**, not nil (§10.1's fourth exit). Hiding + /// An id the catalog cannot resolve is **"UNKNOWN"**, not nil (`0x48ad1d`). Hiding /// the button instead would silently strip a self-res the server is holding for us. #[test] fn an_unresolvable_spell_id_reads_unknown_not_nil() { @@ -987,7 +987,7 @@ mod self_res_tests { } } - /// **A zero field is not the nil path** (§10.3): the client falls through to the carried + /// **A zero field is not the nil path** (`0x5ed630`): the client falls through to the carried /// inventory and looks for an item whose ON-USE (`trigger == 0`) spell carries /// `SPELL_EFFECT_SELF_RESURRECT`, labelling the button with the ITEM's name. Three items /// stand in the backpack — one whose self-res spell is on an EQUIP trigger (must not count), diff --git a/crates/benilla-app/src/death/net.rs b/crates/benilla-app/src/death/net.rs index 6a37c4dd..09d7fd33 100644 --- a/crates/benilla-app/src/death/net.rs +++ b/crates/benilla-app/src/death/net.rs @@ -108,7 +108,7 @@ fn on_session_end(In(_): In, mut death_net: ResMut) { /// it was `Other` while nothing rendered a corpse), and the owner field is corpse-only, so nothing /// else can match. Rides the `ObjectCreate` arm. /// -/// **Two gates, not one** (wow-re `corpse-click-and-reclaim.md`, §5 cross-checked): the reference's +/// **Two gates, not one**: the reference's /// reclaim guid `[0xb4e328/32c]` has exactly one writer, `0x4920d0`, called from three sites in the /// CGCorpse translation unit — and every one of them is gated on `CORPSE_FIELD_OWNER == me` /// **and `CORPSE_FIELD_FLAGS` bit 0 clear**, i.e. a real body and not a bone pile. So the latch is @@ -191,7 +191,7 @@ fn corpse_query( /// `SMSG_CORPSE_RECLAIM_DELAY` — the delay, anchored to arrival time (`now_secs` is the same /// `Time::elapsed_secs_f64` the feed reads back). The client's `0x269` handler re-fires the -/// corpse-range events through its latch (wow-re death-ui.md §4), so the feed re-announces on the +/// corpse-range events through its latch (`0x4962d0`), so the feed re-announces on the /// generation bump. fn corpse_reclaim_delay(delay_ms: u32, now_secs: f64, death_net: &mut DeathNet) { death_net.reclaim_at = Some(now_secs + f64::from(delay_ms) / 1000.0); diff --git a/crates/benilla-app/src/debug_panel/inspect.rs b/crates/benilla-app/src/debug_panel/inspect.rs index 7afb780c..07c62921 100644 --- a/crates/benilla-app/src/debug_panel/inspect.rs +++ b/crates/benilla-app/src/debug_panel/inspect.rs @@ -166,10 +166,11 @@ pub(super) struct InspectStores<'w, 's> { /// through the publish, so without this the card can show an object the game is not hovering /// at all and give no sign of the difference (2246). hovered_go: Res<'w, crate::target::HoveredObject>, - /// The GameObject **animation** readout (decision 1151) — what the §243 arm is playing right - /// now, for the card's `anim` line. Its own query rather than a `collision` member because it - /// needs the model components: they sit on the same entity as [`crate::go_anim::GoAnim`], but - /// a GO whose model authors no skeleton renders as a static mesh and has none of them. + /// The GameObject **animation** readout (decision 1151) — what the state machine's arm + /// (`0x5f3930`) is playing right now, for the card's `anim` line. Its own query rather than a + /// `collision` member because it needs the model components: they sit on the same entity as + /// [`crate::go_anim::GoAnim`], but a GO whose model authors no skeleton renders as a static + /// mesh and has none of them. go_anims: Query< 'w, 's, @@ -405,7 +406,7 @@ pub(super) fn inspect_ui( } else { format!(" [{}]", named.join("|")) }; - // The **interact gate**, stated rather than inferred (wow-re cursor-system §4a): the + // The **interact gate**, stated rather than inferred (`0x5f2f80`): the // strategy vtable's `+0x14` **highlightable** slot is the single predicate behind the // cursor, the +64 brighten, the right-click USE and the pick priority — so a GO reading // `interact ✗` is saying all four are off *by design*, and one reading `interact ✓` while @@ -702,10 +703,11 @@ pub(super) fn inspect_ui( } }); // The same line for a **GameObject** (decision 1151), which is driven by `GoAnim` rather than - // `AnimDriver` and so never reached the branch above: the sequence the §243 arm is playing, - // whether it is a transient one (a transition motion / a Custom block — something the §2d - // completion advance must end) or the state's held rest pose, and the repeat it is running - // under. A `transition · loops` reading is the "stuck open/closing" bug, stated. + // `AnimDriver` and so never reached the branch above: the sequence the state machine's arm + // (`0x5f3930`) is playing, whether it is a transient one (a transition motion / a Custom block + // — something the completion advance `0x5f4120` must end) or the state's held rest pose, and + // the repeat it is running under. A `transition · loops` reading is the "stuck open/closing" + // bug, stated. let anim_line = anim_line.or_else(|| { let (id, transient, repeat) = net_entity .and_then(|p| go_anims.get(p).ok()) diff --git a/crates/benilla-app/src/debug_panel/mod.rs b/crates/benilla-app/src/debug_panel/mod.rs index bc399c76..de2166a8 100644 --- a/crates/benilla-app/src/debug_panel/mod.rs +++ b/crates/benilla-app/src/debug_panel/mod.rs @@ -482,8 +482,8 @@ fn debug_panel_ui( // UVs (phase 3 — waterfalls). let ticking = anim_hosts.iter().filter(|h| h.active).count(); // Of the material samplers, how many resolve to **0 right now** — the - // batches the reference culls this frame (`A <= 0`, wow-re - // `m2-alpha-combine-cull`). Non-zero as soon as a voidwalker/banshee/ + // batches the reference culls this frame (`A <= 0`, + // `0x707b3a`–`0x707b5c`). Non-zero as soon as a voidwalker/banshee/ // slime/infernal is in view: those models author geometry that only // appears on death, and this counter is what says we are hiding it // rather than drawing it. `dim` counts the partial factors — a batch diff --git a/crates/benilla-app/src/doodad_events.rs b/crates/benilla-app/src/doodad_events.rs index ea8c4bd3..ed4cc502 100644 --- a/crates/benilla-app/src/doodad_events.rs +++ b/crates/benilla-app/src/doodad_events.rs @@ -17,7 +17,7 @@ //! one `$DSL` → `NightElfStreetLampLoop` — are bind-posed, and decision 0130's content gate //! deliberately builds them no rig. That gate is right about pixels and was never a claim about the //! clock: the reference arms **every** placed doodad it creates and cycles it whenever the doodad is -//! in the frame's animate set (wow-re `doodad-anim-host.md` §1/§4a/§5b — see the gate below). So +//! in the frame's animate set (`0x695100` — see the gate below). So //! `doodad_anim::spawn_anim_host` gives a sound-carrying model a clock-only host — //! the arm bookkeeping, no player — and this scanner reads that arm. //! @@ -29,9 +29,7 @@ //! cull `0x683700` appended to, i.e. a doodad that passed frustum + horizon-occlusion **and** came //! out of the radius-tiered horizontal distance fade with `alpha > 0`. A doodad that fails any of //! those is not in the worklist, so its event track is never scanned, so **its `$DSL` never -//! fires and it never takes one of the emitter pool's 32 entries** (wow-re -//! `terrain/scratch/doodad-emitter-drawset-gate.md` §1b/§1c/§2b, `animation/scratch/ -//! doodad-anim-host.md` §5b, `object-layer/scratch/unit-anim-visibility-gate.md` §4). +//! fires and it never takes one of the emitter pool's 32 entries**. //! //! Ungated, this scanner registered **every resident doodad in the streaming radius**, and with the //! pool's four channels arbitrated by claim order (`super::sound::emitter_pool`) that is not a @@ -45,12 +43,11 @@ //! //! **"A campfire behind you keeps crackling" is still true, and this gate does not break it** — but //! by the other mechanism. A registration is released by `$DSE`, by a `$DSL` naming a different id, -//! or by the doodad's own teardown, and by nothing else (`sound/scratch/doodad-sound-emitters.md` -//! §9): turning away stops the *marker firing*, never the entry. The prose this module and +//! or by the doodad's own teardown, and by nothing else (the handler `0x6951e0`, the teardown +//! `0x6a0840`): turning away stops the *marker firing*, never the entry. The prose this module and //! [`DoodadAnimHost::arm_clock`] used to carry — "the reference gates the cycle on residency, not -//! the draw" — read `doodad-anim-host.md` §5b's *linkage* as "loaded" when the note means spliced -//! into `[scene+0x20]`, which **is** the drawn/faded set ("a doodad culled out of the drain stops -//! advancing and resumes on re-link"). +//! the draw" — read *linkage* as "loaded" when it means spliced into `[scene+0x20]`, which **is** +//! the drawn/faded set: a doodad culled out of the drain stops advancing and resumes on re-link. //! //! The clock stays [`DoodadAnimHost::arm_clock`]'s shared one rather than the `AnimationPlayer`'s: //! while a host is in the animate set the two agree by construction (the gate's resume seeks the @@ -62,12 +59,12 @@ //! `0x7074c2` advances every frame whether or not the model is linked; a write census over the //! whole function finds no persisted cursor. So the markers a culled doodad crossed are **lost**, //! and what actually fires on re-link is the completion watchdog re-arming at `startOffset = 0` -//! (wow-re `doodad-sound-emitters.md` §3, corrected 2026-09-07 by the §5 round decision 2065 -//! records). Keeping [`advance_track`]'s entry across the park would hand the resume frame a -//! `prev` from seconds ago and fire the whole span at once — harmless for a `$DSL` (its re-reach -//! only repositions) and a burst of one-shots for `$DSO`/`$SND`. Dropping it makes the resume an -//! **arm frame**, which fires nothing, and the frame after opens the head window `[0, cur]` — the -//! re-arm, in the shape this scanner already has. +//! (`0x6951b0`, as decision 2065 records). Keeping [`advance_track`]'s entry across the park +//! would hand the resume frame a `prev` from seconds ago and fire the whole span at once — +//! harmless for a `$DSL` (its re-reach only repositions) and a burst of one-shots for +//! `$DSO`/`$SND`. Dropping it makes the resume an **arm frame**, which fires nothing, and the +//! frame after opens the head window `[0, cur]` — the re-arm, in the shape this scanner already +//! has. use bevy::prelude::*; diff --git a/crates/benilla-app/src/entities.rs b/crates/benilla-app/src/entities.rs index a47894d7..e00a1a5d 100644 --- a/crates/benilla-app/src/entities.rs +++ b/crates/benilla-app/src/entities.rs @@ -135,17 +135,16 @@ pub(crate) use equipment::ItemModelKind; pub(crate) const ATTACH_OVERHEAD: u16 = 18; /// Its mounted twin (`PlayerNameMounted`, id 29) — preferred while a mount model is attached -/// (VERIFIED wow-re `questgiver-marker.md`/`nameplate-vkey.md`; decision 0441 P2), authored on -/// the RIDER's own model (character models seat it higher so overhead content clears the -/// mount's bulk). A rider model without it falls back to 18 like the client. +/// (`0x6074c0`, `0x608640`; decision 0441 P2), authored on the RIDER's own model (character +/// models seat it higher so overhead content clears the mount's bulk). A rider model without it +/// falls back to 18 like the client. pub(crate) const ATTACH_OVERHEAD_MOUNTED: u16 = 29; /// The overhead attachment slot to use for a unit **right now** — the reference's own pick inside -/// the marker attach `0x6074c0` (VERIFIED, wow-re object-layer `questgiver-marker.md` Q2), always -/// queried on the unit's *body* model: slot **29** when a mount MODEL exists (`unit+0xdc != 0` — -/// our [`mount::MountChild`]) *and* the body authors 29, else slot **18**; `None` when the body -/// authors neither, which is the reference's "marker created but never parented" (invisible) and -/// this file's bbox fallback for the overhead readers. +/// the marker attach `0x6074c0`, always queried on the unit's *body* model: slot **29** when a +/// mount MODEL exists (`unit+0xdc != 0` — our [`mount::MountChild`]) *and* the body authors 29, +/// else slot **18**; `None` when the body authors neither, which is the reference's "marker +/// created but never parented" (invisible) and this file's bbox fallback for the overhead readers. /// /// **The pick is LIVE, not a bake.** `0x6074c0`'s five call sites include `0x5ffae7` inside /// `0x5ffa50` — the `UNIT_FIELD_MOUNTDISPLAYID` field-watch handler (`0x604330 mov edx,0x1fc; mov @@ -173,12 +172,11 @@ const OVERHEAD_FALLBACK_FACTOR: f32 = 1.25; /// The reference's two overhead heights are two different mechanisms, and benilla had recorded them /// as one. The overhead NAME (and the floating combat text, and the V-plate) takes `0x608640`: the /// live posed PlayerName attachment, which tracks the pose. The chat bubble takes `0x711a20`, which -/// wow-re's cross-check followed into the model layer and found reading the **MD20 header image** — -/// file bytes, no bone matrix in the call tree — returning the Stand sequence CAaBox's Z extent, -/// and the client caches it in the bubble at `+0x354` on a parity guard so it is queried **once per -/// chat line**. The recorded claim that the two calls were equivalent ("both are the head-region -/// attachment height, model-scaled", INFERRED) is refuted: they differ precisely on -/// animated-vs-static. +/// reads the **MD20 header image** — file bytes, no bone matrix in the call tree — returning the +/// Stand sequence CAaBox's Z extent, and the client caches it in the bubble at `+0x354` on a parity +/// guard so it is queried **once per chat line**. The recorded claim that the two calls were +/// equivalent ("both are the head-region attachment height, model-scaled", INFERRED) is refuted: +/// they differ precisely on animated-vs-static. /// /// So this is a constant per display, stamped at attach and never re-read — which is also why the /// bubble's height cannot acquire a pose-clock bug of the kind 1398 had to remove from the anchor. @@ -220,8 +218,7 @@ pub(crate) struct OverheadFallback(pub(crate) f32); /// the UI glue), so its global IS its local. A `PostUpdate` caller ([`crate::nameplates`], which /// moved there for this very lag) is unaffected: after propagation the two are the same value. /// The client's **per-attachment z-bias** fallback table, `[0x862708]` — 37 floats, attachment -/// ids `0..=0x24`, read as `[0x862708 + 4·id]` under a `0 ≤ id < 0x25` guard (wow-re -/// `object-layer/scratch/anim-event-position-law.md` §4, §5-cross-checked). It is consulted only +/// ids `0..=0x24`, read as `[0x862708 + 4·id]` under a `0 ≤ id < 0x25` guard. It is consulted only /// when the model does **not** carry the attachment the caller asked for: the sound then plays at /// the unit's own position raised by this much, which is how a headless or attachment-less model /// still puts a mouth sound somewhere plausible instead of at its feet. @@ -243,9 +240,8 @@ const ATTACH_Z_BIAS: [f32; 37] = [ /// Two anim-event arms reach it and they are the reason this exists as a named function rather /// than inline at either: the emote voice `$CSD` asks for **17** (`0x623c3a push 0x11`), and a /// **whiffed** melee swing's `$CSS` asks for **1** (`0x624bdd`). Both are emphatically *not* the -/// fired event's own point, which is what makes them the exceptions in -/// `anim-event-position-law.md` §3's table — a player model's six head-mounted `$CSD` records do -/// not decide where that sound plays. +/// fired event's own point, which is what makes them the exceptions among the unit's anim-event +/// arms — a player model's six head-mounted `$CSD` records do not decide where that sound plays. #[derive(bevy::ecs::system::SystemParam)] pub(crate) struct AttachPoints<'w, 's> { anchors: Query<'w, 's, &'static BoneAttach>, @@ -417,13 +413,13 @@ impl Creatures { } /// A built display's booth framing — the model's own **authored cameras**, which is what both - /// booth families frame through: `camera` for the round portrait (`cameraLookup[0]`, wow-re - /// portrait-render §4) and `pane_camera` for a `` body pane (raw index 1, wow-re - /// `modelframe-camera-law.md`). Each carries the fallback data its own path needs when the model - /// has no such camera: the heuristic anchors (head bone / neck height / footprint) for the - /// portrait, the bbox centre for the pane's fixed camera. All model-local pre-scale. `None` - /// while the display's model is still loading (the booth's part source — the attach-spawned - /// children — won't exist yet either). + /// booth families frame through: `camera` for the round portrait (`cameraLookup[0]`, + /// `0x713540`) and `pane_camera` for a `` body pane (raw index 1, `0x505890`). + /// Each carries the fallback data its own path needs when the model has no such camera: the + /// heuristic anchors (head bone / neck height / footprint) for the portrait, the bbox centre + /// for the pane's fixed camera. All model-local pre-scale. `None` while the display's model is + /// still loading (the booth's part source — the attach-spawned children — won't exist yet + /// either). pub(crate) fn display_anchors( &self, display_id: u32, @@ -486,10 +482,10 @@ impl Creatures { } /// A built display's **booth rig** — what the portrait booth needs to pose a fresh instance at - /// Stand like the ref bake (wow-re portrait-render §4 D2: a throwaway instance armed to - /// Stand/seq-0, not the unit's live world pose): the rest skeleton, the shared inverse bind - /// poses, and the animation surface. `None` while the model is still loading; a boneless / - /// WMO-display model yields an empty skeleton (the booth then bakes the static bind pose). + /// Stand like the ref bake (`0x524f60`: a throwaway instance armed to Stand/seq-0, not the + /// unit's live world pose): the rest skeleton, the shared inverse bind poses, and the animation + /// surface. `None` while the model is still loading; a boneless / WMO-display model yields an + /// empty skeleton (the booth then bakes the static bind pose). pub(crate) fn display_rig(&self, display_id: u32) -> Option> { let dm = self.models.get(&display_id)?; dm.parts.as_ref()?; // not yet built @@ -946,7 +942,7 @@ impl Plugin for EntitiesPlugin { .after(benilla_world::rig_anim::finalize_rig_worlds) .after(benilla_world::particles::buffer::begin_effect_frame), ) - // Terrain conform (decisions 0482/0486, the byte law of wow-re `terrain-tilt.md`): + // Terrain conform (decisions 0482/0486, the byte law of `0x614cd0` → `0x7106c0`): // reads each flagged unit's Update-final transform, writes its conform node's // local rotation — before propagation so the composite's globals carry this // frame's stance. @@ -973,7 +969,7 @@ impl Plugin for EntitiesPlugin { Update, ( // The display's own base alpha first (the reference's DISPLAYID-watcher leg - // of the same recompute, `base-render-alpha.md` §5 — no aura needed), so a + // `0x604990` of the same recompute `0x60d180` — no aura needed), so a // same-frame aura edge retargets from the already-updated base. crate::aura_visual::refresh_base_alpha, crate::aura_visual::drain_aura_procs, @@ -1213,7 +1209,7 @@ fn update_display_models( // its model builds with no wire entity (the screens run pre-world, where no NetEntity carries it). glue_preview: Option>, char_create: Option>, - // The stable pane's want (wow-re `stable-master-window.md` §7.1): a stabled pet is a row in the + // The stable pane's want (`SetPetStablePaperdoll` `0x4cb870`): a stabled pet is a row in the // server's character-pet cache and a `CreatureDisplayInfo` id — there is no wire entity to // carry it, so the booth's own selection is the want. stable_booth: Option>, @@ -1459,10 +1455,10 @@ fn update_display_models( } /// Compose a unit part's **animated material alpha** into the render-alpha `MeshTag` field — the -/// unit-lane half of the verified per-batch combine `A = instanceAlpha × colourAlpha × weight` -/// (wow-re `m2-alpha-combine-cull.md`). The `A ≤ 0` *cull* is already the single `Visibility` -/// authority's (`debug_panel::apply_model_visibility` ANDs `mat_factor > 0`); this is the partial -/// factor, the dimming half, which only a `MeshTag` write can express. +/// unit-lane half of the per-batch combine `A = instanceAlpha × colourAlpha × weight` +/// (`0x707680`). The `A ≤ 0` *cull* is already the single `Visibility` authority's +/// (`debug_panel::apply_model_visibility` ANDs `mat_factor > 0`); this is the partial factor, the +/// dimming half, which only a `MeshTag` write can express. /// /// A fourth alpha writer is exactly what decision 0066's protocol forbids, so this is not one: it /// writes **only** the alpha field, through [`benilla_world::mesh_tag::with_alpha`] (the probe slot, @@ -1728,7 +1724,7 @@ mod overhead_slot_tests { } /// `0x6074c0`'s pick and only it: 29 is *preferred*, never required, and never taken by an - /// unmounted unit. So a character body — which authors both (wow-re's table: HumanMale 18 at + /// unmounted unit. So a character body — which authors both (the shipped HumanMale: 18 at /// z 2.2123, 29 at z 1.4029) — moves 18 ↔ 29 across a mount, a creature that authors only 18 /// (AncientOfLore, Kobold) stays at 18 whether or not it is mounted, and a body authoring /// neither anchors nothing: the reference creates the marker and never parents it. @@ -1765,8 +1761,7 @@ mod overhead_slot_tests { mod attach_bias_tests { use super::*; - /// The z-bias fallback table `[0x862708]`, verbatim (wow-re - /// `object-layer/scratch/anim-event-position-law.md` §4). It is 37 f32 for attachment ids + /// The z-bias fallback table `[0x862708]`, verbatim. It is 37 f32 for attachment ids /// `0..=0x24`, and the two ids that actually reach it through an animation event are the /// emote voice's **17** and a whiffed swing's **1** — so those two are what a drift here /// would move, on exactly the models that lack the attachment. diff --git a/crates/benilla-app/src/entities/attach/char_skin.rs b/crates/benilla-app/src/entities/attach/char_skin.rs index 7f88c80d..face5511 100644 --- a/crates/benilla-app/src/entities/attach/char_skin.rs +++ b/crates/benilla-app/src/entities/attach/char_skin.rs @@ -60,8 +60,8 @@ pub(super) fn resolve_char_look( // swaps a Player-kind entity can wear any display — a druid's bear form is a plain creature // model (no look; Monster skins instead), and a GM-morphed player wearing a humanoid NPC // display wears ITS CreatureDisplayInfoExtra appearance. The reference's own race/gender - // getters answer from the display's cached row with the descriptor as fallback (wow-re - // `w2d2.md`, the `0x60c690` getter family) — exactly this order: display appearance first, + // getters answer from the display's cached row with the descriptor as fallback (the + // `0x60c690` getter family) — exactly this order: display appearance first, // wire appearance only for a character body that carries none. let d = dm?; if let Some(npc) = d.npc_appearance.as_ref() { @@ -194,7 +194,7 @@ pub(super) fn resolve_worn_equip( } /// The worn geoset selectors for a set of equipment display ids (decisions 0074/1864's B1–B8 -/// branches + the cloak group + the helm's RF-0083 hide-mask row pair): each non-zero display +/// branches + the cloak group + the helm's `0x4799a0` hide-mask row pair): each non-zero display /// resolves its ItemDisplayInfo row's geoset columns, and B3's forearm gate comes off the same /// composite plan the atlas blits. One helper for the world attach path and the glue-preview /// builder — the selection law can't fork (decision 0465). diff --git a/crates/benilla-app/src/entities/attach/dress.rs b/crates/benilla-app/src/entities/attach/dress.rs index 93a7399f..9626d034 100644 --- a/crates/benilla-app/src/entities/attach/dress.rs +++ b/crates/benilla-app/src/entities/attach/dress.rs @@ -5,10 +5,10 @@ //! The reference never rebuilds a character to change what it is wearing: the composite atlas is //! re-blitted into the component's own 256² target and the geosets are re-selected on the SAME //! `CM2Model` (its per-instance visibility array `+0x98`, whose only writer is the character -//! compositor — wow-re `charactermodel.md` "Assembly orchestration" + `models.md` -//! §"geoset-visibility-default"). Its attachments — held items, the helm, the shoulders and -//! everything hanging off them — are never touched by that path at all. This module is the half of -//! that law that concerns one part: which materials it draws through, and how it is born. +//! compositor `0x477520`, through `0x7110d0`). Its attachments — held items, the helm, the +//! shoulders and everything hanging off them — are never touched by that path at all. This module +//! is the half of that law that concerns one part: which materials it draws through, and how it is +//! born. use benilla_formats::CharSkinSlot; use bevy::camera::primitives::Aabb; @@ -391,9 +391,9 @@ pub(super) fn spawn_part( blend: part.blend, }, // The portrait booth mirrors this part ([`crate::portrait`]): both mesh twins — the booth - // poses the skinned twin at Stand on its own throwaway skeleton (the ref bake, wow-re §4 - // D2), falling back to the static bind-pose twin for a boneless model — + the steady - // exterior material (not the appear-fade/interior variant the child may wear now). + // poses the skinned twin at Stand on its own throwaway skeleton (the ref bake `0x524f60`), + // falling back to the static bind-pose twin for a boneless model — + the steady exterior + // material (not the appear-fade/interior variant the child may wear now). crate::portrait::PortraitPart { static_mesh: part.mesh.clone(), skinned_mesh: part.skinned_mesh.clone(), @@ -432,11 +432,11 @@ pub(super) fn spawn_part( // object, never per batch, so the view cull belongs to the body ROOT's election // (`exterior_cull`, 1270) and `NoFrustumCulling` keeps Bevy's per-part test out of its // way. (0648 originally justified the marker as "the reference never view-culls entities - // at all", off a ≈1e7 render-bounds reading; wow-re's `outdoor-object-pass-election.md` - // refuted that 2026-08-13 — those fields are a position cache, and units ARE - // frustum/horizon/room-elected per frame. Decision 1473 records the correction and owns - // the outdoor half of the election.) Bevy's bind-pose `Aabb` is the wrong stand-in for - // picking (the duel flag plants itself 9 yd below it) and is stomped anyway by + // at all", off a ≈1e7 render-bounds reading, but those fields are a position cache + // (`0x6717d0`), and units ARE frustum/horizon/room-elected per frame (`0x683340`). + // Decision 1473 records the correction and owns the outdoor half of the election.) + // Bevy's bind-pose `Aabb` is the wrong stand-in for picking (the duel flag plants itself + // 9 yd below it) and is stomped anyway by // `calculate_bounds` on any `Mesh3d` change. So the `Aabb` beside the marker serves ONE // master: the mouseover picker (`target/hover.rs`) — the armed idle's authored CAaBox // when it has one, else the bind box, read from the part's build-time bound (decision @@ -456,8 +456,8 @@ pub(super) fn spawn_part( } // M2 parts can light off a WMO room they stand in: a `MeshTag` + the classifier pick the law by // location (0354). Anchored at the unit root so every part shares the root's verdict, and the - // indoor LAW is one for every entity M2 — the footprint-MOCV bake (wow-re - // `unit-m2-shader-light.md`), with the matte ×1.0 as the bake's miss fallback. + // indoor LAW is one for every entity M2 — the footprint-MOCV bake (`0x69e4c0`, `0x6a7300`), + // with the matte ×1.0 as the bake's miss fallback. if let Some(lit) = part_interior_lit( mats.steady, mats.interior, @@ -467,10 +467,10 @@ pub(super) fn spawn_part( ) { child.insert(lit); } - // The batch's **animated material alpha** (the verified combine's runtime half, wow-re - // `m2-alpha-combine-cull.md`): a creature's colour-alpha/transparency tracks are authored PER - // SEQUENCE, so which of its batches draw is a function of what it is playing. Sampling follows - // the unit's own `AnimationPlayer`, so the alpha stays in phase with the pose. + // The batch's **animated material alpha** (the combine's runtime half, `0x707680`): a + // creature's colour-alpha/transparency tracks are authored PER SEQUENCE, so which of its + // batches draw is a function of what it is playing. Sampling follows the unit's own + // `AnimationPlayer`, so the alpha stays in phase with the pose. if let Some(anim) = &part.alpha_anim { child.insert(benilla_world::doodad_anim::MatAnim::following( anim.clone(), diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs index 4ed7cac5..36ba7d9c 100644 --- a/crates/benilla-app/src/entities/attach/mod.rs +++ b/crates/benilla-app/src/entities/attach/mod.rs @@ -212,13 +212,12 @@ fn arm_sequence_clock( ) { // **Every** GameObject instance gets the loader-idle seed, state machine or not — the // reference's `0x70ebd0` tail arms bone 0 the moment the M2 goes LIVE and has exactly two - // callers, so no M2 instance in the client ever exists with nothing armed (wow-re - // `gameobject-anim-arm.md` §1/§2e). For a door/chest the object-layer arm lands *after* it - // and overrides it (§2, "because it lands after the loader seed, it is the effective arm"); - // seeding first is what stops the one-frame BIND POSE our state GOs used to render on their - // first displayed frame, before `go_anim` had a chance to run — the "explodes for a split - // second" report. The seed is played THROUGH the transitions object so that first arm - // cleanly fades out of it; playing it bare on the player would leave two clips live at once. + // callers, so no M2 instance in the client ever exists with nothing armed. For a door/chest + // the object-layer arm (`0x5f3930`) lands *after* it and overrides it; seeding first is what + // stops the one-frame BIND POSE our state GOs used to render on their first displayed frame, + // before `go_anim` had a chance to run — the "explodes for a split second" report. The seed + // is played THROUGH the transitions object so that first arm cleanly fades out of it; + // playing it bare on the player would leave two clips live at once. let mut player = AnimationPlayer::default(); let mut transitions = AnimationTransitions::new(); if kind == EntityKind::GameObject { @@ -228,9 +227,9 @@ fn arm_sequence_clock( // emitters ended up reading file slot 0 at t = 0 for ever (0941). if let Some(clip) = anims.idle_clip() { // Loop iff the sequence says so (`M2Sequence.flags & 1 == 0`) — the kernel's own - // end-of-band law (wow-re `gameobject-anim-arm.md` §2, byte-verified at - // `0x714585`): bit0 clear loops on the modulo wrap, bit0 set plays the window and - // then FREEZES at `end_ms`. An unconditional repeat replayed one-shot idles. + // end-of-band law (`0x714585`): bit0 clear loops on the modulo wrap, bit0 set plays + // the window and then FREEZES at `end_ms`. An unconditional repeat replayed one-shot + // idles. let active = transitions.play(&mut player, clip.node, std::time::Duration::ZERO); if clip.looping { active.repeat(); @@ -451,7 +450,7 @@ pub(super) fn attach_entity_visuals( // carry a *transparent* placeholder M2, and invisible **trigger creatures** an empty or // constant-zero-alpha one: the real client's mesh gate is **type-independent** — it draws // any loaded model and the per-batch zero-alpha cull skips the transparent geometry - // (decision 0024, superseding 0023's wrong marker-type gate; verified wow-re go-render-gate). + // (decision 0024, superseding 0023's wrong marker-type gate; zero-alpha cull `0x707b3a`). // Our M2 alpha cull already reduces those models to zero submeshes, so `parts` below is // EMPTY for them: the spawn loop draws nothing, and `named_a_model` is what keeps the unit // arm's debug cube off them (1403). @@ -523,9 +522,9 @@ pub(super) fn attach_entity_visuals( .insert_if_new(benilla_world::entity_shade::GroundShade::default()); // The root's canonical fold reference: held items share the root's interior verdict // (one light node per unit — the reference aliases the wearer's collector into each - // equipped item, wow-re `unit-light-combine-storm.md`), and their classifier fold must - // reference the BODY's centre, not the carried position. Plain `insert`: a display-id - // change re-derives it with the new body model. + // equipped item, `0x718960`), and their classifier fold must reference the BODY's + // centre, not the carried position. Plain `insert`: a display-id change re-derives it + // with the new body model. commands .entity(entity) .insert(benilla_world::interior::BodyBakeCenter(bake_center)); @@ -561,8 +560,8 @@ pub(super) fn attach_entity_visuals( // mesh did). Truly static props keep the static mesh. `skin` is `Some((joints, // inverse_bindposes, palette_slot))` when instanced (decision 0720; slot 0 = palette // full, parts fall back to the static mesh). - // Does this entity run the GameObject open/close state machine (the byte-verified - // TYPE_ID census)? Read once — it picks both the rig flavour below and the clock's + // Does this entity run the GameObject open/close state machine (the type dispatch at + // `0x5f76cc`)? Read once — it picks both the rig flavour below and the clock's // driver, and those two must never disagree about the same object. let go_state_machine = net.kind == EntityKind::GameObject && stores @@ -632,10 +631,10 @@ pub(super) fn attach_entity_visuals( // skinned twin like a creature. Two flavours share the rig: a door/button/chest // (`go_animates`) runs the open/close state machine off GAMEOBJECT_STATE (decision // 0242); ANY other animated GO — a mailbox's wind-swung flags, a banner, a windmill — - // loops its first sequence as the reference's universal loader-idle seed (wow-re - // `doodad-anim-host.md`: a non-transport CGGameObject animates identically to a placed - // doodad). The content gate for the non-state flavour is the doodad classifier's: a GO - // whose first sequence is a constant pose and which has no global sequences + // loops its first sequence as the reference's universal loader-idle seed + // (`0x70ebd0`: a non-transport CGGameObject animates identically to a placed + // doodad). The content gate for the non-state flavour is the doodad classifier's: + // a GO whose first sequence is a constant pose and which has no global sequences // (`DoodadAnimTier::Static`) has nothing to loop, so it keeps the static mesh. (EntityKind::GameObject, Some(d)) if !d.skeleton.joints.is_empty() && d.animations.is_some() => @@ -730,7 +729,7 @@ pub(super) fn attach_entity_visuals( let look = resolve_char_look(net, dm, entity, &stores); // The worn geoset selectors (decisions 0074/1864, the B1–B8 branches): a player's from the // resolved equipment display rows; an NPC / naked default otherwise. - // (The helm's hide-mask row pair, RF-0083: hair/facial/ears tuck under it. For a + // (The helm's hide-mask row pair, `0x4799a0`: hair/facial/ears tuck under it. For a // character-model NPC the helm id is its CreatureDisplayInfoExtra head column.) let equip_geosets = equip_geosets( displays.as_deref(), @@ -913,8 +912,8 @@ pub(super) fn attach_entity_visuals( // Final size = the server's per-object scale (`OBJECT_FIELD_SCALE_X`) alone. The server // already folds the unit's DBC scale (`CreatureModelData.modelScale × // CreatureDisplayInfo.scale`, or an explicit per-spawn override) into this field, and the - // real client renders units at the field alone (verified: wow-re `world_model_scale` - // `0x613ef0`, vmangos `Unit::GetScaleForDisplayId`). Multiplying our own DBC scale on top + // real client renders units at the field alone (`0x613ef0`, vmangos + // `Unit::GetScaleForDisplayId`). Multiplying our own DBC scale on top // double-applies it — `native²`, worst for the sub-1.0 starting-zone scales. A GameObject's // display scale was always 1.0, so this is unchanged for it. let placement = if let Ok(mut t) = transforms.get_mut(entity) { @@ -1087,7 +1086,7 @@ pub(super) fn attach_entity_visuals( // A display that DID name a model renders exactly what that model built — including // nothing (decision 1403, bug B13). The reference's mesh gate is type-independent: it // draws any loaded model and the per-batch zero-alpha cull skips what is transparent - // (decision 0024, verified wow-re `go-render-gate`), so a model with no surviving batch + // (decision 0024, zero-alpha cull `0x707b3a`), so a model with no surviving batch // submits no geometry and the unit is invisible. Byte-exact, folded back at 1407: the // batch loop's TRIP COUNT is the selected ModelView's texUnit count (`0x707a72`), so a // view with none skips the whole loop — and the unit lane reaches that same loop, on diff --git a/crates/benilla-app/src/entities/attach/preview.rs b/crates/benilla-app/src/entities/attach/preview.rs index f7f1e4fe..ec84f9b9 100644 --- a/crates/benilla-app/src/entities/attach/preview.rs +++ b/crates/benilla-app/src/entities/attach/preview.rs @@ -21,8 +21,8 @@ //! The assembly is the world's own way — displayId → the shared [`Creatures`] display cache → //! geoset filter → the composited body over a hand-built [`CharLook`] — plus the armor-region //! composite + real equipment geosets, the cloak, and the attach riders: helm (per-race/sex model, -//! hide-helm honored), shoulders, and the weapons **held in the hands** (the byte-verified select -//! build — wow-re `glue-select-model.md`, folded back over 0465's sheathed INTERIM). The portrait +//! hide-helm honored), shoulders, and the weapons **held in the hands** (the select build +//! `0x472950` → `0x47a0c0`, folded back over 0465's sheathed INTERIM). The portrait //! module's `sync_glue_booth` / `sync_dressup_booth` then re-light and bake them (they own the //! *booth*; this owns the *assembly*). A glue body's displayId is added to //! `update_display_models`'s want-list (in [`super::super`]) so its model builds with no wire @@ -64,7 +64,7 @@ const ENUM_HELD: [usize; 3] = [15, 16, 17]; /// Map an item's `InventoryType` to its equipment-slot index in the enum-shaped `[_; 19]` array /// (`EQUIPMENT_SLOT_*`), so a CharStartOutfit item (decision 0527) lands where the Select pipeline /// reads it — the render slots the compositor paints (helm 0 · shoulder 2 · shirt 3 · chest 4 · -/// waist 5 · legs 6 · feet 7 · wrist 8 · hands 9 · back 14 · tabard 18, wow-re `glue-select-model.md`) +/// waist 5 · legs 6 · feet 7 · wrist 8 · hands 9 · back 14 · tabard 18, `0x478cb0`) /// plus the held triple (main 15 · off 16 · ranged 17). `None` for a non-worn / non-rendered type /// (bags, ammo, quiver, relic, and NON_EQUIP consumables — the outfit's food + hearthstone). pub(crate) fn equip_slot(inv_type: u8) -> Option { @@ -175,10 +175,10 @@ pub(in crate::entities) fn build_glue_preview( mut skin_composites: ResMut, asset_server: Res, mut mats: benilla_world::model_render::M2BatchMaterials, - // The ghost kit (wow-re `glue-select-model.md` §A2): the `SpellVisualKit` catalog the row is - // read from, and the path-keyed spell-effect model cache its attached models load through — - // the same one the world's kit plays use, so `Spells\Ghost_state.mdx` is loaded once per - // session whichever screen wants it first. + // The ghost kit (`0x47280f`): the `SpellVisualKit` catalog the row is read from, and the + // path-keyed spell-effect model cache its attached models load through — the same one the + // world's kit plays use, so `Spells\Ghost_state.mdx` is loaded once per session whichever + // screen wants it first. visuals: Option>, mut fx: Option>, // Change tracking: the look we last emitted a bake for, and whether that bake succeeded (so a @@ -462,11 +462,11 @@ pub(in crate::entities) fn build_dressup_preview( /// A caller must treat that as "retry next frame" and leave its bake untouched, never as an empty /// look — a geared character popping in piecewise would flicker on every change. /// The `SpellVisualKit` row the character-select ghost is, **hard-coded exactly as the reference -/// hard-codes it**: `0x47280f` loads `idmap[989]` behind the guard `0x4727f6 cmp ds:0xc0d750,0x3dd` -/// (wow-re `glue-select-model.md` §A2). The id is the constant; every value behind it is DBC data, +/// hard-codes it**: `0x47280f` loads `idmap[989]` behind the guard +/// `0x4727f6 cmp ds:0xc0d750,0x3dd`. The id is the constant; every value behind it is DBC data, /// read below. 989 is `SpellVisual` 886's state kit — the visual of spell 8326 "Ghost", which is -/// also what a released ghost in the WORLD rides, so the two paths share one row by construction -/// (wow-re `ghost-death-visuals.md` §5a). +/// also what a released ghost in the WORLD rides (`0x5ff350`), so the two paths share one row by +/// construction. const GLUE_GHOST_KIT: u32 = 989; /// What the ghost kit contributes to a select bake: the body's own render properties, plus the @@ -623,9 +623,9 @@ fn assemble(spec: &PreviewSpec, ctx: &mut PreviewCtx<'_, '_>) -> Option) -> Option) -> Option { } /// The conform rotation for a smoothed up-vector in the unit's LOCAL frame (yaw already applied -/// by the parent) — the `0x7106c0` flag dispatch, byte-exact (wow-re `terrain-tilt.md`): +/// by the parent) — the `0x7106c0` flag dispatch, byte-exact: /// /// - **flag 1 (pitch only, `0x710769`)**: left is forced horizontal (`row1.z` a hard literal 0), /// forward = left × up — which reduces in the local frame to a pure rotation about local X by @@ -127,7 +127,7 @@ pub(super) fn conform_units( // a flagged NPC body over a mount field — and double-stepping would square the decay). if seen.insert(node.unit) { // A unit riding a FLYING spline settles to level here: the flying attitude (pitch, - // bank pending its §5) is the spline-follow's, applied to the UNIT transform by + // bank still open) is the spline-follow's, applied to the UNIT transform by // `sample_splines` — the client's `0x7c5490` mover-matrix law, model-flags- // independent (decision 0501 corrects 0496's conform-side placement, which never // rendered: the taxi gryphon authors `GlobalModelFlags = 0`, so it built no diff --git a/crates/benilla-app/src/entities/corpse.rs b/crates/benilla-app/src/entities/corpse.rs index 88852e69..44eae27e 100644 --- a/crates/benilla-app/src/entities/corpse.rs +++ b/crates/benilla-app/src/entities/corpse.rs @@ -28,7 +28,7 @@ //! Three slots are carved out of the dress loop, and none of them is a "skip if empty": //! `0x5d6465`/`0x5d6470` skip slot 0 (head) and slot 0xe (back) when this corpse's own //! `CORPSE_FLAG_HIDE_HELM 0x08` / `HIDE_CLOAK 0x10` are set — its own bits on its own field, and the -//! opposite instruction polarity to the player lane (wow-re `helm-cloak-hide.md` §2b) — and +//! opposite instruction polarity to the player lane (`0x5ed700`) — and //! `0x5d644e` skips slot 0x11 (ranged) unconditionally. //! //! ## What a corpse deliberately does NOT wear @@ -55,7 +55,7 @@ //! call.) Mouseover is a different slot — `+0x54` = `0x5d76d0`, which a corpse *does* pass — and //! that is the highlight/cursor/tooltip route we ship, not a ring. //! -//! **The divergence to know about** (flagged open by wow-re's §5, not settled at the bytes): the +//! **The divergence to know about** (still open, not settled at the bytes): the //! reference computes the drowned verdict *inside* the create, from the scene node's cached liquid //! probe — and its own `[node+0x90] & 0x20` gate means an un-probed node falls through to `Dead`. //! Whether the node is already probed that early is a byte question nobody has closed. Ours asks @@ -66,11 +66,10 @@ //! ## The pose //! //! `0x5d63de`/`0x5d6402` arm bone 0 through the shared M2 arm `0x7121a0` with **AnimationData id 6 -//! (`Dead`)**, or **132 (`Drowned`)** when `0x5d6540` says so — all of it byte-VERIFIED by wow-re's -//! §5 round (`object-layer/scratch/corpse-drowned-pose.md`, 2026-08-29, commissioned by this work). -//! That predicate is a **liquid** query: `0x670630` reads the corpse scene node's cached probe, -//! whose `[node+0x98]` is named at its *writer* (`0x69e280` → terrain's own bit-exact -//! `liquid_status`) as the **liquid surface height**. The comparison is +//! (`Dead`)**, or **132 (`Drowned`)** when `0x5d6540` says so. That predicate is a **liquid** +//! query: `0x670630` reads the corpse scene node's cached probe, whose `[node+0x98]` is named at +//! its *writer* (`0x69e280` → the terrain liquid query `0x69b6d0`) as the **liquid surface +//! height**. The comparison is //! //! ```text //! liquidSurfaceZ − CORPSE_FIELD_POS_Z > [0x80abfc] // 0x3f2aaaab = 0.66666669 diff --git a/crates/benilla-app/src/entities/dest_fx.rs b/crates/benilla-app/src/entities/dest_fx.rs index 21d4f03b..6b20b1c7 100644 --- a/crates/benilla-app/src/entities/dest_fx.rs +++ b/crates/benilla-app/src/entities/dest_fx.rs @@ -3,8 +3,8 @@ //! //! - **The DynamicObject machine** ([`arm_ground_effects`]): a TYPEID-6 create is the anchor a //! persistent area effect hangs on (Blizzard's storm, Flamestrike's burn). The reference -//! builds **two disjoint visuals** (wow-re `dynobject-visual-machine.md` — never through the -//! unit-kit pipeline; all 15 `PlaySpellVisualKit` call sites censused, none on this class): +//! builds **two disjoint visuals**, never through the unit-kit pipeline (none of the 15 +//! `PlaySpellVisualKit` `0x60edf0` call sites is on this class): //! - **Visual A** — the object's own `.mdx`: SPELLID → `Spell.dbc` SpellVisual → //! `SpellVisual` field 11 ≠ 0 gate → field 12 → `SpellVisualEffectName` field 2 path //! (`0x5d57c0`), instanced at the object's position **verbatim** (no terrain projection), @@ -21,9 +21,9 @@ //! `SMSG_SPELL_GO` itself plays the field-12 model ONCE at the packet's dest point when //! `SpellVisual` field 6 == 0 (no missile owns the arrival) — `0x6e8088`–`0x6e8143`, a //! self-terminating CEffect. Flamestrike's initial burst; it fires **before** the dynobj -//! create arrives and must not wait for it (wow-re trap #6). +//! create arrives and must not wait for it (a trap). //! -//! **Teardown is a tail, not a snap** (trap #3): `SMSG_DESTROY_OBJECT` despawns the anchor — +//! **Teardown is a tail, not a snap** (a trap): `SMSG_DESTROY_OBJECT` despawns the anchor — //! visual A and the emitter die with it (the ref zeroes the emit rate at `0x6ecf20`) — but the //! already-emitted shards are FREE entities that run out their own one-pass lifetimes, exactly //! the ref's "spawned particles finish". The looping sound dies via the sound module's @@ -44,8 +44,8 @@ use benilla_protocol::EntityKind; use super::spell_fx::{attach_effect_visuals, ensure_model, FxMaterials, SpellFx}; -/// The client's hardcoded shard-model table (`0x870e24`, 7 entries — wow-re -/// `dynobject-visual-machine.md` Q-A1). `CharParamZero`'s decoded small int indexes it. +/// The client's hardcoded shard-model table (`0x870e24`, 7 entries). `CharParamZero`'s decoded +/// small int indexes it. const SHARD_MODELS: [&str; 7] = [ "Spells\\Blizzard_Impact_Base.mdx", "Spells\\RainOfFire_Impact_Base.mdx", @@ -64,8 +64,8 @@ const PROC_TYPE_SHARD_EMITTER: i32 = 9; /// The exact small-int decode the client applies to `CharParamZero` /// ([`benilla_formats::char_proc_small_int`] — the one idiom every integer-in-a-float-column proc /// uses, the chain proc included; decision 0955 lifted it into the format crate) — clamped into -/// [`SHARD_MODELS`] because the client itself has **no bounds check** (`mov cl,al` — data ≥ 7 -/// reads past the table; wow-re trap #2). +/// [`SHARD_MODELS`] because the client itself has **no bounds check** (`mov cl,al` in `0x5d55c0` +/// — data ≥ 7 reads past the table; a trap). fn shard_model_index(param0: f32) -> usize { let idx = benilla_formats::char_proc_small_int(param0) as usize; idx.min(SHARD_MODELS.len() - 1) @@ -213,7 +213,7 @@ pub(super) fn arm_ground_effects( } /// Spawn the router's GO dest one-shots — free entities at the packet's point, one sequence -/// pass, then gone. Fired at the GO, never waiting on the dynobj create (trap #6). +/// pass, then gone. Fired at the GO, never waiting on the dynobj create (a trap). pub(super) fn spawn_ground_bursts( mut commands: Commands, mut bursts: MessageReader, @@ -345,8 +345,8 @@ mod tests { /// The `0x5d55c0` decode: `bits(f32(param0 + 512.0)) >> 14 & 0xff` recovers the small int /// exactly (the real rows carry 0.0 → Blizzard, 1.0 → Rain of Fire), and out-of-table data - /// clamps instead of reading past the 7 entries (the client's own missing bounds check — - /// wow-re trap #2). + /// clamps instead of reading past the 7 entries (the client's own missing bounds check — a + /// trap). #[test] fn shard_model_index_decodes_and_clamps() { assert_eq!(shard_model_index(0.0), 0); diff --git a/crates/benilla-app/src/entities/display.rs b/crates/benilla-app/src/entities/display.rs index dd39efca..dc019880 100644 --- a/crates/benilla-app/src/entities/display.rs +++ b/crates/benilla-app/src/entities/display.rs @@ -57,8 +57,8 @@ pub(super) struct EntityPart { /// The interior BAKE variant (every M2 part): the material in interior-PROP mode, whose /// `MeshTag` payload the shader reads as an SH-probe slot — the footprint-MOCV law. Units and /// GameObjects alike consume it: the reference registers EVERY entity M2 with the same - /// entity-node fill (`Node::SetModel`), so one law lights them all indoors (wow-re - /// `unit-m2-shader-light.md`, superseding the 0315 unit/GO split). `None` for WMO parts. + /// entity-node fill (`Node::SetModel` `0x6716f0`), so one law lights them all indoors + /// (`0x6a7300`, superseding the 0315 unit/GO split). `None` for WMO parts. pub(super) material_interior_bake: Option>, /// The bake variant's own `AlphaMode::Blend` twin — a fade (the self-avatar zoom feather, a /// despawn ramp) on a bake-classified part rides THIS, keeping the probe light through the @@ -69,7 +69,7 @@ pub(super) struct EntityPart { /// (creatures + M2 GameObjects, the CGObjects that appear-fade); `None` for WMO-display parts. pub(super) fade_blend: Option>, /// The **depth-prime twin** material ([`benilla_world::model_render::zfill_material`] — the reference's - /// `M2UseZFill` clone, wow-re `m2-blend-promotion-zfill.md` §4). While the part draws + /// `M2UseZFill` clone, `0x707f7d`–`0x708072`). While the part draws /// translucent, `model_fade::sync_zfill_twins` spawns a child mesh on this: colour-masked, /// blend-off, z-writing, sorted before the colour parts — one blended layer everywhere, no /// self-overlap darkening on a stealthed or fading body. `None` when the batch's material @@ -96,10 +96,10 @@ pub(super) struct EntityPart { /// Plenty of them do not: 101 creature models author several (`Creature\Banshee` carries `0` /// and `402`). They still draw whole, because the reference's per-model visibility array is /// allocated **filled with `1`** — all visible — and the only writer is the character - /// compositor, which a creature never reaches (VERIFIED, wow-re `models.md` §"M2 geoset - /// visibility": the `CCharacterComponent` is created on exactly two guarded paths and a - /// creature fails both). That array is indexed by **submesh ordinal**, not by this id — the - /// client never decomposes a `group*100 + variant` id anywhere in its render band. + /// compositor, which a creature never reaches (`0x5fb200`: the `CCharacterComponent` is + /// created on exactly two guarded paths and a creature fails both). That array is indexed by + /// **submesh ordinal**, not by this id — the client never decomposes a `group*100 + variant` id + /// anywhere in its render band. pub(super) geoset_id: u16, /// The character runtime texture slot this part carries (M2 type 1 = body, type 6 = hair). For a /// player the per-entity attach swaps its material to one carrying that per-appearance texture @@ -134,7 +134,7 @@ pub(super) struct EntityPart { /// only ever seed white: the hunter's Freezing Trap glow card is the class (decision 2295). pub(super) rgb_seq: Option>>, /// The part's **texture-transform (UV) loop** — the translation track that scrolls this - /// batch's stage UVs (decision 0130 phase 3, wow-re `m2-texanim-uv`). The **effect lane** + /// batch's stage UVs (decision 0130 phase 3, `0x70b740`). The **effect lane** /// ([`super::spell_fx`]) samples it per instance on its own clip clock, through a material /// clone (decision 2282); the unit/GameObject lane runs it on the shared, deduped material /// (decision 2295). `None` for the ~98.6% of batches with no texture transform, and for all @@ -238,7 +238,7 @@ pub(crate) struct DisplayModel { pub(crate) first_seq_span: Option, /// Camera framing-pivot height in **model-local yards, pre-scale** — `0.9 × bbox_z_extent` from the /// M2 authored bounds, captured in [`build_parts`]. Stamped onto each instance as [`CameraPivot`] so - /// the third-person camera targets ~neck height rather than a fixed offset (wow-re `follow-camera`). + /// the third-person camera targets ~neck height rather than a fixed offset (`0x50cbc0`). /// `0.0` for a bounds-less / WMO / model-less display (→ the camera floors it). pub(super) pivot_height_local: f32, /// How far that framing pivot drops while this body **swims**, model-local yards, pre-scale — @@ -251,25 +251,25 @@ pub(crate) struct DisplayModel { pub(super) swim_pivot_drop_local: f32, /// The target selection-ring radius in **model-local yards, pre-scale**: the Stand-animation footprint /// `sqrt(0.5 · sqrt(dx² + dy²))` ([`M2Bounds::ring_footprint`]). The ring's world radius is this × the - /// unit's `OBJECT_FIELD_SCALE_X` (wow-re selection-ring RE, `0x608e00`/`0x60aee0`, emulated to the + /// unit's `OBJECT_FIELD_SCALE_X` (`0x608e00`/`0x60aee0`, emulated to the /// reference pixels). `0.0` for a bounds-less / WMO / model-less display (the ring then uses a fallback). pub(super) ground_radius_local: f32, - /// The model's authored **portrait camera** (wow-re portrait-render §4), captured with `parts` on + /// The model's authored **portrait camera** (`0x713540`), captured with `parts` on /// load — the exact rig the portrait booth frames through. `None` for WMO / model-less / the few /// camera-less M2s (the booth then falls back to heuristic framing). pub(super) portrait_camera: Option, /// The model's **model-frame pane camera** — raw camera-table index 1, the rig a 1.12 - /// `` widget renders through (wow-re `ui/scratch/modelframe-camera-law.md`; + /// `` widget renders through (`0x505890`; /// [`benilla_assets::M2Model::pane_camera`]). Captured with `parts` on load. `None` for WMO / /// model-less / a model with fewer than two cameras — the body booth then uses the client's own /// FIXED fallback camera instead. pub(super) pane_camera: Option, - /// A bow display's `$WTT`/`$WTB` bowstring anchors (wow-re `nocked-ammo-cancel.md` §G2), + /// A bow display's `$WTT`/`$WTB` bowstring anchors (`0x611ff0`), /// captured with `parts`: `[top, bottom]` as `(bone, model-local Bevy position)`. The held-item /// attach marks the prop root with them so the string drawer can span the tips. `None` for /// every non-bow model. pub(super) string_anchors: Option<[(u16, Vec3); 2]>, - /// The fishing pole's `$CCH` line anchor (wow-re `fishing-line.md`), mesh-frame Bevy space, + /// The fishing pole's `$CCH` line anchor (`0x61f780`), mesh-frame Bevy space, /// captured with `parts`. The held-item attach marks the mainhand prop with it so the line /// drawer can span rod tip → bobber. `None` for every model that doesn't author it. pub(super) cch_marker: Option, @@ -278,7 +278,7 @@ pub(crate) struct DisplayModel { /// text at `feet + scale × this × 1.25`). `0.0` for a bounds-less display. pub(super) bbox_z_local: f32, /// The **Stand animation** box's z-extent (model-local yards, pre-scale) — the CHAT BUBBLE's - /// anchor height (`0x711a20`, wow-re 2026-08-17; 1406). Distinct from `bbox_z_local` above, + /// anchor height (`0x711a20`; 1406). Distinct from `bbox_z_local` above, /// which is the all-animation vertex box, and from the posed PlayerName attachment the overhead /// NAME uses: this one is a file constant, queried once and cached, so the bubble's height /// cannot move with the pose. `0.0` for a bounds-less display. @@ -287,16 +287,15 @@ pub(crate) struct DisplayModel { /// reference point for a GameObject's footprint bake (the byte-cited `[def+0x5c]` anchor /// family; `benilla_world::interior`). `Vec3::ZERO` for a bounds-less / WMO / model-less display. pub(super) bake_center_local: Vec3, - /// The model's terrain-conform gate — MD20 `GlobalModelFlags & 3` (wow-re `terrain-tilt.md`, - /// §5): `1` = pitch to slope, `3` = pitch+roll, else level. Captured with `parts` on load; + /// The model's terrain-conform gate — MD20 `GlobalModelFlags & 3` (`0x7106c0`): `1` = pitch to + /// slope, `3` = pitch+roll, else level. Captured with `parts` on load; /// `0` for WMO / model-less / still-loading displays. pub(super) terrain_tilt: u8, /// Whether this display resolves to a **character body** (a `Character\…` model path) — the /// gate for the char-customization pipeline (geoset filter + skin composite). The look follows /// the DISPLAY, not the entity kind (decision 0695): a druid in bear form is a Player-kind /// entity wearing a plain creature model, and the reference's own race/gender getters answer - /// from the display's cached row, not the unit's descriptor (wow-re `w2d2.md`'s `0x60c690` - /// getter family). + /// from the display's cached row, not the unit's descriptor (the `0x60c690` getter family). pub(super) is_character_body: bool, } @@ -476,13 +475,13 @@ pub(super) fn build_parts( // Target selection-ring radius (model-local, pre-scale): the **Stand-animation footprint** // `sqrt(0.5 × sqrt(dx² + dy²))` ([`M2Bounds::ring_footprint`], dx/dy = the Stand sequence // box's horizontal extents). This is the exact model-local input the real client's living-unit - // ring uses (wow-re selection-ring RE, `0x608e00`/`0x60aee0` — byte-verified + emulated to the + // ring uses (`0x608e00`/`0x60aee0`, emulated to the // reference pixels); the ring's world radius = this × OBJECT_FIELD_SCALE_X. NOT the render // sphere (`0xCC`) — that is the *corpse* decal's source (`0x5d6fe0`); the nested-sqrt footprint // is why the sphere never fit (it over-sized tall humans, under-sized the squat chicken). ground_radius_local = model.bounds.map_or(0.0, |b| b.ring_footprint); // Camera framing-pivot height (model-local, pre-scale), the reference's follow-camera target - // (`0x50cbc0`, wow-re `follow-camera`): **attachment id 17's Z + 0.0972** when present (every + // (`0x50cbc0`): **attachment id 17's Z + 0.0972** when present (every // character model has it → ~neck height, e.g. 1.90 human / 0.88 gnome), else the fallback // `0.9 × vertex-box Z-extent` for a non-character model. The vertex box alone is the WRONG // source for characters — it's the all-animation extent (a human's is ~3.8, way over the diff --git a/crates/benilla-app/src/entities/equipment/mod.rs b/crates/benilla-app/src/entities/equipment/mod.rs index 82ed4e7f..1921ab37 100644 --- a/crates/benilla-app/src/entities/equipment/mod.rs +++ b/crates/benilla-app/src/entities/equipment/mod.rs @@ -14,7 +14,7 @@ //! - **Attach** ([`attach_held_items`]) — spawn each resolved item's model as a child of the body's //! **attach-point joint entity** (via [`BoneAttach`], inserted by the visual attach), so it rides the //! hand/hip/back bone through every animation — the modern analogue of the client's attach-transform -//! install (`0x47a380`, wow-re charactermodel). The item model itself is a static mesh: its origin +//! install (`0x47a380`). The item model itself is a static mesh: its origin //! *is* the grip, aligned by the attach bone's animated frame. //! //! Item models cache per **item display id** in [`ItemDisplays`] (a [`DisplayModel`] like creatures/ @@ -79,13 +79,13 @@ pub(crate) mod attach_id { pub(crate) const HIP_MAIN: u16 = 32; pub(crate) const HIP_OFF: u16 = 33; /// HandArrow (35, bone 126 — flag-0x04 ignore-parent-rotation) — the in-hand nocked arrow's - /// ONE body-bone attach (wow-re `nocked-ammo-cancel.md` §E2: `0x712f70(body, 0x23)` from - /// `0x60ba30`/the `$BWP` BowPull handler; bow/wand only). The old Special2/Special3 (0x18/ - /// 0x19) reading is REFUTED — those are the `0x479f40` model-DIRECTORY selectors - /// (`Item\ObjectComponents\Ammo\` vs `…\Weapon\`), never attach ids (§E1). + /// ONE body-bone attach (`0x712f70(body, 0x23)` from `0x60ba30`/the `$BWP` BowPull handler; + /// bow/wand only). The old Special2/Special3 (0x18/0x19) reading is REFUTED — those are the + /// `0x479f40` model-DIRECTORY selectors (`Item\ObjectComponents\Ammo\` vs `…\Weapon\`), never + /// attach ids. pub(crate) const HAND_ARROW: u16 = 0x23; - /// The quiver-on-back attach (wow-re §H2, byte-verified 3×): the worn ammo container's - /// model parents at M2 attachment id 26 — the same point the stowed two-hander family uses. + /// The quiver-on-back attach (`0x479c50`): the worn ammo container's model parents at M2 + /// attachment id 26 — the same point the stowed two-hander family uses. pub(crate) const QUIVER: u16 = 26; } @@ -126,7 +126,7 @@ pub(crate) struct Equipment { /// The back slot's display id (0 = no cloak) — a geoset + runtime cape texture, no body region. pub(crate) cloak: u32, /// The head slot's display id (0 = no helm) — an attach sub-model, plus the HelmetGeosetVisData - /// hide-masks that tuck hair/facial/ears under it (wow-re RF-0083). + /// hide-masks that tuck hair/facial/ears under it (`0x4799a0`). pub(crate) helm: u32, /// The wearer's guild tabard (decision 1704) — the emblem five, joined off their own PUBLIC /// `PLAYER_GUILDID` against the guild identity cache. `None` for a guildless player **and** @@ -137,7 +137,7 @@ pub(crate) struct Equipment { /// wears*: the composite key includes it, so the re-dress diff below is the one place a change /// of guild, of tabard, and a late-arriving identity all converge — no second latch. pub(crate) emblem: Option, - /// The tabard designer's preview (decision 1977, wow-re RF-0089 §7c `[cc+0xc]`): set on the + /// The tabard designer's preview (decision 1977, `[cc+0xc]` via `0x5e07d0`): set on the /// LOCAL player's body while the designer is open, so the body wears the tabard geoset over an /// empty slot and the composite paints `emblem` (the design under preview) onto it. pub(crate) tabard_preview: bool, @@ -183,13 +183,13 @@ pub(super) struct HeldItems { } /// Total attach sub-model slots: the 3 held + helm + shoulder L/R + the nocked ammo + the -/// worn quiver (self-only, ranged-drawn — wow-re `nocked-ammo-cancel.md` §H). +/// worn quiver (self-only, ranged-drawn — `0x611e10`). pub(super) const ATTACH_SLOTS: usize = 8; -/// **What the disarm reflex left attached** (decision 1863; wow-re `disarm-weapon-gate-law.md` -/// §6, byte-verified). The reference's attachment state is built by *events*, not recomputed from -/// the descriptor, and disarm is where that difference becomes visible — so this is the one piece -/// of latched attachment state the equipment layer keeps. +/// **What the disarm reflex left attached** (decision 1863). The reference's attachment state is +/// built by *events*, not recomputed from the descriptor, and disarm is where that difference +/// becomes visible — so this is the one piece of latched attachment state the equipment layer +/// keeps. /// /// `UNIT_FIELD_FLAGS`' change reflex `0x5ff580` runs when the DISARM bit **changes** /// (`0x5ff619 test edi,eax`), for observed remote units as well as our own body, and when the bit @@ -279,7 +279,7 @@ pub(crate) enum ItemModelKind { /// rule (its module docs): a display with `model[0]` is a thrown weapon in `Weapon\`; one /// with only `model[1]` is an arrow/bullet in `Ammo\`. Ammo, - /// The worn ammo container on the back (wow-re §H2): `Quiver\model[0]` + its own + /// The worn ammo container on the back (`0x479c50`): `Quiver\model[0]` + its own /// `model_texture[0]` skin, parented at attachment 26 while the ranged weapon is drawn. Quiver, } diff --git a/crates/benilla-app/src/entities/equipment/resolve.rs b/crates/benilla-app/src/entities/equipment/resolve.rs index 92f032ba..17061b20 100644 --- a/crates/benilla-app/src/entities/equipment/resolve.rs +++ b/crates/benilla-app/src/entities/equipment/resolve.rs @@ -27,12 +27,11 @@ use super::{ /// Drawn: the unit's sheath state (`UNIT_FIELD_BYTES_2` byte 0: 0 stowed · 1 melee · 2 ranged) draws /// the matching slots into the hands (shield → forearm). Stowed: the **item's** sheath type picks the /// body point — 1 two-hander → back · 2 staff → lower back · 3 one-hander → hip · 4 shield → centre -/// back (mainhand takes the `K−1` side of each pair — byte-verified: `0x47a070`'s `dl != 0` is the -/// mainhand bodyslot `0xf`, wow-re `ranged-sheath-display.md`, decision 0370). A sheathed ranged -/// weapon renders **nothing** — the client detaches it rather than re-pointing it to a body bone -/// (byte-verified `0x7130a0`: a pure unlink/release, uniform across bow/gun/crossbow/thrown/wand; -/// wow-re `ranged-sheath-display.md`). Drawn ranged splits by inventory type: a **bow** rides the -/// left hand, gun/crossbow/wand/thrown the right (`0x611e10`'s invType test, same note). +/// back (mainhand takes the `K−1` side of each pair — `0x47a070`'s `dl != 0` is the mainhand +/// bodyslot `0xf`, decision 0370). A sheathed ranged weapon renders **nothing** — the client +/// detaches it rather than re-pointing it to a body bone (`0x7130a0`: a pure unlink/release, +/// uniform across bow/gun/crossbow/thrown/wand). Drawn ranged splits by inventory type: a **bow** +/// rides the left hand, gun/crossbow/wand/thrown the right (`0x611e10`'s invType test). pub(in crate::entities) fn placement( slot: usize, inv_type: u32, @@ -88,10 +87,10 @@ fn enchant_fold( .fold(0i32, |acc, e| acc.wrapping_mul(31).wrapping_add(e as i32)) } -/// The nocked ammo's attach point (wow-re `nocked-ammo-cancel.md` §E2/E5, byte-verified): the -/// ONE body-bone attach in the whole mechanism is HandArrow (35), fired for a **bow** once its -/// BowPull event latches `[+0xd58]&0x4000` — `nock_latched` is [`NockLatch`], driven by the real -/// `$BWP`/`$BWR` listener (`drive_nock_latch`, decision 0408). Everything else shows NO nocked +/// The nocked ammo's attach point (`0x60ba30`): the ONE body-bone attach in the whole mechanism is +/// HandArrow (35), fired for a **bow** once its BowPull event latches `[+0xd58]&0x4000` — +/// `nock_latched` is [`NockLatch`], driven by the real `$BWP`/`$BWR` listener +/// (`drive_nock_latch`, decision 0408). Everything else shows NO nocked /// model: gun/crossbow hit the client's `gunXbow` early return, thrown resolves the `0x19` /// *directory* (its own weapon-model copy) but fails the `==0x18` attach gate, and a wand's /// Shoot has no ammo item. @@ -139,7 +138,7 @@ pub(in crate::entities) struct ResolveKey { /// The **dress** half of the same resolve — [`ResolveKey`]'s complement, and the reason it is a /// separate component: a model widget's duplicate is re-taken on a *dress* change and not on a -/// *placement* one (wow-re `ui/scratch/paperdoll-liveness-law.md`; `crate::portrait::SnapKey`). +/// *placement* one (`0x5dee30`; `crate::portrait::SnapKey`). /// /// Everything here is read **before** [`placement`], which is the whole point. The three weapon /// slots are the only ones whose very *presence* a sheath state can decide — a stowed ranged @@ -288,12 +287,12 @@ pub(in crate::entities) fn resolve_equipment( // The suppression is a **display id of zero**, applied below to every place the piece is // resolved — which is the same thing "no helm equipped" means everywhere else in this // module, so the whole downstream chain follows for free: the helm's attach sub-model is not - // requested, its RF-0083 HelmetGeosetVisData hide-masks are not applied (hair, facial hair - // and ears come back), the cloak's geoset group is not selected and its cape texture is not - // resolved. It is also the shape the glue lane has honoured since 0465 (`attach::preview` - // zeroes the same two slots off the char-enum record's `CHARACTER_FLAG_HIDE_*`, which - // vmangos round-trips into these very bits at login) — the world was the half that never - // consumed it. + // requested, its `0x4799a0` HelmetGeosetVisData hide-masks are not applied (hair, facial + // hair and ears come back), the cloak's geoset group is not selected and its cape texture + // is not resolved. It is also the shape the glue lane has honoured since 0465 + // (`attach::preview` zeroes the same two slots off the char-enum record's + // `CHARACTER_FLAG_HIDE_*`, which vmangos round-trips into these very bits at login) — the + // world was the half that never consumed it. let (hide_helm, hide_cloak) = (s.player_hides_helm(), s.player_hides_cloak()); // Worn armor (players; decision 0074): resolve the composite slots' entries → display ids. // `settled` only once every non-empty entry has an answer, so the first attach composites the @@ -323,8 +322,9 @@ pub(in crate::entities) fn resolve_equipment( None => eq.settled = false, } } - // The helm (equipment slot 0): attach model + the RF-0083 hide-masks (its geoset effect - // — a hair/facial/ears change — rides the Equipment diff, so donning one re-attaches). + // The helm (equipment slot 0): attach model + the `0x4799a0` hide-masks (its geoset + // effect — a hair/facial/ears change — rides the Equipment diff, so donning one + // re-attaches). if let Some(entry) = s.player_visible_item_entry(0).filter(|e| *e != 0) { match templates.held(entry, &net) { Some(t) => eq.helm = t.display_info_id, @@ -369,7 +369,7 @@ pub(in crate::entities) fn resolve_equipment( }; // A player wearing a NON-character display (druid form, GM morph — decision 0695) // attaches no equipment sub-models at all: the reference's held/helm/shoulder attach - // lives on the CCharacterComponent (`0x47a0c0`, wow-re charactermodel node), which only + // lives on the CCharacterComponent (`0x47a0c0`), which only // a character body builds — a bear-form druid shows no weapon by construction, not by a // hide flag. [`Wielded`] (the anim-class pair) still resolves below: what's IN the hand // is independent of whether its model is displayed. The creature virtual-item path (a @@ -558,8 +558,7 @@ pub(in crate::entities) fn resolve_equipment( if current_dress != Some(&dress) { commands.entity(entity).insert(dress); } - // The nocked ammo (byte-verified `0x60ba30` + the Q-E round, wow-re - // `nocked-ammo-cancel.md` §E2/E5): the ONE body-bone attach in the whole mechanism is + // The nocked ammo (`0x60ba30`): the ONE body-bone attach in the whole mechanism is // HandArrow (35), **bow-only** — gun/crossbow (`gunXbow` early return) and thrown // (directory selector `0x19`, never an attach id) show NO nocked model. The [`NockedAmmo`] // display is written per shot from `SMSG_SPELL_START`, any caster; the attach follows the @@ -594,13 +593,13 @@ pub(in crate::entities) fn resolve_equipment( visual, }); } - // The quiver on the back (wow-re `nocked-ammo-cancel.md` §H, byte-verified): while the - // RANGED weapon is drawn — the same `0x611e10` ranged-draw transition, cleared on every - // other ranged state — the client scans the player's OWN inventory for an ItemClass-11 - // container (Quiver/Ammo Pouch) and parents its display model at attachment 26 (no - // transform override; no cloak conflict). Self-only by construction, exactly like the - // client: bag slots are never replicated in 1.12, so a remote player's scan finds - // nothing (§H1 — a two-client capture would be the clean confirmation). + // The quiver on the back: while the RANGED weapon is drawn — the same `0x611e10` + // ranged-draw transition, cleared on every other ranged state — the client scans the + // player's OWN inventory for an ItemClass-11 container (Quiver/Ammo Pouch) and parents its + // display model at attachment 26 (no transform override; no cloak conflict). Self-only by + // construction, exactly like the client: bag slots are never replicated in 1.12, so a + // remote player's scan finds nothing (a two-client capture would be the clean + // confirmation). // Timed off the RANGED slot's own visual state, so the quiver arrives with the bow it // feeds. (Named deviation: the ref attaches it at the *start* of the ranged draw — the // `0x611e10(1)` call inside each drawer — where ours waits for that clip's $SHL. Same @@ -692,9 +691,9 @@ pub(in crate::entities) fn resolve_equipment( } } let next = HeldItems { slots }; - // Per-hand grip: a weapon in a hand's attach point curls that hand's fingers (wow-re - // `hand-grip-mechanism.md`) — mainhand → right (id 1), non-shield offhand → left (id 2); a - // forearm shield (id 0) or an empty hand stays open. Drives [`HandGrip`]'s finger overlay. + // Per-hand grip: a weapon in a hand's attach point curls that hand's fingers (`0x60b590`) + // — mainhand → right (id 1), non-shield offhand → left (id 2); a forearm shield (id 0) or + // an empty hand stays open. Drives [`HandGrip`]'s finger overlay. let grip = HandGrip { right: next .slots @@ -740,10 +739,9 @@ pub(in crate::entities) fn resolve_equipment( /// **Three slots never dress**, and the two shapes are different: /// - slot 0 (head) when this corpse's own `CORPSE_FLAG_HIDE_HELM 0x08` is set, and slot 14 (back) /// when `HIDE_CLOAK 0x10` is (`0x5d6465`/`0x5d6470` — its own bits on its own field, snapshotted -/// from `PLAYER_FLAGS` at death; wow-re `helm-cloak-hide.md` §2b). Suppression is a display id of -/// **zero**, the same shape the player lane uses (decision 1472), so the whole downstream chain — -/// the helm's attach model, its RF-0083 hide-masks, the cloak's geoset and cape texture — follows -/// for free. +/// from `PLAYER_FLAGS` at death). Suppression is a display id of **zero**, the same shape the +/// player lane uses (decision 1472), so the whole downstream chain — the helm's attach model, its +/// `0x4799a0` hide-masks, the cloak's geoset and cape texture — follows for free. /// - slots 15/16/17 (mainhand, offhand, ranged) — **always**. Ranged is skipped outright /// (`0x5d644e`); the two weapon slots take a branch that looks up the packed item word as an /// *object guid* (`0x5d649b` → `0x468460`, typemask 2) and can therefore never resolve. A corpse @@ -802,7 +800,7 @@ pub(in crate::entities) fn resolve_corpse_equipment( // The guild tabard crest, from the corpse's OWN `CORPSE_FIELD_GUILD` snapshot — the // reference's `0x5d6ec0`, reached from the dress loop at the tabard slot (`ebx == 0x12` // with that display's `ItemDisplayInfo` flag bit 0) and resolved through the same - // name cache a living body's is (wow-re `corpse-decal-and-loot-sparkle.md` §6b). A + // name cache a living body's is (`0x6d6d20`). A // bone pile builds no character component, so it never reaches this leg — which is // exactly where this sits. eq.emblem = guilds @@ -996,8 +994,8 @@ mod tests { assert_eq!(placement(0, 21, 3, 1), Some(attach_id::HAND_RIGHT)); } - /// The nocked-ammo attach law (`0x60ba30`, wow-re `nocked-ammo-cancel.md` §E2, decision - /// 0408): HandArrow (35) is the ONE attach, bow-only, gated on the `$BWP` nock latch. + /// The nocked-ammo attach law (`0x60ba30`, decision 0408): HandArrow (35) is the ONE attach, + /// bow-only, gated on the `$BWP` nock latch. /// Gun/crossbow/thrown never attach a nocked model. #[test] fn ammo_attach_hands_the_volleying_bow_arrow_and_nothing_else() { @@ -1076,7 +1074,7 @@ mod tests { /// **B123** (decision 1472): the two equipment-display preferences are consumed on the WORLD /// body, not only on the character-select one. `PLAYER_FLAGS_HIDE_HELM 0x400` / /// `HIDE_CLOAK 0x800` zero the resolved display id, which is what makes every downstream - /// consumer follow — no cape geoset, no cape texture, no helm attach model, and no RF-0083 + /// consumer follow — no cape geoset, no cape texture, no helm attach model, and no `0x4799a0` /// hide-mask stripping the hair. #[test] fn the_hide_preferences_undress_the_helm_and_cloak_on_a_world_body() { @@ -1177,13 +1175,12 @@ mod tests { drop(rx); } - /// **What a disarm does to the weapon MODEL** (decision 1863; wow-re - /// `disarm-weapon-gate-law.md` §6). The reference's attachment state is built by events, and - /// `UNIT_FIELD_FLAGS`' change reflex `0x5ff580` is one of them: when the DISARM bit goes up it - /// calls `0x47a310(model, 0xf, 0, 0)` — an unlink of the main-hand attachment — **but only - /// while the weapon is drawn**, and afterwards every attach site declines because its - /// `GetWeapon(slot, 0)` reads NULL. So the weapon does NOT stay in the fist (the older note - /// said it did, from a partial read of `0x60b590`), and it does NOT vanish off the back + /// **What a disarm does to the weapon MODEL** (decision 1863). The reference's attachment + /// state is built by events, and `UNIT_FIELD_FLAGS`' change reflex `0x5ff580` is one of them: + /// when the DISARM bit goes up it calls `0x47a310(model, 0xf, 0, 0)` — an unlink of the + /// main-hand attachment — **but only while the weapon is drawn**, and afterwards every attach + /// site declines because its `GetWeapon(slot, 0)` reads NULL. So the weapon does NOT stay in + /// the fist (as `0x60b590` read alone would suggest), and it does NOT vanish off the back /// either. Three cases, one body each. #[test] fn a_disarm_takes_a_drawn_weapon_off_the_hand_and_leaves_a_stowed_one() { diff --git a/crates/benilla-app/src/entities/equipment/spawn.rs b/crates/benilla-app/src/entities/equipment/spawn.rs index d69b8123..edb108a3 100644 --- a/crates/benilla-app/src/entities/equipment/spawn.rs +++ b/crates/benilla-app/src/entities/equipment/spawn.rs @@ -117,13 +117,13 @@ impl SeatWriters<'_, '_> { /// popping in opaque (decision 0032 read as a per-unit property). /// /// **The diff is per SLOT, and an attach-point change is a MOVE** (decision 0826). The reference's -/// sheath paths touch only the weapon/quiver attach ids (`0x611770`, wow-re `sheath-policy.md` / -/// `ranged-sheath-display.md`) and stow a melee weapon by detaching the sub-model and **re-parenting -/// it** at the sheath point (`0x60b590` → `0x712f70`) — the model instance, and everything riding -/// it, survives the swap. Rebuilding a unit's whole kit on any change did neither: drawing a sword -/// respawned the shoulders' and helm's emitters too, and every orphaned pool then lived out its -/// lifespan FROZEN in world space (`particles::sim`'s drain) — the sparkle cloud that hung behind -/// the character on every weapon draw and every step of the login gear cascade. +/// sheath paths touch only the weapon/quiver attach ids (`0x611770`) and stow a melee weapon by +/// detaching the sub-model and **re-parenting it** at the sheath point (`0x60b590` → `0x712f70`) — +/// the model instance, and everything riding it, survives the swap. Rebuilding a unit's whole kit +/// on any change did neither: drawing a sword respawned the shoulders' and helm's emitters too, and +/// every orphaned pool then lived out its lifespan FROZEN in world space (`particles::sim`'s drain) +/// — the sparkle cloud that hung behind the character on every weapon draw and every step of the +/// login gear cascade. #[allow(clippy::type_complexity)] pub(in crate::entities) fn attach_held_items( mut commands: Commands, @@ -345,7 +345,7 @@ fn spawn_slot( // left-hand fork (a bow is the one ranged weapon placed in HAND_LEFT; the client // registers the string callback bow-only, from the ranged-draw path). The `$WTT`/ // `$WTB` anchors alone are NOT the gate — they are generic weapon-TRAIL begin/end - // markers (wow-re w2d2: `WTBT`/`WTTT`, the swing-trail vertex build) that melee + // markers (`WTBT`/`WTTT`, the swing-trail vertex build `0x6c67f0`) that melee // weapons author too; keying on their presence drew a phantom "bowstring" chord // across the Whirlwind Axe's blade tips (decision 0531). if slot_idx == 2 && hs.attach == attach_id::HAND_LEFT { @@ -358,7 +358,7 @@ fn spawn_slot( }); } } - // The **weapon swing trail's** object (wow-re `charproc8-weapon-trail.md` §9, decision 2076) + // The **weapon swing trail's** object (decision 2076) // — the reference's `WTOBJECT`, built per weapon HAND by `0x608d60` on the model it finds at // that hand's attachment and freed with it. This is the first-class consumer decision 0531 // named and deferred: the `$WTB`/`$WTT` pair is a *trail* marker, and the bowstring above is @@ -371,7 +371,7 @@ fn spawn_slot( .insert(crate::weapon_trail::WeaponTrail::new(top.1, bottom.1)); } } - // The fishing line's near anchor (wow-re `fishing-line.md`, decision 1099): a MAINHAND prop + // The fishing line's near anchor (`0x61f780`, decision 1099): a MAINHAND prop // whose model authors `$CCH` is the pole (the reference gates on ItemCache {class 2, subclass // 20} + the marker's presence; exactly one weapon model in the chain authors the marker, so // presence alone is data-equivalent — unlike the bow's trail-marker trap above, which is why @@ -404,13 +404,13 @@ fn spawn_slot( // attach model runs a joint palette. 0847 pulled it believing a spherical billboard swept the // spikes through the plate; that was wrong (0853: the spikes run ALONG their bone, worst vertex // 12° off axis, so the arc never existed), and the byte answer is that the reference billboards - // an attached model exactly as it does a standalone one (wow-re `billboard-bone-law.md` §6). + // an attached model exactly as it does a standalone one (`0x718657`–`0x71876f`). // A // display whose geometry is WELDED to a billboard bone (`welds_billboard`: the R14 pauldron's // two spikes, whose root rings are 50/50 with the plate) has no correct rigid placement at all; // 0839 stopped tearing it into a card, which left it whole and *still*. The reference bends it - // by blending the billboard bone's camera-replaced palette row per vertex (`m2_vertex_skin`, - // `0x71a460`) — so we build exactly that: the joint hierarchy, a palette slot, and the + // by blending the billboard bone's camera-replaced palette row per vertex (`0x71a460`) — so + // we build exactly that: the joint hierarchy, a palette slot, and the // [`benilla_world::billboard::BillboardJointRig`] that rewrites the billboard joints' world rotations. // // Nothing else about the item lane's "rests at bind pose" law moves: no `AnimationPlayer`, no @@ -595,10 +595,9 @@ fn spawn_slot( // yields via its own `Without`/`Without` filter, same // as a body part. // Anchored at the WEARER's root: an equipped item M2 aliases its wearer's - // light collector by pointer (`[item+0x3b8]=[wearer+0x3b8]`, `0x718960` — - // wow-re `unit-light-combine-storm.md`), so it never runs its own - // classify/footprint at the carried position. The animating hand joint - // once anchored these, and a swing alone could trip the resample gate and + // light collector by pointer (`[item+0x3b8]=[wearer+0x3b8]`, `0x718960`), so it + // never runs its own classify/footprint at the carried position. The animating hand + // joint once anchored these, and a swing alone could trip the resample gate and // split the shield's light from the body's (director-caught, 2026-07-13). // The fold reference is the wearer's BODY centre for the same reason. // @@ -627,11 +626,11 @@ fn spawn_slot( ) { child.insert(lit); } - // The batch's authored **material alpha** — the verified combine's `colourAlpha × - // weight` (wow-re `m2-alpha-combine-cull.md`). An attach model spawns no rig of its - // own and rests in its file's first sequence, so its loops are PINNED there (the - // doodad lane's clock) while the tag compose stays the unit lane's, ordered against - // the appear-fade and the interior classifier exactly like the wearer's own batches. + // The batch's authored **material alpha** — the combine's `colourAlpha × weight` + // (`0x707680`). An attach model spawns no rig of its own and rests in its file's first + // sequence, so its loops are PINNED there (the doodad lane's clock) while the tag + // compose stays the unit lane's, ordered against the appear-fade and the interior + // classifier exactly like the wearer's own batches. // Skipping this drew every one of the 321 item models that dim a batch at full // strength — the Hungering Cold's five glow cards blaze at 1.0 where the file says // 0.30 (decision 0836). @@ -1175,8 +1174,7 @@ mod tests { /// /// 0847 withdrew this and pinned the withdrawal with the mirror of this test; both are gone /// again. The withdrawal's premise — a spherical billboard sweeping the spikes through the - /// plate — was refuted at the geometry (0853) and at the bytes (wow-re - /// `billboard-bone-law.md` §6), and + /// plate — was refuted at the geometry (0853) and at the bytes (`0x718657`–`0x71876f`), and /// [`benilla_world::billboard::tests::a_spike_along_its_bone_axis_points_screen_down_from_every_angle`] /// is the standing guard that the arc stays absent in our own basis. #[test] diff --git a/crates/benilla-app/src/entities/item_glow.rs b/crates/benilla-app/src/entities/item_glow.rs index 246e6a32..93719fd8 100644 --- a/crates/benilla-app/src/entities/item_glow.rs +++ b/crates/benilla-app/src/entities/item_glow.rs @@ -7,8 +7,7 @@ //! `ItemDisplayInfo` col 22 → `ItemVisuals` → 5 × `ItemVisualEffects` → a glow `.mdx` per slot //! ([`benilla_formats::ItemVisualCatalog`], whose module doc owns the table shapes and the //! reference's per-slot skip rules). Two sources feed the id, and the reference resolves exactly -//! one of them per item (wow-re `object-layer/scratch/item-visual-enchant.md`, byte-verified; -//! the call sites below were read at the bytes for decision 0805): +//! one of them per item (the call sites below were read at the bytes for decision 0805): //! //! - **Intrinsic** — the display's own id, passed by the character's held-item attach: //! `0x47a200: mov edx,[esi+0x58]` → `0x4798c0(…, itemVisualsId)`, which calls the attach diff --git a/crates/benilla-app/src/entities/live_display.rs b/crates/benilla-app/src/entities/live_display.rs index f86c37f4..848410a0 100644 --- a/crates/benilla-app/src/entities/live_display.rs +++ b/crates/benilla-app/src/entities/live_display.rs @@ -10,8 +10,7 @@ //! from the new display (`0x60afb0 ResolveDisplayInfo`), and re-selects the stand/ride animation //! (`0x60ce70`) — an **instant** swap, no morph transition (the ghost→alive revive swap rides the //! same path; a shapeshift's green flash is the spell visual kit, a separate system). The SCALE_X -//! handler instead **eases the render scale over 2 s with a cosine smoothstep** (byte-verified, -//! `0x614bbf`). All wow-re: `questgiver-marker.md` §W6, `w2d2.md` §2.x, `object-layer.md`. +//! handler instead **eases the render scale over 2 s with a cosine smoothstep** (`0x614bbf`). //! //! Our shape is a diff-and-rebuild: the visual was BUILT with [`AppliedDisplay`], the live truth //! is the descriptor store, and a difference tears the visual down for `attach_entity_visuals` to @@ -47,7 +46,7 @@ pub(super) struct AppliedDisplay(pub(super) Option); /// A live display-id swap happened on this entity — the visual was torn down and will rebuild. /// The reference's rebuild `0x60abe0` ends by REPLAYING the pending-morph latch's impact kit -/// (wow-re `shapeshift-morph-cloud.md`); this edge is what carries "the rebuild ran" to that +/// (`0x60ad67`); this edge is what carries "the rebuild ran" to that /// replay (`crate::creature_anim`'s morph-latch watcher), which owns the latch and the kit /// resolve. Written only when the rebuild actually fires (the diff branch — our `0x60ae10`). #[derive(Message, Clone, Copy)] diff --git a/crates/benilla-app/src/entities/missile.rs b/crates/benilla-app/src/entities/missile.rs index da0af1f7..29015251 100644 --- a/crates/benilla-app/src/entities/missile.rs +++ b/crates/benilla-app/src/entities/missile.rs @@ -21,7 +21,7 @@ //! arrives on the spot — at melee range there is no visible flight or trail at all, just the //! impact on the target, exactly the reference's close-range look. //! -//! In flight, the client's **arrive-on-time** mover (`0x61ceb0`/`0x61e2a0`, wow-re `w2f1.md`): +//! In flight, the client's **arrive-on-time** mover (`0x61ceb0`/`0x61e2a0`): //! one entity per target, model parts + particle emitters from the shared [`SpellFx`] path-keyed //! cache (the same held-items pattern as the attach-point effects); each frame the missile covers //! `remaining distance / remaining time · dt` toward the target's **destination attach point** — @@ -30,10 +30,9 @@ //! hand-off `0x61dc50`); arrival on a missed target plays the victim's dodge/block defense clip //! instead ([`miss_defense_state`]) and floats the outcome WORD ([`MissileMiss`], decision 2229). //! -//! **`0x61e1d0` picks between THREE arms, and the selector is not the one an earlier reading -//! recorded** (wow-re `missile-arrival-dispatch.md`; it is not ground-vs-unit): first whether the -//! missile's target guid still resolves to a live object, then a **HIT bit** — `[missile+0x38] & -//! 1`, written once image-wide at the spawn `0x60a4e8` from an explicit argument. +//! **`0x61e1d0` picks between THREE arms, and the selector is not ground-vs-unit**: first whether +//! the missile's target guid still resolves to a live object, then a **HIT bit** — +//! `[missile+0x38] & 1`, written once image-wide at the spawn `0x60a4e8` from an explicit argument. //! //! | arm | reached when | what it does | ours | //! |---|---|---|---| @@ -72,14 +71,14 @@ //! despawn in every case.//! //! A GO with **no unit targets at all** but a point on the wire flies **one** projectile at the //! point — the client's location fallback (`0x6e8a50`'s empty-hit-array arm → `0x60a3d0` once, -//! owning unit slot −1; wow-re `spell-go-dest-effect.md` §3). That is the whole visible flight of +//! owning unit slot −1). That is the whole visible flight of //! a pure ground cast: the hunter's Flare arcing out to where it was placed, a bomb thrown at //! empty dirt. Such a missile homes to nothing — its aim is a fixed world point ([`Aim::Ground`]) //! — and it arrives as [`CastEventKind::GroundImpact`] on the CASTER (`0x61d870`, the //! no-live-target arm — see the dispatch table above), never through the unit hand-off. Named approximation: it flies the same straight //! arrive-on-time line a unit missile does; the reference's trajectory *class* comes from -//! `CMissile+0x48` through `0x61d720`'s remap table, whose wire origin wow-re traced to -//! consumption only — so a lobbed shot reads as a straight glide here. +//! `CMissile+0x48` through `0x61d720`'s remap table, whose wire origin is still open — so a lobbed +//! shot reads as a straight glide here. use bevy::animation::transition::AnimationTransitions; use bevy::ecs::entity::EntityHashMap; @@ -124,7 +123,7 @@ const DEST_FALLBACKS: [u16; 2] = [0xf, 0x13]; const INFLIGHT_ANIM: u16 = 144; /// The missile's orientation for flight direction `f` (Bevy space) — the client's exact frame -/// build (`0x61e2a0`, wow-re `w2f1.md`): model **+X = the flight direction**, side = up × dir, +/// build (`0x61e2a0`): model **+X = the flight direction**, side = up × dir, /// up re-orthogonalized as dir × side — i.e. NO roll, model-up stays world-up-ish however the /// flight pitches. (A shortest-arc rotation rolls on pitched flight, which would tilt the trail /// ribbons' authored + cross.) In Bevy terms: local −Z (the wow_to_bevy image of wow +X) → `f`, @@ -141,7 +140,7 @@ fn missile_facing(f: Vec3) -> Quat { Quat::from_mat3(&Mat3::from_cols(f.cross(up), up, -f)) } -/// The projectile's in-flight LOOP sound (`SpellVisual` field 10, wow-re `w2f1.md`'s +/// The projectile's in-flight LOOP sound (`SpellVisual` field 10, the reference's /// `CMissile+0x44` loop handle): a channel tracked to the missile entity — it follows the /// projectile as it flies and is reaped when the projectile arrives or streams out. Written by /// [`spawn_missiles`]/[`move_missiles`]; consumed by `crate::sound::missile`. @@ -173,7 +172,7 @@ enum Aim { /// `None` — the spell landed: arrival plays the impact hand-off. `Some(code)` — the wire's /// `SpellMissInfo` miss: arrival instead plays the victim's defense clip for DODGE(3)/ /// BLOCK(5) (the client's `Missile_C::Update 0x61ceb0` dispatch — Dodge 30 / ShieldBlock - /// 24, never Parry; wow-re `smsg-attackerstate-consequences.md` §Q4) and no impact kit. + /// 24, never Parry) and no impact kit. miss: Option, }, /// A fixed world point — the GO's dest, with no unit to home to and nothing that can make the @@ -294,12 +293,11 @@ struct QueuedGo { /// Every caster's pending queue (the client's per-unit `+0xac` list heads). A caster that /// streams out drops its queue with it. /// -/// **It is also a GATE, not only a queue** (decision 2288, wow-re -/// `missile-queue-gates-the-release-event.md`). `[CGUnit+0xac]` holds `CMissile` nodes — the -/// binary names them itself (`Missile_C.cpp`) — inserted by the missile spawner `0x60a3d0` and -/// drained by the release event, and the `$BWR` handler reads it *before* draining it -/// (`0x600182 mov eax,[esi+0xac]; test eax,eax; je 0x600299`). Everything between those two points -/// — the ranged prop's own re-anim and the cast-sound reposition — happens **only when a +/// **It is also a GATE, not only a queue** (decision 2288). `[CGUnit+0xac]` holds `CMissile` +/// nodes — the binary names them itself (`Missile_C.cpp`) — inserted by the missile spawner +/// `0x60a3d0` and drained by the release event, and the `$BWR` handler reads it *before* draining +/// it (`0x600182 mov eax,[esi+0xac]; test eax,eax; je 0x600299`). Everything between those two +/// points — the ranged prop's own re-anim and the cast-sound reposition — happens **only when a /// projectile is actually waiting to be released**. So a shot's flex and its launch are two /// effects of one act, and a consumer that arms the prop without asking this has half a mechanism. #[derive(Resource, Default)] @@ -422,8 +420,8 @@ fn launch_outcome_code(code: u8) -> u8 { /// `select::defense_anim`): DODGE(3) → the dodge state (Dodge 30), BLOCK(5) → the block state /// (ShieldBlock 24). Every other code — miss/resist/evade/immune/deflect — plays nothing, and /// PARRY(4) can never reach here at all: [`launch_outcome_code`] has already rewritten it to -/// DEFLECT(9), which is the mechanism behind the recorded negative that a ranged arrival never -/// parries (wow-re `smsg-attackerstate-consequences.md` §Q4). +/// DEFLECT(9), which is the mechanism behind the negative that a ranged arrival never parries +/// (`0x61ceb0`). fn miss_defense_state(code: u8) -> Option { match code { 3 => Some(2), // SPELL_MISS_DODGE → the dispatch's DODGES state diff --git a/crates/benilla-app/src/entities/mount.rs b/crates/benilla-app/src/entities/mount.rs index 237c4205..9355e936 100644 --- a/crates/benilla-app/src/entities/mount.rs +++ b/crates/benilla-app/src/entities/mount.rs @@ -8,14 +8,14 @@ //! //! ## The transition is a RE-SEAT, not a rebuild (B199) //! -//! **VERIFIED** — wow-re `mount-composition.md` Q3/Q4, byte-cited: the change handler `0x5ffa50` -//! runs *teardown old, then build new*, and neither half touches the rider's own model. +//! The change handler `0x5ffa50` runs *teardown old, then build new*, and neither half touches the +//! rider's own model. //! //! - **Mount up, `0x607a00`:** load the mount M2 → `0x613d80 SetMountModel` into `CGUnit+0xdc` → //! `0x712f70 CM2Model::attachChild(this = BODY [+0xd8], parent = MOUNT, slot 0)`, which sets //! `body+0x1cc = mount` and links the body into the mount's child list. The **body model is //! re-parented, not re-created**; its world matrix is recomposed each frame from the mount's live -//! posed attachment-0 bone (`m2_animate`'s child recursion `0x718657`–`0x71876f`). +//! posed attachment-0 bone (the animate kernel's child recursion `0x718657`–`0x71876f`). //! - **Dismount, `0x607ce0`:** `0x713020` **detaches** the body, `0x613d80(0)` destroys the mount //! model, the body goes to op4 seq 0 (Stand). Instant, no transition anim — and again the body //! model itself is untouched. @@ -65,8 +65,7 @@ pub(super) struct AppliedMount(pub(super) u32); /// Spawn a unit's mount child — the client's `0x607a00` model creation. Its `NetEntity` registers /// the display want (`update_display_models` scans every `NetEntity`) and `attach_entity_visuals` /// builds it as an ordinary creature; `scale` is the **CDI `creatureModelScale` column alone** -/// (byte law, wow-re `mount-composition.md` Q3/Q4 — the unit root's `SCALE_X` composes through the -/// hierarchy). +/// (byte law, `0x607a75` — the unit root's `SCALE_X` composes through the hierarchy). /// /// `fade_skip` suppresses the appear-fade: a mount blinking into existence under its rider is what /// the teardown-era transition rendered (every rebuilt child carried `Reattached`), and it is what @@ -100,9 +99,9 @@ pub(super) fn spawn_mount_child( } /// The seat anchor — the rider's model frame while mounted: a child of the mount's attachment-0 -/// joint at the authored offset, counter-scaled so the rider keeps its own size (byte-verified: -/// the client's body carries a compensating own/mount base ratio, `0x607b49 fld [esi+0x98]; fdiv -/// [esi+0x9c]` → `0x710620` — wow-re `mount-composition.md` Q3). +/// joint at the authored offset, counter-scaled so the rider keeps its own size (the client's body +/// carries a compensating own/mount base ratio, `0x607b49 fld [esi+0x98]; fdiv [esi+0x9c]` → +/// `0x710620`). /// /// [`benilla_world::rig_anim::RigFrame`] marks it as `rider`'s model frame so the world pass cascades /// a re-seat into the rider's palette (decision 0724). @@ -166,7 +165,7 @@ pub(super) fn seat_or_spawn_mount( mount_display: u32, fade_skip: bool, ) -> Seat { - // The mount scale law (byte-verified, wow-re `mount-composition.md` Q3/Q4): rendered = + // The mount scale law (`0x607a75`): rendered = // `SCALE_X × CreatureDisplayInfo.creatureModelScale` — the CDI column ALONE (no // CreatureModelData.modelScale). let mount_scale = creatures diff --git a/crates/benilla-app/src/entities/spell_fx/lifecycle.rs b/crates/benilla-app/src/entities/spell_fx/lifecycle.rs index 0a78ec6c..430de0c0 100644 --- a/crates/benilla-app/src/entities/spell_fx/lifecycle.rs +++ b/crates/benilla-app/src/entities/spell_fx/lifecycle.rs @@ -1,8 +1,7 @@ //! The effect-model **animation lifecycle**: `Stand` → `Hold` → `Decay`, the state machine a -//! spell-visual `CEffect`'s attached model runs for as long as it lives (VERIFIED — wow-re -//! `ceffect-anim-lifecycle.md`, §5 trio + corpus census 2026-08-02). Split from [`super`] along the -//! concern seam: that file owns the *instances* (the model cache, the attach cascade, the reap, the -//! world plants); this one owns *what they play*. +//! spell-visual `CEffect`'s attached model runs for as long as it lives. Split from [`super`] along +//! the concern seam: that file owns the *instances* (the model cache, the attach cascade, the reap, +//! the world plants); this one owns *what they play*. //! //! ## The mechanism //! @@ -30,8 +29,7 @@ //! Reap ([`FxDecay`]) is the same shape from the other end (`0x614150` → `0x6141c0`): if the model //! authors **`Decay` (159)**, arm it and the instance **keeps rendering for that sequence's //! authored span** before it despawns; otherwise it goes immediately. `0x6203e0` — the destructor — -//! plays nothing itself (its `0x4` bit is owner-attachment teardown, not a decay-out; wow-re §8 -//! corrects `ceffect-selfterm.md` on exactly this). +//! plays nothing itself (its `0x4` bit is owner-attachment teardown, not a decay-out). //! //! ## What this replaces, and how wrong it was //! @@ -211,8 +209,8 @@ fn arm_decay(player: &mut AnimationPlayer, armed: AnimationNodeIndex, anims: &Mo } /// The authored span of a model's `Decay` sequence — how long a reaped instance keeps rendering -/// before it despawns (wow-re §8: the node is *not* torn down synchronously). `None` when the model -/// authors no `Decay`, which is the reference's immediate-destroy gate. +/// before it despawns (`0x6141f0`: the node is *not* torn down synchronously). `None` when the +/// model authors no `Decay`, which is the reference's immediate-destroy gate. pub(crate) fn decay_span(anims: Option<&ModelAnimations>) -> Option { anims?.find(ANIM_DECAY).map(|c| c.duration) } diff --git a/crates/benilla-app/src/entities/spell_fx/mod.rs b/crates/benilla-app/src/entities/spell_fx/mod.rs index 7cbe2083..41ea132f 100644 --- a/crates/benilla-app/src/entities/spell_fx/mod.rs +++ b/crates/benilla-app/src/entities/spell_fx/mod.rs @@ -9,8 +9,8 @@ //! the render side: a path-keyed [`DisplayModel`] cache (the held-items pattern — entries created //! here, parts built by `super::update_display_models` once the M2 loads), and per-unit effect //! instances spawned under the unit's [`BoneAttach`] joints so they ride the animating bone — -//! exactly the client's `CEffect` bone attach, re-resolved per frame off the live bone matrix -//! (wow-re `spell-visual-apply.md` §1.5/§5; riding the joint entity gives us the same for free). +//! exactly the client's `CEffect` bone attach (`0x620be0`), re-resolved per frame off the live +//! bone matrix (riding the joint entity gives us the same for free). //! //! Lifetime is the client's stage policy (decision 0107 verdict 2): a **persistent** instance //! (precast/channel) lives until its spell-id-keyed [`SpellKitFx::Reap`] (the client's @@ -26,10 +26,8 @@ //! //! **The attach cascade below is mis-attributed and is a known divergence** (named in 2057, not //! yet fixed): `tag → 0xf → 0x13 → the unit's base` is `CMissile`'s (`0x61ceb0`, -//! `Missile_C.cpp`), not `AddEffect`'s. wow-re corrected this on 2026-09-02 -//! (`spell-visual-apply.md`, from `melee-blood-spurt-suppression.md` §1/§7): `0x61fdd0` has no -//! attachment test at all, so on a model lacking the tag the reference drops the effect -//! permanently and invisibly where we relocate it. +//! `Missile_C.cpp`), not `AddEffect`'s: `0x61fdd0` has no attachment test at all, so on a model +//! lacking the tag the reference drops the effect permanently and invisibly where we relocate it. //! //! Effect models run their **bone rigs** ([`arm_effect_rig`] — the birth clip + global sequences //! pose the joints that meshes skin to and emitters/ribbons/cards ride), advance them through the @@ -81,9 +79,8 @@ use super::{BoneAttach, DisplayModel, EntityPart, ModelHandle}; pub(super) use lifecycle::advance_fx_anim; use lifecycle::{decay_span, FxAnimLife, FxDecay}; -/// The client's attach fallback cascade when a model lacks the requested point (wow-re -/// `spell-visual-apply.md` §5, `0x61ceb0`/`0x61fae0`): retry `0xf`, then `0x13`, then the unit's -/// base position. +/// The client's attach fallback cascade when a model lacks the requested point +/// (`0x61ceb0`/`0x61fae0`): retry `0xf`, then `0x13`, then the unit's base position. const ATTACH_FALLBACKS: [u16; 2] = [0xf, 0x13]; /// The self-termination span when an effect model has no sequence table at all (the cube @@ -520,10 +517,11 @@ pub(crate) fn attach_effect_visuals( }, // The emitters' rate/enabled windows ride the played sequence: a `CEffect` ADVANCES // (`Stand` → `Hold` → `Decay`), so it reads the live one off its own player like a - // unit's do — the reference's `m2_animate` phase samples the CURRENT sequence record - // either way. A lane that never advances (a missile, whose InFlight is not the model's - // Stand) keeps its slot pinned on the spawn clock. gseq loops ride the instance's own - // spawn age in both (0856/0858 — an effect instance is fresh per play). + // unit's do — the phase in the reference's animate kernel `0x714260` samples the + // CURRENT sequence record either way. A lane that never advances (a missile, whose + // InFlight is not the model's Stand) keeps its slot pinned on the spawn clock. gseq + // loops ride the instance's own spawn age in both (0856/0858 — an effect instance is + // fresh per play). match seq_host { Some(h) => particles::EmitClock::Host(h), None => particles::EmitClock::Effect(played_seq), @@ -659,8 +657,8 @@ struct FxInstance { /// The self-termination deadline (`time.elapsed_secs()` clock), set at spawn for a /// non-persistent instance — and, for a reaped persistent one, the end of its `Decay` span. expires: Option, - /// Reaped, and playing its `Decay` out (wow-re `ceffect-anim-lifecycle.md` §8: the node is not - /// torn down synchronously — it keeps rendering for that sequence's authored duration). Such an + /// Reaped, and playing its `Decay` out (`0x614150`/`0x6141f0`: the node is not torn down + /// synchronously — it keeps rendering for that sequence's authored duration). Such an /// instance is already dying, so it no longer answers a reap or a replacing `Begin`: the /// reference's walk can't reach it either, having moved it to the pending-destroy list. decaying: bool, @@ -674,10 +672,10 @@ pub(super) struct FxAttached { /// A **world-planted** kit instance root (the field-12 slot, decisions 0848/0850): a free world /// entity, NOT a scene child of its owner — so [`tend_world_plants`] owns the two jobs the tree -/// would otherwise do. Byte law (wow-re `kit30-effect-slot.md`): planted once at the owner's -/// position × yaw × scale; a **root-aura** spell's plant (`EffectApplyAuraName` 26 anywhere in -/// the spell — the client's flag 0x4000) re-plants when the owner is displaced (knockback, -/// blink), re-baking facing and scale. +/// would otherwise do. The reference plants it once at the owner's position × yaw × scale +/// (`0x620a90`); a **root-aura** spell's plant (`EffectApplyAuraName` 26 anywhere in the spell — +/// the client's flag 0x4000) re-plants when the owner is displaced (knockback, blink; +/// `0x620580`), re-baking facing and scale. #[derive(Component)] pub(super) struct WorldPlantFx { /// The unit the instance belongs to — despawn tracking (the client's node dies with its @@ -705,8 +703,8 @@ fn world_plant_transform(owner: &GlobalTransform) -> Transform { } /// Whether any of the spell's three `EffectApplyAuraName` slots is `SPELL_AURA_MOD_ROOT` (26) — -/// the client's `spellRec+0x16c[0..2] == 0x1a` scan that arms the re-plant flag 0x4000 (wow-re -/// `kit30-effect-slot.md`; the column *name* is INFERRED there, the byte behaviour VERIFIED). +/// the client's `spellRec+0x16c[0..2] == 0x1a` scan that arms the re-plant flag 0x4000 +/// (`0x60f081`; only the column *name* is inferred). /// Exactly the field-12 state family: Frost Nova, Net, Web, Entangling Roots, Frostbite. fn spell_has_root_aura(spells: Option<&crate::ui_action::Spells>, spell_id: u32) -> bool { const SPELL_AURA_MOD_ROOT: u32 = 26; @@ -845,8 +843,7 @@ pub(super) fn resolve_spell_fx( } /// **Hand an ending instance's emitters to the drain** — the half of the reference's teardown -/// `0x6203e0` that is not a despawn (wow-re `ceffect-particle-drain.md`, §4a byte-settled -/// 2026-09-07). +/// `0x6203e0` that is not a despawn. /// /// An ending `CEffect` is *hidden*, not freed: emission stops, the already-emitted particles keep /// drawing and age out one at a time, and the node is released only on the frame the last one dies @@ -873,11 +870,10 @@ fn drain_instance_emitters( } } -/// **The same-slot replace walk** (`0x6208e0`, VERIFIED `[0x6208e0, 0x62092c]` — wow-re -/// `kit30-effect-slot.md` §4; decision 2057): every `CEffect::AddEffect` (`0x61fdd0`) opens by -/// walking the owner's `+0xb4` list and **destroying** (`0x6203e0`) each node carrying the same -/// `SpellVisualEffectName` record at the same attach tag, so a re-play *replaces* a still-live -/// same-model-same-slot emitter instead of stacking on it. +/// **The same-slot replace walk** (`0x6208e0`, `[0x6208e0, 0x62092c]`; decision 2057): every +/// `CEffect::AddEffect` (`0x61fdd0`) opens by walking the owner's `+0xb4` list and **destroying** +/// (`0x6203e0`) each node carrying the same `SpellVisualEffectName` record at the same attach tag, +/// so a re-play *replaces* a still-live same-model-same-slot emitter instead of stacking on it. /// /// This is the whole reason a busy fight does not brighten without bound in the reference: five /// mobs' blood spurts on one flank of one body are one instance there, not five, and the count is @@ -905,8 +901,8 @@ fn replace_same_slot( } if let Some(root) = i.root { // The replaced node's particles finish — the replace bounds the number of EMITTING - // nodes, not the number of live particles (`ceffect-particle-drain.md` §1's note on - // the shared teardown). + // nodes, not the number of live particles (it ends the node through the shared + // teardown `0x6203e0`). drain_instance_emitters(emitters, root); commands.entity(root).despawn(); } @@ -1019,8 +1015,7 @@ pub(super) fn attach_spell_fx( // dangling entity, and this gate's `root.is_some()` then means it is never rebuilt. // The reference's rebuild `0x60abe0` DRAINS the whole `+0xb4` list unconditionally // and then RE-CREATES what must persist — the aura-state re-arm `0x5ff130`, the - // channel re-arm `0x612a30` (wow-re `shapeshift-morph-cloud.md`, which REFUTED the - // earlier survives-the-rebuild reading). Our edge-driven aura watcher cannot notice + // channel re-arm `0x612a30`. Our edge-driven aura watcher cannot notice // (the aura never left the slots), so re-arming the PERSISTENT instance here IS that // re-create leg, in the one place that sees the drain. A one-shot goes, exactly as // the reference's drained Mount Poof does — the shapeshift cloud is NOT survival: @@ -1078,7 +1073,7 @@ pub(super) fn attach_spell_fx( // to the unit root, ground-anchored, and a persistent instance is never re-seated // after that: an Ice Barrier or a Power Word: Shield sat decaled at the feet for the // aura's whole life. The reference re-arms the persistent kits after the rebuild has - // set the new model (`0x60abe0` → `0x5ff130`, wow-re `shapeshift-morph-cloud.md`); + // set the new model (`0x60abe0` → `0x5ff130`); // `VisualAttached` is that "the model is set" — the cube fallback carries it too, so // a unit whose model never loads still gets its effect at the root. if !planted && !body_built { @@ -1318,8 +1313,8 @@ mod tests { .collect() } - /// **The drain-then-recreate law** (0835's named-open item closed; the reference byte-read - /// in wow-re `shapeshift-morph-cloud.md`): the rebuild drains EVERY effect node and then + /// **The drain-then-recreate law** (0835's named-open item closed; the reference's rebuild + /// `0x60abe0`): the rebuild drains EVERY effect node and then /// re-creates what must persist. A teardown of the unit's visual leaves each instance /// holding a dangling root; the persistent instance re-arms (our re-create leg — the /// edge-driven aura watcher cannot notice, the aura never left the slots), the one-shot diff --git a/crates/benilla-app/src/fishing_line.rs b/crates/benilla-app/src/fishing_line.rs index 06665cb2..984c0789 100644 --- a/crates/benilla-app/src/fishing_line.rs +++ b/crates/benilla-app/src/fishing_line.rs @@ -1,5 +1,4 @@ -//! The engine-drawn **fishing line** (wow-re `object-layer/scratch/fishing-line.md`, §5 -//! byte-verified, their `aed458c7`): rod tip → bobber while a unit channels Fishing. +//! The engine-drawn **fishing line**: rod tip → bobber while a unit channels Fishing. //! //! The reference draws this as a per-UNIT effect, not a GO one — one line object per unit //! (`[unit+0xb4c]`, ctor `0x61f490`), created when the unit's `UNIT_FIELD_CHANNEL_OBJECT` @@ -9,7 +8,7 @@ //! line. Benilla draws it immediate-mode per frame from exactly those conditions, so the //! reference's create/watcher/teardown lifecycle falls out as the condition holding or not. //! -//! The geometry (`0x61f780`, bit-proven `PRIMITIVE:effect_beam_trail`): **65 vertices**, straight +//! The geometry (`0x61f780`): **65 vertices**, straight //! lerp near → far, then a fixed half-sine sag — `z −= 0.5 × sin(π·t)`, 0.5 world-units at the //! midpoint, not length- or physics-scaled. Near = the pole M2's `$CCH` event marker (the bobber //! authors one too and the reference NEVER reads it — its far end is the bobber's position with diff --git a/crates/benilla-app/src/footprints.rs b/crates/benilla-app/src/footprints.rs index c57ca982..92b3f120 100644 --- a/crates/benilla-app/src/footprints.rs +++ b/crates/benilla-app/src/footprints.rs @@ -5,11 +5,11 @@ //! exactly the reference's own shape (baked once at spawn over collector-gathered ground //! triangles; the per-frame draw `0x69a3e0` only re-copies with the current alpha). //! -//! **The mechanism — wow-re §5 cross-checked** (`footprint-decals.md`, folded back in 1012): +//! **The mechanism** (decision 1012): //! - **Surface gate**: `TerrainType.Flags & 1` (`0x699eb6`) — set on exactly **Snow** and //! **Sand** — on the surface [`benilla_world::surface`] resolves under the unit, the *same* value the //! footstep sound uses. The reference resolves it once per unit into `CGUnit+0xc60` and the -//! decal, the spray and `$FSD` all read that one dword (wow-re `wmo-footstep-surface.md` F3), so +//! decal, the spray and `$FSD` all read that one dword (`0x5fc06e`, `0x5fc20f`, `0x62341d`), so //! indoors the gate reads the building's own floor: a tavern's floorboards take no prints while //! the snow outside its door does (decision 1161). //! - **Trigger**: each per-foot animation event tag (`$xL*`/`$xR*`; the same [`AnimSoundEvent`] @@ -24,8 +24,7 @@ //! stream) at the RIDER's scale. **The texture is authored as the LEFT foot** — right-foot //! prints mirror (`0x5fc07f`, scale(−1,1,1) before the yaw). //! - **Fade**: lifetime **6000 ms**, `t = 1 − age/6000`, `alpha = min(127, ⌊255·t⌋)` — ≈3.0 s -//! hold at ~50 % opacity then a ≈3.0 s linear fade, no fade-in ([`fade`], byte-diffed -//! `PRIMITIVE:footprint_alpha` @`0x69a3e0`). +//! hold at ~50 % opacity then a ≈3.0 s linear fade, no fade-in ([`fade`], `0x69a3e0`). //! - **Caps**: ring pools of **64 local-player + 512 everyone-else** slots, unconditional //! rotation (ring-select = GUID == local player, `0xca05f0`). //! - **Suppressions**: hover (`MOVEFLAG 0x4000_0000`) · stealth (`BYTES_1` byte 3 bit 0x2 — @@ -39,9 +38,9 @@ //! are always-on until a settings page wires the knob (the cvar-policy line, like the blob //! shadow's `shadowLOD`). //! -//! Draw state per the RE: src-alpha blend, depth-write off, unlit, white vertex RGB with the -//! fade in vertex alpha — the ink darkness lives in the texture. Still open there (none -//! load-bearing, note §13): the forward-axis sign convention inside the shared UV basis, the +//! Draw state per the reference: src-alpha blend, depth-write off, unlit, white vertex RGB with the +//! fade in vertex alpha — the ink darkness lives in the texture. Still open (none +//! load-bearing): the forward-axis sign convention inside the shared UV basis, the //! uv1 64×8 edge-fade ramp's combine mode (not modeled here — prints are small; the blob //! shadow's vertical trapezoid is that ramp's other consumer), and the frame order vs the blob //! shadow (our rung 2048 under its 4096 is a deterministic stand-in). @@ -63,8 +62,8 @@ use benilla_world::particles::buffer::{begin_effect_frame, EffectVertex}; use benilla_world::schedule::WorldStage; use benilla_world::view::WorldCamera; -/// Print lifetime, spawn to gone — the reference's 6000 ms (byte-verified, wow-re -/// `footprint-decals.md`: `t = 1 − age/6000`, die at `t < 0`). +/// Print lifetime, spawn to gone — the reference's 6000 ms (`0x69a3e0`: `t = 1 − age/6000`, die +/// at `t < 0`). const LIFETIME: f32 = 6.0; /// The local player's own ring pool: 64 slots (the reserved head of the reference's 576-slot /// table @`0xca05f0`), unconditional rotation. @@ -166,7 +165,7 @@ fn spawn_footprints( // are the MOUNT child's tags, whose local Transform is the seat-relative ~origin. units: Query<(&NetEntity, &GlobalTransform)>, // The spawner's ROOT (the rider for a mount child): the pool select, the print scale (the - // rider's SCALE_X — RE-corrected), and the state gates all read the root. + // rider's SCALE_X, `0x469f10`), and the state gates all read the root. parents: Query<&ChildOf>, roots: Query, camera: Query<&GlobalTransform, With>, @@ -200,7 +199,8 @@ fn spawn_footprints( continue; }; // The ink + dims come from the EVENT's model (the mount for a mounted composite); the - // scale and every state gate from the ROOT unit (the rider) — the RE's split. + // scale and every state gate from the ROOT unit (the rider) — the reference's split + // (`0x607920`). let Some(params) = net.display_id.and_then(|d| creatures.footprint(d)) else { continue; }; @@ -236,7 +236,7 @@ fn spawn_footprints( } // The surface gate, on the SAME terrain type the footstep sound used: the reference // resolves it once per unit (`CGUnit+0xc60`) and the decal, the spray and `$FSD` all read - // that one dword (wow-re `wmo-footstep-surface.md` F3). So this reads the UNIT's surface, + // that one dword (`0x5fc06e`, `0x5fc20f`, `0x62341d`). So this reads the UNIT's surface, // not a per-foot sample of the ground — indoors that is the building's own floor, which is // why a tavern's floorboards take no prints while the snow outside its door does. let terrain = world.terrain_type( @@ -249,15 +249,15 @@ fn spawn_footprints( } // Print frame: length along the unit's facing, width across, yawed to the facing. // With `(sin, cos) = (sin θ, cos θ)` the facing maps to the frame's −z′ axis, so v = 0 - // is the toe end (the absolute sign convention is the RE's open §13 item — a - // backwards-pointing print flips one sign here). + // is the toe end (the absolute sign convention is still open — a backwards-pointing + // print flips one sign here). let yaw = transform .to_scale_rotation_translation() .1 .to_euler(EulerRot::YXZ) .0; - // The rider's wire SCALE_X for a mounted composite, the unit's own otherwise (RE: the - // print scale is SCALE_X alone — display scales never multiply in). + // The rider's wire SCALE_X for a mounted composite, the unit's own otherwise (`0x469f10`: + // the print scale is SCALE_X alone — display scales never multiply in). let scale = root_net.unwrap_or(net).scale.max(0.0); let (half_len, half_wid) = (params.length * scale * 0.5, params.width * scale * 0.5); if half_len <= 0.0 || half_wid <= 0.0 { @@ -320,7 +320,7 @@ fn spawn_footprints( } } -/// The reference's fade at age seconds (byte-diffed `PRIMITIVE:footprint_alpha` @`0x69a3e0`): +/// The reference's fade at age seconds (`0x69a3e0`): /// `t = 1 − age/6 s`, `alpha_byte = min(127, ⌊255·t⌋)` — ≈3.0 s hold at 127/255 (~50 % /// opacity), then linear to 0; no fade-in; dead past [`LIFETIME`]. fn fade(age: f32) -> f32 { diff --git a/crates/benilla-app/src/game_plugins.rs b/crates/benilla-app/src/game_plugins.rs index acaceeb9..d4faff83 100644 --- a/crates/benilla-app/src/game_plugins.rs +++ b/crates/benilla-app/src/game_plugins.rs @@ -135,7 +135,7 @@ impl PluginGroup for GamePlugins { .add(crate::combat_log::CombatLogPlugin) .add(CreatureAnimPlugin) // The unit blob shadow: the dark ground oval under every unit, sized from the playing - // animation's box (the byte-verified law — wow-re unit-blob-shadow RE), on the same + // animation's box (`0x6d7920`), on the same // surface-decal projector as the selection ring. .add(BlobShadowPlugin) // Footprint decals (B212, decision 1006): the prints a walking unit leaves on snow/sand, @@ -347,10 +347,11 @@ impl PluginGroup for GamePlugins { // unlearns anything. Beside UiTalentPlugin for the subject, but it is UiBinderPlugin's twin // in shape — a guid-carrying question over an already-closed gossip menu. .add(UiTalentWipePlugin) - // The stance/shapeshift bar feed (wow-re shapeshift-bar-api.md): builds the form list from - // PlayerActions.spells per the byte-verified admission/order, drives the stock shapeshift bar through - // the engine's shapeshift seam, and drains its clicks (cancel-if-active else cast). After - // UiActionPlugin (shares `Spells`) and SpellPlugin (the `usable` walk, the cast tail). + // The stance/shapeshift bar feed: builds the form list from PlayerActions.spells per + // the reference's admission and order (`0x4b25b0`, `0x4b2bb0`), drives the stock + // shapeshift bar through the engine's shapeshift seam, and drains its clicks + // (cancel-if-active else cast). After UiActionPlugin (shares `Spells`) and SpellPlugin + // (the `usable` walk, the cast tail). .add(UiShapeshiftPlugin) // The pet action bar (decision 0982) — the stance bar's mirror image: server-authoritative, // so this renders the ten packed words the last `SMSG_PET_SPELLS` delivered and sends diff --git a/crates/benilla-app/src/game_tip.rs b/crates/benilla-app/src/game_tip.rs index 358ce6da..3172491f 100644 --- a/crates/benilla-app/src/game_tip.rs +++ b/crates/benilla-app/src/game_tip.rs @@ -1,5 +1,4 @@ -//! The loading screen's **tip of the day** (decision 2077; wow-re -//! `system/loadingscreen/scratch/game-tip-of-the-day.md`). +//! The loading screen's **tip of the day** (decision 2077). //! //! `showGameTips`' only mention in 1.12's FrameXML is its options row and the tooltip string //! "Uncheck this to hide the tip of the day in the load screens", so the whole feature is diff --git a/crates/benilla-app/src/glue/backdrop.rs b/crates/benilla-app/src/glue/backdrop.rs index d69c2b91..659a88f4 100644 --- a/crates/benilla-app/src/glue/backdrop.rs +++ b/crates/benilla-app/src/glue/backdrop.rs @@ -1,5 +1,5 @@ -//! WoW `Backdrop`s, at the geometry the client itself uses (wow-re `backdrop-mechanism.md`, -//! byte-verified; decision 0543): the `edgeFile` strip split into its eight upright pieces +//! WoW `Backdrop`s, at the geometry the client itself uses (`0x77e8d0`, `0x77f0c0`; +//! decision 0543): the `edgeFile` strip split into its eight upright pieces //! ([`split_backdrop_edges`] — the un-rotation law), the eight-piece border rig //! ([`backdrop_border`]) seated to its frame's laid-out size by [`fit_backdrop_borders`], and the //! tiled backdrop bg ([`tiled_bg_node`]). Split out of [`super::art`] (which loads the pieces and @@ -29,8 +29,8 @@ pub(crate) struct BackdropEdges { /// Split a WoW `edgeFile` strip into its eight upright `e`×`e` pieces, in the returned order /// LEFT, RIGHT, TOP, BOTTOM, TOPLEFT, TOPRIGHT, BOTTOMLEFT, BOTTOMRIGHT — the strip's own order -/// (wow-re `backdrop-mechanism.md` §3, byte-verified from the client's UV constants; independently -/// confirmed here by reading the shapes out of `Glue-Tooltip-Border`'s pixels). +/// (read from the client's UV constants in `0x77e8d0`/`0x77f0c0`; independently confirmed here +/// by reading the shapes out of `Glue-Tooltip-Border`'s pixels). /// /// TOP and BOTTOM are stored **rotated 90°** in the strip — the client maps atlas-u to screen-Y /// and atlas-v to screen-X reversed — so they are un-rotated here and every piece draws with plain @@ -90,10 +90,10 @@ pub(super) fn backdrop_edges( }) } -/// A `Backdrop`'s border, at the geometry the client itself uses (wow-re `backdrop-mechanism.md` -/// §2, byte-verified): the border sits **inside** the frame rect, flush with its edges — corner -/// squares of exactly `e`×`e` at the four corners, edge strips spanning between them and **tiling** -/// at period `e` (the run math `side/e − 2` has no upper clamp, so edges never stretch). Eight +/// A `Backdrop`'s border, at the geometry the client itself uses (`0x77e8d0`): the border sits +/// **inside** the frame rect, flush with its edges — corner squares of exactly `e`×`e` at the +/// four corners, edge strips spanning between them and **tiling** at period `e` (the run math +/// `side/e − 2` has no upper clamp, so edges never stretch). Eight /// pieces on a full-bleed rig child of the frame node, one per authored piece — the client's own /// eight `CSimpleTexture`s — seated to the frame's **laid-out** size by [`fit_backdrop_borders`], /// so content-sized frames and window rescales stay correct without a spawn-time size. @@ -259,7 +259,7 @@ pub(crate) fn tiled_bg_node(sheet: Handle, period: f32, s: f32, color: Co mod tests { use super::*; - // The split against the backdrop UV law (wow-re `backdrop-mechanism.md` §3): an e=2 strip with + // The split against the backdrop UV law (`0x77f0c0`): an e=2 strip with // pixel (x,y) = (x, y) in the r/g channels, so every copied texel is assertable by coordinates. #[test] fn backdrop_split_follows_the_edgefile_law() { diff --git a/crates/benilla-app/src/glue/mod.rs b/crates/benilla-app/src/glue/mod.rs index 4f55443d..d387f20c 100644 --- a/crates/benilla-app/src/glue/mod.rs +++ b/crates/benilla-app/src/glue/mod.rs @@ -88,11 +88,11 @@ impl GlueClicks { /// The glue layer's click law: a button fires on the **release**, and only when the release lands /// back on the button that took the press. /// -/// VERIFIED against the real 1.12.1 client (wow-re `ui.md`, the `OnDoubleClick`/`RegisterForClicks` -/// byte law): `CSimpleButton`'s ctor default click mask `[+0x330] = 0x100` is **`LeftButtonUp` -/// alone**, so a stock `