comments cite the reference by address, not by private note (#30)
Comments that pointed into the maintainer's private reverse-engineering notes (a note by name, a path, a section or finding id, the analysis's own name for a client function, a process word) now carry the fact itself, cited by the reference client's address, by vmangos file and line, or as "the reference". Nobody outside could follow those pointers. Comment text only, plus the test-assert and log strings that carried the same pointers. No claim changes: a comment the notes have since corrected keeps its wording here, and correcting it is separate work. The two reference generators, `scripts/gen-reference-globals.py` and `scripts/gen-reference-verb-events.py`, named the private notes' layout to find their inputs. They now take the input files as arguments (`--capture`, `--framexml`, `--names`, `--extents`, `--disasm`). Run on the same inputs, both write the committed tables row for row; only the two header lines that named the old environment variable change. Checked per file: - the code is identical: each file with its comments stripped compares equal before and after; a changed string literal is compared under a mask that proves only literal text moved and every literal kept its line count; - no client address the old text cited was dropped; - a second reviewer checked every added address against its derivation; - a detector for the notes' names, paths, ids, function names and process words finds none left; - `scripts/gates.sh` is green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Opus 5.5
parent
beb6cc854f
commit
7b66dac9db
@@ -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]
|
||||
|
||||
@@ -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<MclqChunk> {
|
||||
let want = (mcnk_flags & MCNK_LIQUID_BITS).count_ones().max(1) as usize;
|
||||
let mut out = Vec::with_capacity(want);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<String>,
|
||||
/// Names it calls that the VM does not have — see the module doc on what this is worth.
|
||||
pub missing_globals: Vec<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AuraNode> {
|
||||
///
|
||||
/// `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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 => "=",
|
||||
|
||||
@@ -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 `<Binding>` 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
|
||||
// `<Binding>` 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.
|
||||
|
||||
@@ -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<AssetServer>) {
|
||||
|
||||
/// 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<crate::entities::mount::MountBody>>,
|
||||
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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -226,12 +226,12 @@ fn glue_screen() -> Option<GlueScreen> {
|
||||
///
|
||||
/// **`WOW_FX_GO=<GameObjectDisplayInfo id>` 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=<Data> WOW_CAPTURE=fxview WOW_FX_GO=3073 WOW_FX_GO_STATE=1 WOW_FX_AGE=4 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> {
|
||||
script
|
||||
.eval::<Vec<String>>("return ProbeBinderSystemLines or {}")
|
||||
|
||||
@@ -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<String> {
|
||||
use benilla_ui::script::QuadContent;
|
||||
script
|
||||
|
||||
@@ -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
|
||||
//!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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 `<PlayerModel>`'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
|
||||
//! `<PlayerModel>`'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
|
||||
|
||||
@@ -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>@<start_s>[:<deg_per_sec>][;…]"` — `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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u64>,
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 `<Model>` 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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WorldText>);
|
||||
/// 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<usize> = 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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The in-game **console command registry** (decision 2303) — the reference's `ConsoleCommand`
|
||||
//! table, host side: `ConsoleCommandRegister 0x63f9e0` over a `TSExplicitList<CONSOLECOMMAND>`,
|
||||
//! 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 <line>` reaches
|
||||
//! [`execute`] through the chat drain.
|
||||
|
||||
@@ -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<u32> },
|
||||
/// 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<Entity>,
|
||||
/// 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<Vec3>,
|
||||
/// 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<u16>,
|
||||
/// 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<u16> {
|
||||
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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<super::NockLatch>();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<AnimationNodeIndex>) {
|
||||
(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::<AnimDriver>().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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<TrackSeek>;
|
||||
/// [`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() {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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<u8>,
|
||||
/// **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<Vec3>,
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Special> {
|
||||
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<Spec
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a requested one-shot (a swing or an emote) is routed this play (decision 0087, wow-re
|
||||
/// `anim-composition-model.md` §3/§5 at `923ac7bc`).
|
||||
/// Where a requested one-shot (a swing or an emote) is routed this play (decision 0087; the route
|
||||
/// flag `esi`, `0x5fe6c8..0x5fe74d`).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(super) enum OneShotRoute {
|
||||
/// Onto the SpineLow-subtree **masked overlay** ([`AnimClip::upper_node`]): the torso plays the
|
||||
@@ -1054,18 +1037,18 @@ pub(super) enum OneShotRoute {
|
||||
FullBody,
|
||||
}
|
||||
|
||||
/// The **CLASS_A** membership (wow-re `0x5fed90`) — the maskable-eligible set that gates the whole
|
||||
/// The **CLASS_A** membership (`0x5fed90`) — the maskable-eligible set that gates the whole
|
||||
/// state-route block; a non-CLASS_A id is always full-body. The load-bearing memberships were
|
||||
/// byte-decoded (17/66/68/80 ∈; the 37–45 jump/swim/locomotion band excluded); the patchy interior of
|
||||
/// the wide ranges is INFERRED (wow-re note §3 Open) but never load-bearing here — the only ids that
|
||||
/// reach [`route_oneshot`] are swings, emotes, and the spell-kit cast anims (32/33/51–54 — wow-re's
|
||||
/// own CLASS_A gloss: "swings, emotes, casts"), all squarely inside these ranges.
|
||||
/// the wide ranges is inferred and still open, but never load-bearing here — the only ids that
|
||||
/// reach [`route_oneshot`] are swings, emotes, and the spell-kit cast anims (32/33/51–54 — CLASS_A
|
||||
/// covers swings, emotes and casts), all squarely inside these ranges.
|
||||
fn is_class_a(id: u16) -> 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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Vec3>,
|
||||
/// 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<Res<Worl
|
||||
|
||||
/// The lookup bundle behind the client's `0x60d450` ranged fallback: caster →
|
||||
/// equipped-ranged-weapon display → `ItemDisplayInfo` col 10 substitute `SpellVisual` id
|
||||
/// (wow-re `throw-ranged-attack-anim.md`, decision 0370).
|
||||
/// (decision 0370).
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(super) struct WeaponVisualSrc<'w, 's> {
|
||||
displays: Option<Res<'w, ItemDisplays>>,
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<u8> {
|
||||
|
||||
/// 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
|
||||
|
||||
+93
-109
@@ -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<Item = (&'static str, &'static
|
||||
/// is `ret 4`) have no reader in the reference either. `maxLOD` is no CVar: none of the 214
|
||||
/// `CVar::Register` (`0x63db90`) sites names it.
|
||||
pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// The realm the session is on — a REAL 1.12 CVar (`0x83f2d0`, persisted, wow-re
|
||||
// `savedvariables-protocol.md`: the client builds its SavedVariables path from it), and a live
|
||||
// The realm the session is on — a REAL 1.12 CVar (`0x83f2d0`, persisted: the client builds its
|
||||
// SavedVariables path from it through `0x5ab7d0`), and a live
|
||||
// Lua consumer in the strongest sense the honest-tree rule asks for. `Ace/AceState.lua:27` does
|
||||
// `ace.trim(GetCVar("realmName"))` inside `SetGameState`, which every Ace addon runs at
|
||||
// PLAYER_ENTERING_WORLD — so a nil there was `gsub(nil)` and took the whole Ace family down.
|
||||
@@ -227,14 +224,14 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
//
|
||||
// The default is EMPTY, deliberately and not as a guess: the value is written from the session's
|
||||
// real realm the moment addons load (`ui_script::addons::load_third_party`), so the default only
|
||||
// ever describes a client that has not connected. wow-re records a string
|
||||
// ever describes a client that has not connected. `0x85d684` holds a string
|
||||
// `"Last realm connected to"` beside the registration, but that reads like the CVar's HELP text
|
||||
// rather than its value and nothing here needs to resolve it — `""` is what `ace.trim` handles
|
||||
// cleanly, and inventing a realm name would be worse than admitting we have none yet.
|
||||
same("realmName", ""),
|
||||
// The address of the logon server — the reference's own CVar, byte-verified in `WoW.exe`
|
||||
// (the registration's string neighbours are `realmlist.wtf`, "Address of realm list server"
|
||||
// and `us.logon.worldofwarcraft.com:3724`; wow-re `mpq/scratch/startup-order-A.md` row 62).
|
||||
// The address of the logon server — the reference's own CVar (register site `0x5ab6a6`; the
|
||||
// registration's string neighbours are `realmlist.wtf`, "Address of realm list server" and
|
||||
// `us.logon.worldofwarcraft.com:3724`).
|
||||
// A **string** row: the registry takes any string for it, and `realmlist::on_cvar` judges it.
|
||||
// The default diverges knowingly — see `realmlist::DEFAULT_REALMLIST`.
|
||||
deviates(
|
||||
@@ -246,8 +243,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
),
|
||||
// The implicit AFK clear (2088) — a REAL 1.12 CVar, byte-read off its own registration
|
||||
// (`0x5e24d4 push 0x82e748`, handle taken from the store AFTER the call at `0x5e24ef` into
|
||||
// `[0xc4d68c]`, whose single reader `0x5eb84b` tests `[cvar+0x28]` for non-zero; wow-re
|
||||
// `ui/scratch/afk-dnd-command-law.md` §10). Registered default `"1"`.
|
||||
// `[0xc4d68c]`, whose single reader `0x5eb84b` tests `[cvar+0x28]` for non-zero). Registered
|
||||
// default `"1"`.
|
||||
//
|
||||
// It gates FIVE implicit clears, not one: any chat send whose type is not `0x14` (which is why
|
||||
// `/dnd` clears AFK before marking), plus Jump, forward/back, strafe and turn
|
||||
@@ -258,7 +255,7 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
same("SoundVolume", "1"),
|
||||
same("MusicVolume", "0.4"),
|
||||
same("AmbienceVolume", "0.6"),
|
||||
// The three 1.12 sound enables (registrar defaults all "1", wow-re B10):
|
||||
// The three 1.12 sound enables (registrar defaults all "1", `0x45737a`/`0x45739b`/`0x460a9d`):
|
||||
// `MasterSoundEffects` is the MASTER "Enable All Sound" checkbox (SoundOptionsFrame.lua
|
||||
// index 1 — its callback sets the engine-wide pause flag), NOT an SFX-only toggle; 1.12
|
||||
// has no `EnableSound`/`EnableSFX` at all.
|
||||
@@ -266,13 +263,13 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
same("EnableMusic", "1"),
|
||||
same("EnableAmbience", "1"),
|
||||
// Error speech (1815) — the race/sex refusal lines your character says. A real 1.12 CVar
|
||||
// (`CVar::Register` at `0x457877`, registrar default `"1"`; wow-re
|
||||
// `re/cvar/cvar-register-sites.tsv` row 54) and a real 1.12 checkbox: SoundOptionsFrame.lua's
|
||||
// `ENABLE_ERROR_SPEECH`, index 4, which the master enable greys along with Ambience.
|
||||
// (`CVar::Register` at `0x457877`, registrar default `"1"`) and a real 1.12 checkbox:
|
||||
// SoundOptionsFrame.lua's `ENABLE_ERROR_SPEECH`, index 4, which the master enable greys along
|
||||
// with Ambience.
|
||||
same("EnableErrorSpeech", "1"),
|
||||
// Sound while the window is in the background (1847). **Not a 1.12 CVar and not a 1.12
|
||||
// checkbox**: none of the reference's 214 `CVar::Register` sites names it
|
||||
// (`re/cvar/cvar-register-sites.tsv`), and `SoundOptionsFrame.lua` declares seven checkboxes
|
||||
// checkbox**: none of the reference's 214 `CVar::Register` (`0x63db90`) sites names it, and
|
||||
// `SoundOptionsFrame.lua` declares seven checkboxes
|
||||
// (indices 1, 2, 4-8) and four sliders, none of them this. `CVar::Register` is the only
|
||||
// creation path, so `Config.wtf` can hold no such key either. The spelling is the later-era
|
||||
// engine's — the `autoLootDefault` / `nameplateShowEnemies` posture, where benilla's
|
||||
@@ -282,10 +279,9 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// **`same`, not `ours`**, for the nameplate pair's reason: the reference has no CVar to match
|
||||
// but it very much has a *behaviour* to match, and it goes quiet in the background —
|
||||
// unconditionally, on `WM_ACTIVATE` → event-bus category 2 → `0x7a4860`'s
|
||||
// `FSOUND_SetMute(-3, active ? 0 : 1)`, music included (wow-re
|
||||
// `sound/scratch/focus-mute-law.md`, VERIFIED). "0" IS the reference's own behaviour. The knob
|
||||
// is `SoundConfig::background_sound`, which carries the mechanism and the one disclosed
|
||||
// divergence.
|
||||
// `FSOUND_SetMute(-3, active ? 0 : 1)`, music included. "0" IS the reference's own behaviour.
|
||||
// The knob is `SoundConfig::background_sound`, which carries the mechanism and the one
|
||||
// disclosed divergence.
|
||||
same("Sound_EnableSoundWhenGameIsInBG", "0"),
|
||||
// Zone reverb (1153). The binary registers this one `"1"` (`0x4573be`) and we register it
|
||||
// `"0"` — the only row here that knowingly leaves the registrar's default, because the
|
||||
@@ -349,8 +345,7 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
same("farclip", "350"),
|
||||
// **`nearclip` — farclip's other half, and a knob we had been holding as a constant** (2163).
|
||||
// `0x68867a` passes name `0x84ffb0` `"nearclip"`, default string `0x84fb48` `"0.1"`, help
|
||||
// "Near clip plane distance", flags `1`, callback `0x688d90`, record `[0xc7f348]` (wow-re
|
||||
// `re/cvar/cvar-register-sites.tsv` row 187).
|
||||
// "Near clip plane distance", flags `1`, callback `0x688d90`, record `[0xc7f348]`.
|
||||
//
|
||||
// **The reader is the camera, and it re-reads every frame.** `0x511bc0` — the per-frame camera
|
||||
// outer, sole caller `0x483094` — stamps `[cam+0x38]` from this record's float before the
|
||||
@@ -361,9 +356,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
//
|
||||
// **Why it was not registered for so long, and why that reasoning was wrong.** The near plane
|
||||
// was a `CAM_NEAR = 1.0/9.0` const documented as the reference's own, on the true finding that
|
||||
// the callback's *derived global* `[0xc7b480]` has one writer and no readers (wow-re
|
||||
// `cvar/scratch/graphics-cost-cvar-census.md` §8 lists `nearclip` among the eleven dead knobs
|
||||
// for exactly that). The camera does not read that global; it reads the record. So the ctor's
|
||||
// the callback's *derived global* `[0xc7b480]` has one writer and no readers. The camera does
|
||||
// not read that global; it reads the record. So the ctor's
|
||||
// `0x3de38e39` = 1/9 is overwritten by the first frame's stamp and never reaches a picture —
|
||||
// a verified-but-partial mechanism, which docs/METHOD.md §4 names as the classic trap.
|
||||
//
|
||||
@@ -390,8 +384,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// name and there was nothing for it to toggle (decision 1745).
|
||||
//
|
||||
// Register site `0x6e731d`, default string `"0"`, record `[0xceac34]`, one reader at
|
||||
// `0x6e53d7` (wow-re `cvar/scratch/registered-defaults-census.md`, 1804's §5 round). **This
|
||||
// row used to cite `0x870dc0` as the record; that is the NAME string** — corrected there.
|
||||
// `0x6e53d7` (1804). **This row used to cite `0x870dc0` as the record; that is the NAME
|
||||
// string**.
|
||||
//
|
||||
// **benilla ships it ON and the reference registers "0"** — a named deviation that predates
|
||||
// this row (`cast_target::AutoSelfCast`): with it off, an unbindable friendly cast falls into
|
||||
@@ -483,8 +477,8 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
),
|
||||
// The overhead-name trio (0992): 1.12's own UnitName* CVars (UIOptionsFrame.lua indices
|
||||
// 21/30/67) over the nameplates module's gates. Defaults mirror `NameConfig::default()` and
|
||||
// are the binary's own, byte-read at the `0x6c7470` registrar (wow-re
|
||||
// `object-layer/scratch/overhead-name.md`, name string / default string per row, folded into
|
||||
// are the binary's own, byte-read at the `0x6c7470` registrar (name string / default string
|
||||
// per row, folded into
|
||||
// mask `0xce8720`) — `UnitNamePlayer` `0x86c694` → `"1"` `0x82e748`, `UnitNameNPC` `0x86c6a4`
|
||||
// and `UnitNameOwn` `0x86c6b0` → `"0"` `0x82e570`. Corroborated the other way by the
|
||||
// reference install's own `Config.wtf`, which carries `SET UnitNameNPC "1"` and
|
||||
@@ -498,13 +492,12 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// The fourth of the same registrar's five (2149): `UnitNamePlayerGuild` `0x86c680` -> `"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
|
||||
|
||||
@@ -65,8 +65,8 @@ pub(crate) struct DeathNet {
|
||||
pub(crate) corpse: Option<CorpsePoint>,
|
||||
/// A pending resurrection offer; cleared when answered or when the popup times out.
|
||||
pub(crate) resurrect: Option<ResurrectOffer>,
|
||||
/// 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<CorpseRange>,
|
||||
/// 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<String>) -> 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<crate::ui_action::UiErrorKeys>,
|
||||
// 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),
|
||||
|
||||
@@ -108,7 +108,7 @@ fn on_session_end(In(_): In<SessionEvent>, mut death_net: ResMut<DeathNet>) {
|
||||
/// 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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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 `<PlayerModel>` 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 `<PlayerModel>` 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<DisplayRig<'_>> {
|
||||
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<Res<crate::portrait::GluePreview>>,
|
||||
char_create: Option<Res<CharCreate>>,
|
||||
// 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<Res<crate::portrait::StableBooth>>,
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<usize> {
|
||||
@@ -175,10 +175,10 @@ pub(in crate::entities) fn build_glue_preview(
|
||||
mut skin_composites: ResMut<SkinComposites>,
|
||||
asset_server: Res<AssetServer>,
|
||||
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<Res<crate::creature_anim::SpellVisuals>>,
|
||||
mut fx: Option<ResMut<super::super::spell_fx::SpellFx>>,
|
||||
// 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<Assemble
|
||||
};
|
||||
let mut held = held_wants(&equipment, helm, race, sex, spec.ranged_in_hand);
|
||||
// Per-hand grip `[right, left]`: a hand whose attach point holds a weapon curls into `HandsClosed`
|
||||
// (wow-re `hand-grip-mechanism.md` — the ref's paperdoll rule `0x5059a0`: attach-point occupancy, per
|
||||
// hand). Resolved here because the flat rider list drops each held item's attach id. A shield sits on
|
||||
// the forearm (Shield attach, id 0), never a hand point, so its hand stays open.
|
||||
// (the ref's paperdoll rule `0x5059a0`: attach-point occupancy, per hand). Resolved here
|
||||
// because the flat rider list drops each held item's attach id. A shield sits on the forearm
|
||||
// (Shield attach, id 0), never a hand point, so its hand stays open.
|
||||
let grip = [
|
||||
held.iter().any(|w| w.attach == attach_id::HAND_RIGHT),
|
||||
held.iter().any(|w| w.attach == attach_id::HAND_LEFT),
|
||||
@@ -674,7 +674,7 @@ fn assemble(spec: &PreviewSpec, ctx: &mut PreviewCtx<'_, '_>) -> Option<Assemble
|
||||
}
|
||||
|
||||
// The dressed geoset set: the worn armor's geoset groups + the cloak + the helm's hide-masks
|
||||
// (RF-0083), exactly the world path's selection (the shared helper).
|
||||
// (`0x4799a0`), exactly the world path's selection (the shared helper).
|
||||
let eg = equip_geosets(ctx.displays.as_deref(), &bodyslots, cloak, helm, false);
|
||||
let visible = ctx.characters.map(|c| {
|
||||
c.0.visible_geosets(race, sex, char_look.hair_style, char_look.facial_hair, &eg)
|
||||
@@ -897,15 +897,14 @@ struct HeldWant {
|
||||
}
|
||||
|
||||
/// The select character's attach-model wants from its enum record: helm + the shoulder pair, and
|
||||
/// the weapons **held in the hands** — the byte-verified select build (wow-re
|
||||
/// `glue-select-model.md` TU-A, folding back decision 0465's sheathed INTERIM): the builder
|
||||
/// forces SheatheType 0 (`0x472c8c`), so the sheath resolver never yields a stow point and the
|
||||
/// hand code runs — mainhand to HandRight (attach 1), an off-hand weapon to HandLeft (2), a
|
||||
/// shield to the Shield point (0) — and the RANGED slot is skipped outright (`0x472bfe`).
|
||||
/// [`placement`]'s melee-drawn arm (`unit_sheath = 1`) is exactly that law, ranged-skip included
|
||||
/// (its ranged arm only yields while ranged-drawn). A held off-hand frill (INVTYPE_HOLDABLE)
|
||||
/// rides the same hand law — the verdict enumerates weapons/shields only, and the world's held
|
||||
/// placement is the natural reading for the rest.
|
||||
/// the weapons **held in the hands** — the select build (folding back decision 0465's sheathed
|
||||
/// INTERIM): the builder forces SheatheType 0 (`0x472c8c`), so the sheath resolver never yields a
|
||||
/// stow point and the hand code runs — mainhand to HandRight (attach 1), an off-hand weapon to
|
||||
/// HandLeft (2), a shield to the Shield point (0) — and the RANGED slot is skipped outright
|
||||
/// (`0x472bfe`). [`placement`]'s melee-drawn arm (`unit_sheath = 1`) is exactly that law,
|
||||
/// ranged-skip included (its ranged arm only yields while ranged-drawn). A held off-hand frill
|
||||
/// (INVTYPE_HOLDABLE) rides the same hand law — the reference confirms it at `0x47a0c0` for
|
||||
/// weapons/shields only, and the world's held placement is the natural reading for the rest.
|
||||
///
|
||||
/// **`ranged_in_hand` is the dressing room's departure from that** (decision 1076): the dress-up
|
||||
/// widget does *not* skip the ranged slot, and installs it at a hand. That is the same mapping
|
||||
@@ -1133,9 +1132,9 @@ fn assemble_pet(
|
||||
material: p.material.clone(),
|
||||
alpha_anim: p.alpha_anim.clone(),
|
||||
// The pet is its own CM2 instance, chained to the SCENE (attachment 1), never to
|
||||
// the character (wow-re `glue-select-model.md` §B1) — so it composes onto nothing
|
||||
// and is never ghosted with its owner. The twins ride anyway, for the same reason
|
||||
// the body's do: they are a property of the batch.
|
||||
// the character (`0x47306e`) — so it composes onto nothing and is never ghosted
|
||||
// with its owner. The twins ride anyway, for the same reason the body's do: they
|
||||
// are a property of the batch.
|
||||
twins: BoothTwins {
|
||||
blend: p.fade_blend.clone(),
|
||||
zfill: p.zfill.clone(),
|
||||
@@ -1206,17 +1205,15 @@ mod tests {
|
||||
/// hard-codes for the character-select ghost, decoded through the same dispatch point the world
|
||||
/// aura uses.
|
||||
///
|
||||
/// Every number here is byte-VERIFIED in wow-re at the record itself
|
||||
/// (`ghost-death-visuals.md` §5a: row 989's `+0x3c` reads
|
||||
/// `01 00 00 00 0e 00 00 00 ff ff ff ff ff ff ff ff fd b9 0c 4b 00 00 00 3f`), and the
|
||||
/// note records a §5 derivation that got `0x4B0CB53D → (140,181,61)` and was refuted at those
|
||||
/// bytes. That is exactly the failure this test exists to catch: a tint transcribed one nibble
|
||||
/// wrong is still a plausible pale blue, and nothing but the shipped row can tell them apart.
|
||||
/// Every number here is read at the record itself (row 989's `+0x3c` reads
|
||||
/// `01 00 00 00 0e 00 00 00 ff ff ff ff ff ff ff ff fd b9 0c 4b 00 00 00 3f`), and
|
||||
/// `0x4B0CB53D → (140,181,61)` is a misreading of those bytes. That is exactly the failure
|
||||
/// this test exists to catch: a tint transcribed one nibble wrong is still a plausible pale
|
||||
/// blue, and nothing but the shipped row can tell them apart.
|
||||
///
|
||||
/// It also pins the *shape* the select path depends on — one populated effect slot, at
|
||||
/// attachment `0x13` (Base) — because "nine ghost sub-models" was this note's own earlier
|
||||
/// reading (benilla decision 0469 §6 carried it), corrected 2026-08-21 when `0x472780`'s
|
||||
/// `test esi,esi; jl` was seen rejecting the eight `-1`s.
|
||||
/// attachment `0x13` (Base) — because "nine ghost sub-models" was an earlier reading (benilla
|
||||
/// decision 0469 §6 carried it), and `0x472780`'s `test esi,esi; jl` rejects the eight `-1`s.
|
||||
#[test]
|
||||
fn the_shipped_ghost_kit_decodes_to_the_verified_row() {
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
//! The reference re-dresses on the SAME `CM2Model`. Its character compositor re-blits the dirty
|
||||
//! region groups into the component's own 256² target and re-runs the geoset selection `0x477520`,
|
||||
//! whose only reach into the model is the per-instance visibility array `+0x98` — a range filter
|
||||
//! writing ordinals, nothing more (wow-re `charactermodel.md` "Assembly orchestration";
|
||||
//! `models.md` §"geoset-visibility-default", where the writer census proves the character compositor
|
||||
//! is the *only* thing in the binary that can hide a submesh). Attachments are installed through a
|
||||
//! different mechanism entirely (`0x712f70 CM2Model::attachChild`) and the compositor never touches
|
||||
//! them.
|
||||
//! writing ordinals (`0x7110d0`), nothing more; the character compositor is the *only* thing in
|
||||
//! the binary that can hide a submesh. Attachments are installed through a different mechanism
|
||||
//! entirely (`0x712f70 CM2Model::attachChild`) and the compositor never touches them.
|
||||
//!
|
||||
//! So this system is that law, and only that law:
|
||||
//!
|
||||
|
||||
@@ -96,7 +96,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// GOLDEN — the carried-light spawn law. Only a **casting** light spawns (`type==1`, not held
|
||||
/// dark by a static `0` visibility key — wow-re `m2-dynamic-lights.md` §9.4, the shape 11 of
|
||||
/// dark by a static `0` visibility key — the gather gates in `0x718960`, the shape 11 of
|
||||
/// the corpus's 85 point lights actually ship), it lands as a CHILD of its host bone's joint
|
||||
/// so the animation carries it, and its offset is the def position rebased into that bone's
|
||||
/// frame (`position − bone_pivot`, wow→bevy). Colour × intensity survives the `PointLight`
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
//! A kit whose `CharProc` decodes to a chain ([`benilla_formats::ChainProc`]) draws a **polyline of
|
||||
//! hops** — `caster → t1 → t2 → t3` — with one ribbon per hop, each subdivided, jittered per frame
|
||||
//! and scrolled. The data half (the DBC row, the sentinel fix, the wire) is slice 1; this module is
|
||||
//! the client's `LightningObject` (`0x6ec460`) + `CLightning` (`0x7af9b0`/`0x7afcb0`) pair, whose
|
||||
//! every load-bearing number is byte-VERIFIED in wow-re `system/spell/scratch/chain-beam-law.md`
|
||||
//! (their `31715619`).
|
||||
//! the client's `LightningObject` (`0x6ec460`) + `CLightning` (`0x7af9b0`/`0x7afcb0`) pair.
|
||||
//!
|
||||
//! ## What the reference does, and what this transcribes
|
||||
//!
|
||||
@@ -58,7 +56,7 @@
|
||||
//! `phase = fmod(phase + dt, period)`; a **negative** period reverses the scroll, which is what
|
||||
//! flows the four drain textures back toward the caster.
|
||||
//!
|
||||
//! **Render state** (`0x7afcb0`, decoded against wow-re's own verified `EGxRs` map): additive
|
||||
//! **Render state** (`0x7afcb0`, decoded against the `EGxRs` applicator `0x59d350`): additive
|
||||
//! `SRC_ALPHA/ONE`, emissive white — a beam is **never tinted** — two-sided, depth-write off, fog
|
||||
//! **off**. It rides the shared effect-quad stream ([`benilla_world::particles::buffer::EffectQuads`]) like
|
||||
//! every other dynamic effect; `benilla_world::ribbons` is the structural model (same strip-as-quads
|
||||
@@ -69,7 +67,7 @@
|
||||
//! byte-identical arguments and they diverge *only* through their interleaved draws on the shared
|
||||
//! PRNG, which is a shared-generator detail we do not reproduce, so ours diverge by construction
|
||||
//! instead. (b) The `SMSG_SPELL_GO` producer leg is gated in the reference on `0x6e4870`'s return, a
|
||||
//! predicate the §5 could not settle; we fill unconditionally — the superset, harmless because
|
||||
//! predicate that is not settled; we fill unconditionally — the superset, harmless because
|
||||
//! consumption still requires a chain proc, and because every producer clears before it fills.
|
||||
//! (c) A channel re-enters the dispatcher every tick (`0x612b18`) where we hold one beam for the
|
||||
//! channel's life; the observable — a steady beam that ends with the channel — is the same.
|
||||
@@ -569,10 +567,10 @@ pub(crate) fn simulate_chain_beams(
|
||||
}
|
||||
}
|
||||
|
||||
/// The beam's arithmetic, against the numbers wow-re read out of the binary — the subdivision floor,
|
||||
/// The beam's arithmetic, against the numbers read out of the binary — the subdivision floor,
|
||||
/// the spindle taper, the view-plane cross-section and its degenerate regime, and the signed
|
||||
/// scroll. Each of these is a value that would go straight into pixels, and two of the four columns
|
||||
/// they read were mis-named in the community schemas until the §5 (decision 0955).
|
||||
/// they read are mis-named in the community schemas (decision 0955).
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
@@ -679,8 +677,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// `n = trunc(len / avgSegLen + 2.0)`, `n+1` points — a **floor of two**, so even a hop shorter
|
||||
/// than one segment still bends (`0x7af713`, the `2.0f` at `0x801628`). The 30-yard case is the
|
||||
/// §5's own worked example: 30 / 2.78 + 2 → 12 segments, 13 points.
|
||||
/// than one segment still bends (`0x7af713`, the `2.0f` at `0x801628`). The 30-yard case:
|
||||
/// 30 / 2.78 + 2 → 12 segments, 13 points.
|
||||
#[test]
|
||||
fn subdivision_carries_the_plus_two_floor() {
|
||||
let (e, mut rng, mut out) = (lightning(), 1u32, Vec::new());
|
||||
|
||||
@@ -14,29 +14,29 @@ use super::Creatures;
|
||||
/// A unit's **collision height** in world yards — the reference's `CMovement+0xb4`, and the `h`
|
||||
/// that every depth line in the client is a fraction of: swim enters and rests at `0.75·h`, the
|
||||
/// splash fires at `0.4·h`, wading ends where swimming begins (`0.75·h`), and the foam gate is
|
||||
/// `max(2·h, 1.0)`. Each of those *fractions* is byte-verified against the real client (wow-re
|
||||
/// `swim-transition.md`, `swim-mechanism.md`, `water-ripple-decal.md`); until decision 0645 the
|
||||
/// `h` they multiplied was a **constant** — [`crate::player::DEFAULT_COLLISION_HEIGHT`], the
|
||||
/// client's own empty-world ctor default — so all five lines were right for a human male (2.031,
|
||||
/// within 2 mm of it) and wrong for every other race. A gnome female stands 1.15 yd and was held
|
||||
/// 1.52 yd under: she could not reach the surface, which is how the defect finally showed (B76).
|
||||
/// `max(2·h, 1.0)`. Each of those *fractions* is the real client's (`0x6030c0`, `0x5fa760`);
|
||||
/// until decision 0645 the `h` they multiplied was a **constant** —
|
||||
/// [`crate::player::DEFAULT_COLLISION_HEIGHT`], the client's own empty-world ctor default — so all
|
||||
/// five lines were right for a human male (2.031, within 2 mm of it) and wrong for every other
|
||||
/// race. A gnome female stands 1.15 yd and was held 1.52 yd under: she could not reach the
|
||||
/// surface, which is how the defect finally showed (B76).
|
||||
///
|
||||
/// `CreatureModelData.collisionHeight × k`. The column is the model's own MD20 collision-box Z
|
||||
/// extent in raw model units — machine-pinned against the shipped client (`benilla_formats`
|
||||
/// `collision_height_is_the_m2_collision_box`), which is what settles that it scales with the
|
||||
/// geometry it bounds.
|
||||
///
|
||||
/// **`k = max(OBJECT_FIELD_SCALE_X, CreatureDisplayInfo.creatureModelScale)`** — VERIFIED byte law
|
||||
/// (wow-re `collision/scratch/mover-collision-scalars.md`): `0x60b270` fetches `SCALE_X` through
|
||||
/// vtable slot `+0x1c` (`0x469f10`), compares it against the display row's own column at
|
||||
/// `0x60b312` (`CreatureDisplayInfo+0x10`), and hands the **larger** to `0x6174b0`, which stores
|
||||
/// `CollisionHeight · k` into `CMovement+0xb4` at `0x617501`. Its two callers include the unit
|
||||
/// model (re)build `0x5fb9dd` with `force = 1`, so every unit gets it, not just the mover.
|
||||
/// **`k = max(OBJECT_FIELD_SCALE_X, CreatureDisplayInfo.creatureModelScale)`** — the byte law:
|
||||
/// `0x60b270` fetches `SCALE_X` through vtable slot `+0x1c` (`0x469f10`), compares it against the
|
||||
/// display row's own column at `0x60b312` (`CreatureDisplayInfo+0x10`), and hands the **larger** to
|
||||
/// `0x6174b0`, which stores `CollisionHeight · k` into `CMovement+0xb4` at `0x617501`. Its two
|
||||
/// callers include the unit model (re)build `0x5fb9dd` with `force = 1`, so every unit gets it,
|
||||
/// not just the mover.
|
||||
///
|
||||
/// The `max` is a **floor, never a second multiplier**. The *render* scale is [`NetEntity::scale`]
|
||||
/// = `OBJECT_FIELD_SCALE_X` alone, which the server has already folded the DBC scales into
|
||||
/// (`CreatureModelData.modelScale × CreatureDisplayInfo.scale`, vmangos `Unit::GetScaleForDisplayId`
|
||||
/// — and wow-re `object-layer.md`'s render-scale CORRECTION says in as many words that a client
|
||||
/// — and the reference's render scale (`0x613fc9`) has no display-column term, so a client
|
||||
/// multiplying the display column in again would *square* it). So the collision box and the drawn
|
||||
/// body still cannot disagree; the floor only stops the box shrinking below the display's own size.
|
||||
///
|
||||
@@ -52,8 +52,7 @@ use super::Creatures;
|
||||
/// `[unit+0x110]+0x1f8` (index 132), while its sibling `0x60ae10` reads `+0x1f4` (index 131 =
|
||||
/// `UNIT_FIELD_DISPLAYID`) off the same array; the cached-row arm is taken only when the two
|
||||
/// fields are equal (`[unit+0xc58] & 0x100` = "not transformed", set from their equality at
|
||||
/// `0x60b166`), so **every arm yields the native model's row**. VERIFIED — wow-re
|
||||
/// `mover-collision-scalars.md` and `remote-swim-decision.md` §4.
|
||||
/// `0x60b166`), so **every arm yields the native model's row**.
|
||||
///
|
||||
/// So **a transform does not resize the collision prism**: a druid in bear form keeps the druid's
|
||||
/// swim, splash, foam and wade lines; a GM `.modify morph` into a gnome keeps the human's. That is
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Terrain conform — **fidelity** (decisions 0482/0486; wow-re `terrain-tilt.md` §5): the
|
||||
//! Terrain conform — **fidelity** (decisions 0482/0486; `0x7106c0`): the
|
||||
//! reference tilts every model whose M2 authors `GlobalModelFlags & 3 ∈ {1,3}` — all 152 mounts
|
||||
//! + quadrupeds pitch (flag 1), the 74 low-wide models (kodo/basilisk/crab/crocodile/spider)
|
||||
//! pitch AND roll (flag 3) — wild or mounted alike: the gate is the model flag, not mountedness.
|
||||
@@ -52,7 +52,7 @@ fn tilt_target(sampled: Option<Vec3>) -> Option<Vec3> {
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<GroundBurst>,
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Handle<WowModelMaterial>>,
|
||||
/// 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<Handle<WowModelMaterial>>,
|
||||
/// 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<std::sync::Arc<benilla_formats::SeqLoops<[f32; 3]>>>,
|
||||
/// 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<f32>,
|
||||
/// 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<benilla_assets::PortraitCamera>,
|
||||
/// The model's **model-frame pane camera** — raw camera-table index 1, the rig a 1.12
|
||||
/// `<PlayerModel>` widget renders through (wow-re `ui/scratch/modelframe-camera-law.md`;
|
||||
/// `<PlayerModel>` 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<benilla_assets::PortraitCamera>,
|
||||
/// 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<Vec3>,
|
||||
@@ -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
|
||||
|
||||
@@ -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<benilla_formats::GuildEmblem>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<RenderFade>`/`Without<PendingAppearFade>` 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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<u32>);
|
||||
|
||||
/// 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)]
|
||||
|
||||
@@ -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<u8>,
|
||||
},
|
||||
/// 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<u32> {
|
||||
match code {
|
||||
3 => Some(2), // SPELL_MISS_DODGE → the dispatch's DODGES state
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<f32> {
|
||||
anims?.find(ANIM_DECAY).map(|c| c.duration)
|
||||
}
|
||||
|
||||
@@ -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<f32>,
|
||||
/// 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RootState>,
|
||||
camera: Query<&GlobalTransform, With<WorldCamera>>,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Image>, 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() {
|
||||
|
||||
@@ -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 `<Button>` — every glue button is one — is dispatched from the mouse-**up**
|
||||
/// dispatcher `0x7792d0`; the mouse-**down** dispatcher `0x779210` fires nothing a glue screen
|
||||
/// registers. Two more predicates ride the release: the button must be in state `[+0x328] == 2`
|
||||
/// In the real 1.12.1 client, `CSimpleButton`'s ctor default click mask `[+0x330] = 0x100`
|
||||
/// (`0x7786d0`) is **`LeftButtonUp` alone**, so a stock `<Button>` — every glue button is one — is
|
||||
/// dispatched from the mouse-**up** dispatcher `0x7792d0`; the mouse-**down** dispatcher
|
||||
/// `0x779210` fires nothing a glue screen registers. Two more predicates ride the release: the
|
||||
/// button must be in state `[+0x328] == 2`
|
||||
/// (PUSHED — it took the press), and the release must **hit-test inside the frame** (`0x76b020`).
|
||||
/// So press-and-slide-off cancels, and press-off-slide-on does nothing — which is what a player
|
||||
/// expects of every button they have ever used. Our own in-game FrameXML path already implements
|
||||
@@ -161,8 +161,8 @@ pub(crate) const ROTATION_PER_UI_UNIT: f32 = 0.6 * std::f32::consts::PI / 180.0;
|
||||
/// `CHARACTER_ROTATION_CONSTANT` is per UI unit, not per pixel: `GetCursorPosition` (`0x46dad0`)
|
||||
/// answers on the glue engine's `aspect·768 × 768` virtual canvas, so the same physical drag turns
|
||||
/// the character *less* on a taller display — 0.6°/px only at a 768-line window, 0.427°/px at 1080p
|
||||
/// (wow-re `glue/scratch/glue-preview-facing-law.md`, 1533). Feeding it the raw pixel delta ran the
|
||||
/// drag ~40 % fast at 1080p and made it scale with the window instead of with the canvas.
|
||||
/// (1533). Feeding it the raw pixel delta ran the drag ~40 % fast at 1080p and made it scale with
|
||||
/// the window instead of with the canvas.
|
||||
///
|
||||
/// The divisor is the true `height / 768`, not [`screen_scale`]: that one is clamped at 2.2 so the
|
||||
/// *layout* stops growing on a very tall window, and the cursor's canvas has no such clamp.
|
||||
|
||||
@@ -67,8 +67,8 @@ pub(crate) struct ArtSwap {
|
||||
/// DEVICE pixel by [`super::seat_outline_copies`].
|
||||
///
|
||||
/// The reference's `outline="NORMAL"` is not string geometry at all: the ring is **baked into the
|
||||
/// glyph atlas cell by one 8-neighbour dilation pass at rasterization** (wow-re
|
||||
/// `font/scratch/outline-bake-tint.md` §3, byte-verified) — and the glyph rasterizes at final
|
||||
/// glyph atlas cell by one 8-neighbour dilation pass at rasterization** (`0x5ce440`/`0x5cea30`) —
|
||||
/// and the glyph rasterizes at final
|
||||
/// device-pixel size, so the ring is one device pixel hugging the glyph at every resolution
|
||||
/// (vanilla's famously thin outlines at high res). Offsetting copies by an authored *unit*
|
||||
/// instead put the ring 2–3 device px out at fullscreen scales, where it read as a separate
|
||||
@@ -186,7 +186,7 @@ pub(crate) fn outlined_text_centered<W: Bundle, T: Bundle>(
|
||||
}
|
||||
|
||||
/// Split a glue string into its coloured spans — WoW's `|cAARRGGBB…|r` inline markup, decoded by
|
||||
/// the byte-verified grammar ([`benilla_ui::markup`], RF-0087) instead of drawn literally.
|
||||
/// the reference's grammar ([`benilla_ui::markup`]) instead of drawn literally.
|
||||
///
|
||||
/// **This lives in the primitive on purpose.** It used to be one call site's private helper (the
|
||||
/// AddOns row title), which is exactly how the same `|cff0055FF…|r` came out coloured in a list
|
||||
@@ -554,11 +554,11 @@ const EDIT_LINE_HEIGHT: f32 = 1.2;
|
||||
/// The edit caret's colour: the reference's caret re-applies **`FONTINSTANCE.textColor`** whenever
|
||||
/// the font changes (`0x77e2a0`, mask bit 2) — the ctor's `0xFFFFFFFF` is only the pre-font default.
|
||||
/// So it is the box's text colour by law, not white by coincidence; both the line and the bar take
|
||||
/// it from here (wow-re `system/ui/scratch/rf85-editbox-caret.md`).
|
||||
/// it from here.
|
||||
const EDIT_TEXT_COLOR: Color = Color::WHITE;
|
||||
/// The edit caret is a drawn **bar**, not a character — a `CSimpleTexture` at `E+0x368`, allocated
|
||||
/// with a different allocator/tag/ctor than the `CSimpleFontString` beside it (wow-re
|
||||
/// `rf85-editbox-caret.md`, §5-verified; the cursor flush `0x77da80` fires `OnCursorChanged` with
|
||||
/// with a different allocator/tag/ctor than the `CSimpleFontString` beside it (`0x779c86` vs
|
||||
/// `0x779bee`; the cursor flush `0x77da80` fires `OnCursorChanged` with
|
||||
/// four float caret-*position* args). benilla used to append a `"|"` glyph (login) or a static `"_"`
|
||||
/// (create), which put a font's shape on a font's baseline and re-laid the text out every blink.
|
||||
///
|
||||
@@ -587,8 +587,8 @@ const CARET_W: f32 = 4.0;
|
||||
///
|
||||
/// This is also what the client does, and the two facts are the same fact: the caret is a
|
||||
/// `CSimpleTexture` quad at `drawLayer 3`, anchored LEFT-to-LEFT on the FontString with
|
||||
/// `x = W(lineStart → cursor)` — the measured advance of the text before the cursor (wow-re
|
||||
/// `rf85-editbox-caret.md` §1, §8, both §5-VERIFIED). A quad anchored *over* the line cannot
|
||||
/// `x = W(lineStart → cursor)` — the measured advance of the text before the cursor (`0x779c86`,
|
||||
/// `0x77da80`). A quad anchored *over* the line cannot
|
||||
/// displace it, and its left edge sits exactly on the seam flex puts us on. `drawLayer 3` is above
|
||||
/// the FontString's `2`, which is the [`ZIndex`] here.
|
||||
pub(crate) fn caret_bar<C: Bundle>(
|
||||
@@ -670,8 +670,7 @@ pub(crate) fn paint_glue_field<'a>(
|
||||
// (`0x77da80`) explicitly hides when `E != [0xcf4dc8]`, while the selection flush (`0x77d950`)
|
||||
// and its geometry worker (`0x77de70`) contain no read of the focus global anywhere, and the
|
||||
// per-frame update calls the flush BEFORE its own focus test. The quads show iff
|
||||
// `start < end` — focus never enters it (wow-re `editbox-selection-focus-law.md` §1-§3,
|
||||
// §5-VERIFIED, asked because this asymmetry looked wrong). An unfocused box that still holds a
|
||||
// `start < end` — focus never enters it. An unfocused box that still holds a
|
||||
// selection paints it, and that is correct; the login screen simply never leaves one behind
|
||||
// (`LoginForm::focus` collapses the box it leaves).
|
||||
let display = field.display();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! GameObject open/close animation (decision 0242; chest lid folded in by 2271) — a **client-side**
|
||||
//! `GAMEOBJECT_STATE` drives a skeletal M2 sequence, so a **door** swings, a **button** depresses, and a
|
||||
//! **chest lid** opens/closes on its §243 state machine.
|
||||
//! **chest lid** opens/closes on its state machine.
|
||||
//!
|
||||
//! **The model (2271, §5-VERIFIED):** the real client keeps *one* stored state per GameObject (the
|
||||
//! binary's `go+0x27c`) and *one* `SetGoState` that all callers funnel through; a change of that state
|
||||
//! plays the §243 transition. benilla mirrors that exactly — [`GoAnim::state`] is the single source of
|
||||
//! truth, written by the **three callers** the RE census pinned:
|
||||
//! **The model (2271):** the real client keeps *one* stored state per GameObject (the binary's
|
||||
//! `go+0x27c`) and *one* `SetGoState` (`0x5f8bd0`) that all callers funnel through; a change of
|
||||
//! that state plays the transition. benilla mirrors that exactly — [`GoAnim::state`] is the single
|
||||
//! source of truth, written by the **three callers** of that setter:
|
||||
//!
|
||||
//! 1. **the wire** ([`sync_wire_go_state`]) — a `GAMEOBJECT_STATE` UpdateField change. This is the
|
||||
//! door/button driver (the server flips their state over the wire) and the first-sight rest-pose seed.
|
||||
@@ -17,15 +17,15 @@
|
||||
//! state to READY, closing the lid, with no server round-trip (the client's loot-frame close handler).
|
||||
//!
|
||||
//! This supersedes 0244's "chests aren't animated": 0244 was right that the *wire* state never changes on
|
||||
//! loot, but wrong to conclude the chest is off the machine — it runs §243 identically, just fed from
|
||||
//! loot, but wrong to conclude the chest is off the machine — it runs identically, just fed from
|
||||
//! loot events instead of the wire. Wiring a chest to the *wire* watch alone gave it a state that never
|
||||
//! changed — the "instant open" glitch; feeding it from the loot events is the fix.
|
||||
//!
|
||||
//! The mechanism (wow-re `object-layer.md` §243 + `scratch/go-anim-state-machine.md`, §5-VERIFIED): the
|
||||
//! The mechanism (`0x5f3c30`, `0x5f3cb0`): the
|
||||
//! `GAMEOBJECT_STATE` value maps — **with no inversion**, the same polarity the sound path uses — to a
|
||||
//! held rest pose, and a *change* of state plays a one-shot transition motion that settles onto the new
|
||||
//! rest pose. The client keys the played sequence by its **AnimationData.dbc id** (the door-machine's
|
||||
//! internal index and the debug state-name strings at `0x860850` are both stale/off-by-one — the RE's
|
||||
//! internal index and the debug state-name strings at `0x860850` are both stale/off-by-one — the
|
||||
//! central trap; we key by id):
|
||||
//!
|
||||
//! | wire state | rest pose (held) | entered by motion (one-shot) |
|
||||
@@ -40,8 +40,8 @@
|
||||
//! Clips are keyed by AnimationData.dbc id, so a resolved id becomes a clip by a scan of
|
||||
//! [`ModelAnimations::clips`], exactly as `creature_anim` does.
|
||||
//!
|
||||
//! **A transition motion is a TRANSIENT substate, and the settle is explicit** (decision 1151,
|
||||
//! wow-re `gameobject-anim-arm.md` §2d/§3). The kernel's `flags` bit 0 says nothing about how long
|
||||
//! **A transition motion is a TRANSIENT substate, and the settle is explicit** (decision 1151).
|
||||
//! The kernel's `flags` bit 0 (`0x714585`) says nothing about how long
|
||||
//! a transition lasts: bit 0 clear means the *pose* wraps its band for ever, and the whole
|
||||
//! door family (`G_Crate01`, every `World\Goober\` prop, the books) authors Close/Open/Destroy
|
||||
//! that way. What ends a swing is the **object layer**: the model's completion callback fires once
|
||||
@@ -54,14 +54,15 @@
|
||||
//! instead is a chest lid that springs open and slams shut ~1.5×/s for ever, which is the report
|
||||
//! this record closes.
|
||||
//!
|
||||
//! The §243 **missing-sequence fallback** is no longer deferred: [`remap_missing`] implements the
|
||||
//! four-way remap, including the two legs that freeze a *motion* clip at frame 0 to stand in for an
|
||||
//! absent rest pose. It is not a corner case — the Ahn'Qiraj gate's roots (`AHN_QIRAJ_DOORROOTS`,
|
||||
//! Stand + Open only) took the "play nothing" path and rendered its 42-bone tangle at bind pose.
|
||||
//! The **missing-sequence fallback** (`0x5f3930`) is no longer deferred: [`remap_missing`]
|
||||
//! implements the four-way remap, including the two legs that freeze a *motion* clip at frame 0 to
|
||||
//! stand in for an absent rest pose. It is not a corner case — the Ahn'Qiraj gate's roots
|
||||
//! (`AHN_QIRAJ_DOORROOTS`, Stand + Open only) took the "play nothing" path and rendered its 42-bone
|
||||
//! tangle at bind pose.
|
||||
//!
|
||||
//! Still deferred (noted, not this slice): the **ANIMPROGRESS** half — the field selects the *motion*
|
||||
//! substate rather than the rest one at spawn, and seeks the clip to `duration × progress / 100`
|
||||
//! (wow-re `gameobject-anim-arm.md` §2b, `go-anim-state-machine.md`'s seek section); benilla reads
|
||||
//! (`0x5f3c30`, `0x5f3ac5`); benilla reads
|
||||
//! only `GAMEOBJECT_STATE` and always plays from frame 0. And the mid-flight **reverse blend**
|
||||
//! (interrupting a half-open door).
|
||||
|
||||
@@ -83,7 +84,7 @@ const GO_STATE_ACTIVE: u32 = 0;
|
||||
const GO_STATE_READY: u32 = 1;
|
||||
|
||||
/// `AnimationData.dbc` **157 Despawn** — the id the one-shot channel's code 6 resolves to
|
||||
/// (wow-re `gameobject-anim-arm.md` §2c: `0x80b0e0[6]` = substate 12, `0x8607e4[12]` = 157). The
|
||||
/// (`0x80b0e0[6]` = substate 12, `0x8607e4[12]` = 157). The
|
||||
/// object plays this once and *then* goes away; see [`DespawnAnimAnnounced`].
|
||||
const ANIM_DESPAWN: u16 = 157;
|
||||
|
||||
@@ -93,7 +94,7 @@ const ANIM_DESPAWN: u16 = 157;
|
||||
#[derive(Component, Default)]
|
||||
pub(crate) struct GoAnim {
|
||||
/// Client-authoritative `GAMEOBJECT_STATE` (the binary's stored `go+0x27c`) — the single source of
|
||||
/// truth for the §243 animation + collision. Written by the three "SetGoState callers": the wire
|
||||
/// truth for the animation + collision. Written by the three "SetGoState callers": the wire
|
||||
/// sync, the open-lock spell-go, and the loot-release. `None` until first sight.
|
||||
state: Option<u32>,
|
||||
/// The state we last *animated* to — resolves which transition motion to play next. Distinct from
|
||||
@@ -102,7 +103,7 @@ pub(crate) struct GoAnim {
|
||||
/// must not replay its swing).
|
||||
shown: Option<u32>,
|
||||
/// A pending **one-shot** play, as an `AnimationData.dbc` id — the second, disjoint arm
|
||||
/// channel of wow-re `gameobject-anim-arm.md` §2c, never the §243 lid family. The reference
|
||||
/// channel, never the lid family. The reference
|
||||
/// has ONE such slot fed by ONE entry point (`0x5f8c50(GO, code)` → slot 15), whose 7-entry
|
||||
/// code table `0x80b0e0` is the whole channel:
|
||||
///
|
||||
@@ -115,25 +116,25 @@ pub(crate) struct GoAnim {
|
||||
/// Written by [`queue_custom_anim`] / [`arm_despawn_anim`], consumed by [`drive_go_anim`]
|
||||
/// AFTER the state arm, so the bobber's same-frame pair (the forced `READY → ACTIVE` flip +
|
||||
/// the splash) resolves with the splash on top (decision 1086). Slot 15 pre-gates on the model
|
||||
/// OWNING the resolved id, so this channel takes no §2c remap (decisions 1086/1404).
|
||||
/// OWNING the resolved id, so this channel takes no remap (decisions 1086/1404).
|
||||
one_shot: Option<u16>,
|
||||
/// The clip armed for the current **TRANSIENT substate**, if any — the completion-retire's
|
||||
/// watch (decisions 1100/1151, wow-re `gameobject-anim-arm.md` §2d + `go-display-sound-events.md`
|
||||
/// §6-8). The reference keeps exactly one current substate in `[handler+0x10]`, and its
|
||||
/// watch (decisions 1100/1151). The reference keeps exactly one current substate in
|
||||
/// `[handler+0x10]`, and its
|
||||
/// per-model completion callback fires ONCE at the arm's baked window end (span × replay, the
|
||||
/// loop bit ignored); slot 14 `0x5f4120` then advances a transient substate onto its rest one
|
||||
/// and arms that pose over the transient clip. **Two families reach it, and they share this
|
||||
/// one slot exactly as the reference's does**: the §243 transition motions (2 Open / 4 Close /
|
||||
/// one slot exactly as the reference's does**: the transition motions (2 Open / 4 Close /
|
||||
/// 5 Destroy / 7 Rebuild) and the Custom0..3 block (8..11). [`retire_transient_anim`] models
|
||||
/// the advance for both; without it a bit-0-clear clip runs for ever — the bobber's splash
|
||||
/// looping ~1.3 s (1100's 2-3 audible splashes) and the crate lid never settling shut (1151).
|
||||
transient: Option<Transient>,
|
||||
/// The **rest** pose's armed clip node, when that pose is on the re-arm cycle below. Distinct
|
||||
/// from [`Self::transient`], which is the reference's ONE transient-substate slot
|
||||
/// (`[handler+0x10]`) and drives the §2d *advance*: a rest pose never advances, it re-arms
|
||||
/// (`[handler+0x10]`) and drives the *advance*: a rest pose never advances, it re-arms
|
||||
/// **itself**. `None` while a transient owns the model, on the rate-0 frozen leg (which never
|
||||
/// completes), and once the §2c already-playing check has refused the re-arm and the pose has
|
||||
/// settled for good.
|
||||
/// completes), and once the already-playing check (`0x5f39fa`) has refused the re-arm and the
|
||||
/// pose has settled for good.
|
||||
rest_window: Option<AnimationNodeIndex>,
|
||||
/// The state the machine last **dispatched** — the reference's `0x5f3cb0(old, new)` entry,
|
||||
/// distinct from [`Self::shown`] because the completion retire clears `shown` to force a
|
||||
@@ -144,7 +145,7 @@ pub(crate) struct GoAnim {
|
||||
/// display-sound loop registration ([`GoStateDispatch`]).
|
||||
dispatched: Option<u32>,
|
||||
/// The **requested** animation id currently armed — the reference's `[block0+0xf8]`, written by
|
||||
/// op4 (`0x71252f`) and read back by `0x712090(model, -1)`. The §2c *remap* legs consult it
|
||||
/// op4 (`0x71252f`) and read back by `0x712090(model, -1)`. The *remap* legs consult it
|
||||
/// (`0x5f39fa: cmp esi,eax; je 0x5f3b32`) and refuse an arm that would request what is already
|
||||
/// playing; the model-owns-it leg (`0x5f396c jne`) and the collapse-to-Stand leg
|
||||
/// (`0x5f3a54 jmp 0x5f3a0b`, past the check) do not. That asymmetry is the whole difference
|
||||
@@ -154,7 +155,7 @@ pub(crate) struct GoAnim {
|
||||
|
||||
/// The GameObject **state-machine dispatch** ran on this entity — the reference's `0x5f3cb0`,
|
||||
/// whose first act is `0x5f3cc8 call 0x5f40c0`: **release whatever display-sound loop the object
|
||||
/// holds** (wow-re `go-display-sound-events.md` §2/§6c). Consumed by
|
||||
/// holds**. Consumed by
|
||||
/// [`crate::sound::gameobject`], which owns that registration; the animation half of the dispatch
|
||||
/// is [`drive_go_anim`]'s own work and needs no message.
|
||||
///
|
||||
@@ -192,7 +193,7 @@ struct Transient {
|
||||
}
|
||||
|
||||
/// The GameObject's **stored** state — the binary's `go+0x27c`, which is what every consumer reads
|
||||
/// (the §243 animation, `usable`'s state pre-gate, and the lock chain's per-slot Action gate,
|
||||
/// (the animation, `usable`'s state pre-gate, and the lock chain's per-slot Action gate,
|
||||
/// decision 0752). [`GoAnim::state`] when the object is on the animation machine (it carries the
|
||||
/// client-side predictions the wire never sends — a chest's lid), else the wire field, else the
|
||||
/// wire default `0` = ACTIVE for a field vmangos omitted because it was zero.
|
||||
@@ -202,10 +203,10 @@ pub(crate) fn go_state(anim: Option<&GoAnim>, store: &ObjectStore) -> u32 {
|
||||
.unwrap_or(GO_STATE_ACTIVE)
|
||||
}
|
||||
|
||||
/// The inspector's GameObject **animation** readout (decision 1151): which sequence the §243 arm
|
||||
/// The inspector's GameObject **animation** readout (decision 1151): which sequence the state arm
|
||||
/// is actually playing on the object under the cursor — its `AnimationData` id, whether it is the
|
||||
/// state's held **rest** pose or a **transient** one (a transition motion, or a Custom block, which
|
||||
/// [`retire_transient_anim`]'s §2d advance ends at its window end), and the repeat the player is
|
||||
/// [`retire_transient_anim`]'s advance ends at its window end), and the repeat the player is
|
||||
/// running it under. `None` when the object renders as a static mesh, or nothing is armed yet.
|
||||
///
|
||||
/// The line that closes the loop on this whole class of report. "The crate is stuck open/closing"
|
||||
@@ -233,8 +234,8 @@ pub(crate) fn armed_anim(
|
||||
))
|
||||
}
|
||||
|
||||
/// Which GameObject types get the state-driven **animated** instance (skinned lid/door + §243
|
||||
/// sequences) — **the byte-verified type census**, not a guess (wow-re `gameobject-anim-arm.md` §2f).
|
||||
/// Which GameObject types get the state-driven **animated** instance (skinned lid/door + state
|
||||
/// sequences) — **read off the binary's type dispatch**, not a guess.
|
||||
///
|
||||
/// `CGGameObject::LoadBaseObject` dispatches on the wire TYPE_ID through the 31-entry jump table at
|
||||
/// `0x5f76cc` (`cmp ecx,0x1e` + *unsigned* `ja`, so 0..30) and allocates a per-type strategy handler
|
||||
@@ -242,7 +243,7 @@ pub(crate) fn armed_anim(
|
||||
/// **allocation size**: every type whose handler is `0x1c` bytes gets a 36-slot vtable carrying the
|
||||
/// real `0x5f3c30`/`0x5f3b50` arm; every other size gets a 34-slot vtable whose corresponding slots
|
||||
/// are the abstract base's do-nothing bodies — and whose `+0x88` doesn't even exist (reading it walks
|
||||
/// into the next vtable, which is what made an earlier census report plausible nonsense).
|
||||
/// into the next vtable, which is what makes a naive census of `+0x88` report plausible nonsense).
|
||||
///
|
||||
/// So the machine is **20 of the 31 types**, not the three we had:
|
||||
///
|
||||
@@ -336,10 +337,9 @@ impl Play {
|
||||
}
|
||||
}
|
||||
|
||||
/// The §2c **missing-sequence remap** — what the arm actually requests when the model doesn't author
|
||||
/// the state's animation id. Byte-verified twice over (wow-re `gameobject-anim-arm.md` §2c's
|
||||
/// `0x5f3972` jump table `0x5f3b40`, and `go-anim-state-machine.md`'s independent read of the same
|
||||
/// switch), it is a four-way table keyed on the id, consulted only after the ownership test
|
||||
/// The **missing-sequence remap** — what the arm actually requests when the model doesn't author
|
||||
/// the state's animation id. Read off `0x5f3972`'s jump table `0x5f3b40`, it is a four-way table
|
||||
/// keyed on the id, consulted only after the ownership test
|
||||
/// [`ModelAnimations::owns`] (the reference's `0x711960`) says no:
|
||||
///
|
||||
/// | missing | condition | requests instead |
|
||||
@@ -386,7 +386,8 @@ fn remap_missing(anims: &ModelAnimations, id: u16) -> (u16, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The held rest-pose animation-id for a wire `GAMEOBJECT_STATE` (§243). `None` for an unmapped state.
|
||||
/// The held rest-pose animation-id for a wire `GAMEOBJECT_STATE` (`0x5f3c30`). `None` for an
|
||||
/// unmapped state.
|
||||
fn rest_anim(state: u32) -> Option<u16> {
|
||||
match state {
|
||||
0 => Some(0x95), // ACTIVE → Opened (held open)
|
||||
@@ -396,10 +397,10 @@ fn rest_anim(state: u32) -> Option<u16> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The transition-motion animation-id for a `prev → cur` state change (§243), i.e. the swing. `None`
|
||||
/// The transition-motion animation-id for a `prev → cur` state change, i.e. the swing. `None`
|
||||
/// when the pair has no distinct motion (falls back to snapping the rest pose).
|
||||
///
|
||||
/// The reference (`0x5f3cb0`, byte-verified in wow-re `go-anim-state-machine.md`) dispatches on the
|
||||
/// The reference (`0x5f3cb0`) dispatches on the
|
||||
/// **NEW** state and lets OLD pick transient-vs-rest: NEW 0 takes the Open motion from OLD 1 (else
|
||||
/// rests Opened), NEW 1 takes Close from OLD 0 / Rebuild from OLD 2 (else rests Closed), NEW 2 takes
|
||||
/// Destroy from OLD 1 (else rests Destroyed). Our `(_, 2)` wildcard got that last row wrong: an
|
||||
@@ -430,7 +431,8 @@ fn resolve(prev: Option<u32>, cur: u32) -> Option<Play> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Caller 1 (the wire, §243): track each animated GO's `GAMEOBJECT_STATE` from the wire, acting only on a
|
||||
/// Caller 1 (the wire, `0x5f89e0`): track each animated GO's `GAMEOBJECT_STATE` from the wire,
|
||||
/// acting only on a
|
||||
/// *genuine* wire change. This is the door/button driver (the server flips their state over the wire) and
|
||||
/// the first-sight rest-pose seed for every animated GO (a chest streams in closed). Two inputs, both
|
||||
/// exact (decision 2297): the seed (`Added<GoAnim>`, when attach tags the entity — read off the store),
|
||||
@@ -495,7 +497,7 @@ fn open_go_lid(
|
||||
}
|
||||
|
||||
/// Caller 4 (the custom-anim opcode, decision 1086): queue the one-shot Custom play. This is the
|
||||
/// **disjoint** arm channel of wow-re `gameobject-anim-arm.md` §step 8 — it never touches
|
||||
/// **disjoint** arm channel (`0x5f8c50`) — it never touches
|
||||
/// [`GoAnim::state`] (the lid family), rejects `anim_id >= 4` exactly as the reference handler
|
||||
/// does, and maps the index to its AnimationData id (`153 + n`, Custom0..3). Ownership is judged
|
||||
/// at play time by [`drive_go_anim`] (the model components live there); a guid with no [`GoAnim`]
|
||||
@@ -508,7 +510,7 @@ fn queue_custom_anim(
|
||||
) {
|
||||
for GoCustomAnim { go_guid, anim_id } in plays.read().copied() {
|
||||
let Some(id) = custom_anim_id(anim_id) else {
|
||||
continue; // the reference handler's own reject (step 8)
|
||||
continue; // the reference handler's own reject (`0x5f8971`)
|
||||
};
|
||||
let Some(&e) = index.0.get(&go_guid) else {
|
||||
continue;
|
||||
@@ -520,8 +522,8 @@ fn queue_custom_anim(
|
||||
}
|
||||
|
||||
/// The wire Custom index → its AnimationData id (`153 + n`, Custom0..3), or `None` for the
|
||||
/// reference handler's reject (`anim_id >= 4` — wow-re `gameobject-anim-arm.md` step 8's
|
||||
/// "opcode `0xb3` byte `b` (reject `b >= 4`), substate `8+b`").
|
||||
/// reference handler's reject (`anim_id >= 4` — `0x5f8971`: opcode `0xb3` byte `b`, reject
|
||||
/// `b >= 4`, substate `8+b`).
|
||||
fn custom_anim_id(anim_id: u32) -> Option<u16> {
|
||||
(anim_id < 4).then(|| 153 + anim_id as u16)
|
||||
}
|
||||
@@ -541,8 +543,8 @@ fn custom_anim_id(anim_id: u32) -> Option<u16> {
|
||||
pub(crate) struct DespawnAnimAnnounced;
|
||||
|
||||
/// The reference's **pending-destroy mark** — `[GO+0xe4]` bit `0x10`, set by the object-manager
|
||||
/// destroy `0x464920` when it finds the object still pinned (wow-re
|
||||
/// `go-display-sound-events.md` §6d, §5-VERIFIED). The arm takes the pin whenever the armed
|
||||
/// destroy `0x464920` when it finds the object still pinned.
|
||||
/// The arm takes the pin whenever the armed
|
||||
/// substate is not a resting one (`0x5f3b27 call 0x4683e0` → refcount `[obj+0xe8]`), slot 14's
|
||||
/// footer releases it at the window end (`0x468410`), and only then does the deferred destroy run
|
||||
/// (`0x46844a call 0x464920`) — which is how an object gets to finish its own despawn animation
|
||||
@@ -641,9 +643,9 @@ fn close_go_lid(
|
||||
*last_source = current;
|
||||
}
|
||||
|
||||
/// The **completion-driven retire** of the current TRANSIENT substate — the §2d advance
|
||||
/// (decisions 1100/1151; wow-re `gameobject-anim-arm.md` §2d/§3 + `go-display-sound-events.md`
|
||||
/// §6-8, §5-verified). The reference registers a per-model completion callback at GO model attach
|
||||
/// The **completion-driven retire** of the current TRANSIENT substate — the substate advance
|
||||
/// (decisions 1100/1151).
|
||||
/// The reference registers a per-model completion callback at GO model attach
|
||||
/// (`0x5f7d43` → `[M2+0x70]`) which the driver `0x719370` fires ONCE when the armed sequence
|
||||
/// reaches its baked window end — span × replay-count, the **loop bit ignored** — and slot 14
|
||||
/// `0x5f4120` dispatches on the current substate:
|
||||
@@ -667,12 +669,10 @@ fn close_go_lid(
|
||||
/// **The last row is not "nothing", and 1151 read it as such.** `0x5f4167` is byte-for-byte slot
|
||||
/// 34's own selection and calls `0x5f3930` **directly**, bypassing slot 34's change guard on an
|
||||
/// unchanged `[handler+0x10]` — so a resting substate re-issues its own op4, and with it a fresh
|
||||
/// `variationIdx = -1` roll, every `span × R` ms for ever (wow-re `gameobject-anim-arm.md` §6c/§6d,
|
||||
/// §5-arbitrated). The completion that drives it ignores the loop bit (`0x719503` tests it only
|
||||
/// after the notify block), so a bit-0-CLEAR pose completes too. That is why Onyxia's lava traps
|
||||
/// spurt *continuously* rather than once at stream-in, and it is the same law `loop-replay-fidget.md`
|
||||
/// §7 and `doodad-anim-host.md` §5 already carried for units and placed doodads — the GameObject
|
||||
/// note was the outlier.
|
||||
/// `variationIdx = -1` roll, every `span × R` ms for ever. The completion that drives it ignores
|
||||
/// the loop bit (`0x719503` tests it only after the notify block), so a bit-0-CLEAR pose completes
|
||||
/// too. That is why Onyxia's lava traps spurt *continuously* rather than once at stream-in, and it
|
||||
/// is the same law units and placed doodads follow.
|
||||
///
|
||||
/// Benilla therefore arms EVERY clip `Never`-repeat (one window, the same endpoint), so "the
|
||||
/// window ended" is the player's finished flag; the retire then clears `shown`, which makes the
|
||||
@@ -724,7 +724,7 @@ fn retire_transient_anim(
|
||||
}
|
||||
|
||||
/// **The GameObject arm's instrument** (`WOW_MOVE_TRACE_TAGS=goa`) — one line per arm, carrying
|
||||
/// everything the roll decided: the requested `AnimationData.dbc` id, what the §2c remap turned it
|
||||
/// everything the roll decided: the requested `AnimationData.dbc` id, what the remap turned it
|
||||
/// into, the `_rand` draw itself, and the **file sequence slot** the weighted walk landed on.
|
||||
///
|
||||
/// That last column is the one this exists for. A GameObject's variation chain is not cosmetic:
|
||||
@@ -760,7 +760,7 @@ fn trace_arm(
|
||||
);
|
||||
}
|
||||
|
||||
/// Play the §243 sequence for a change of the client-side [`GoAnim::state`] (written by any of the three
|
||||
/// Play the sequence for a change of the client-side [`GoAnim::state`] (written by any of the three
|
||||
/// callers). Mirrors the state-transition detection of [`crate::sound::gameobject`] (first sight silent),
|
||||
/// but points it at the model instead of the mixer — one system owns the visual, the other the audio.
|
||||
fn drive_go_anim(
|
||||
@@ -775,12 +775,12 @@ fn drive_go_anim(
|
||||
Changed<GoAnim>,
|
||||
>,
|
||||
mut dispatch: MessageWriter<GoStateDispatch>,
|
||||
// The client's single `_rand` stream (wow-re `rf36-rand-stub.md`), as `creature_anim` keeps
|
||||
// The client's single `_rand` stream (`0x7400e5`), as `creature_anim` keeps
|
||||
// one: op4's variation roll draws from it on every GameObject arm below.
|
||||
mut rng: ResMut<benilla_assets::AnimRng>,
|
||||
) {
|
||||
for (entity, mut go, mut player, mut tr, anims) in &mut gos {
|
||||
// ── The §243 state arm ─────────────────────────────────────────────────────────────────
|
||||
// ── The state arm ──────────────────────────────────────────────────────────────────────
|
||||
if let Some(state) = go.state {
|
||||
if go.shown != Some(state) {
|
||||
let prev = go.shown;
|
||||
@@ -800,11 +800,11 @@ fn drive_go_anim(
|
||||
go.rest_window = None;
|
||||
if let Some(play) = resolve(prev, state) {
|
||||
// Resolve the id to this model's clip (keyed by AnimationData.dbc id, as
|
||||
// `creature_anim` does), through the §2c remap for a model that doesn't author
|
||||
// `creature_anim` does), through the remap for a model that doesn't author
|
||||
// it — that is what keeps a lidless model on a real pose instead of bind.
|
||||
let (want, frozen) = remap_missing(anims, play.anim_id());
|
||||
// **`variationIdx = -1` — the arm ROLLS a variation** (wow-re
|
||||
// `gameobject-anim-arm.md` §2c, `0x5f3aee: push -1`), unlike the §1 loader seed
|
||||
// **`variationIdx = -1` — the arm ROLLS a variation** (`0x5f3aee: push -1`),
|
||||
// unlike the loader seed
|
||||
// beneath it, which passes an explicit `0` (`0x710189`). Where a model authors
|
||||
// one sequence per id the two are the same clip and nothing changes; where it
|
||||
// authors a chain they are not, and taking the head is a whole authored
|
||||
@@ -814,7 +814,7 @@ fn drive_go_anim(
|
||||
// the 300/s `LAVALUMP2` burst, the lava spurting out of the floor. Every one of
|
||||
// the 208 trap GameObjects in the lair took the head, so the chamber's whole
|
||||
// ember field was missing.
|
||||
// **The §2c already-playing skip, and it decides settle-vs-cycle.** Three
|
||||
// **The already-playing skip, and it decides settle-vs-cycle.** Three
|
||||
// edges reach the arm (`0x5f3a0b`): the model OWNS the table id
|
||||
// (`0x5f396c jne`), the model is not live, and the 147-absent collapse to
|
||||
// Stand (`0x5f3a52 xor esi,esi; 0x5f3a54 jmp 0x5f3a0b` — a backwards jump
|
||||
@@ -854,8 +854,7 @@ fn drive_go_anim(
|
||||
// parked at rate 0 (the same Open clip serves both) would stay stuck.
|
||||
active.set_speed(1.0);
|
||||
// **ONE baked window, rest pose included** (decision 1151 for the
|
||||
// motions; wow-re
|
||||
// `gameobject-anim-arm.md` §6, correcting 1151's "nothing advances off
|
||||
// motions; correcting 1151's "nothing advances off
|
||||
// a held pose"). The completion notification `0x719370` fires at
|
||||
// `span × R` **without consulting the loop bit** (`0x719503` tests it
|
||||
// only afterwards), so a bit-0-CLEAR rest clip completes too — and
|
||||
@@ -868,10 +867,9 @@ fn drive_go_anim(
|
||||
// a 10 % weight, and with a 5 s ember life that leaves **47 % of them
|
||||
// holding a live plume at any instant** (measured, `WOW_PARTICLE_CENSUS`
|
||||
// in the lair: 49 of 104), instead of one flurry at stream-in. The
|
||||
// period, the re-roll and `R = 1` from a `(0,0)` replay pair are all
|
||||
// byte-converged across two independent §5 rounds; what is *not*
|
||||
// settled is which per-frame advance list a GameObject's model rides
|
||||
// (wow-re `gameobject-anim-arm.md` §6's named residual).
|
||||
// period (`0x7126d8`), the re-roll and `R = 1` from a `(0,0)` replay
|
||||
// pair (`0x7126c3`) are all settled in the binary; what is *not*
|
||||
// settled is which per-frame advance list a GameObject's model rides.
|
||||
active.set_repeat(RepeatAnimation::Never);
|
||||
match play {
|
||||
Play::Motion(_) => {
|
||||
@@ -886,9 +884,9 @@ fn drive_go_anim(
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── The one-shot Custom channel (step 8, decision 1086) — AFTER the state arm, so the
|
||||
// ── The one-shot Custom channel (`0x5f8c50`, decision 1086) — AFTER the state arm, so the
|
||||
// bobber's same-frame pair (forced READY→ACTIVE flip + splash) lands splash-on-top.
|
||||
// Gated on the model OWNING the id; no §2c remap on this channel — an unowned Custom
|
||||
// Gated on the model OWNING the id; no remap on this channel — an unowned Custom
|
||||
// plays nothing. Armed for ONE window regardless of the sequence's loop flag (decision
|
||||
// 1099, correcting 1090's loop-forever): the kernel does loop a bit0-clear clip and
|
||||
// re-fires its events per pass, but the reference's COMPLETION callback fires at window
|
||||
@@ -901,8 +899,8 @@ fn drive_go_anim(
|
||||
if go.one_shot.is_some() {
|
||||
let id = go.one_shot.take().expect("checked is_some");
|
||||
if anims.owns(id) {
|
||||
// Slot 15 funnels through the same slot-34 → `0x5f3930` arm as the state channel
|
||||
// (§2c), so it rolls a variation for the same reason — a Custom0 with two authored
|
||||
// Slot 15 funnels through the same slot-34 → `0x5f3930` arm as the state channel,
|
||||
// so it rolls a variation for the same reason — a Custom0 with two authored
|
||||
// takes alternates them.
|
||||
let roll = rng.draw();
|
||||
if let Some(clip) = anims.pick_variation(id, roll) {
|
||||
@@ -983,8 +981,8 @@ fn drive_go_collision(
|
||||
/// registrar pair. The consumer is a single instruction, `0x5f85f6 test ah,ah` inside `0x5f85f0` —
|
||||
/// `CGGameObject_C`'s collision-candidacy virtual at primary-vtable slot `+0x50` — which reads the
|
||||
/// bit through the sign flag and answers *no candidate* for a GameObject whose
|
||||
/// `GAMEOBJECT_TYPE_ID` is `0`, DOOR. Nothing else about a ghost's collision differs (wow-re
|
||||
/// `collision/scratch/ghost-door-tracemask.md`, decision 1767).
|
||||
/// `GAMEOBJECT_TYPE_ID` is `0`, DOOR. Nothing else about a ghost's collision differs
|
||||
/// (decision 1767).
|
||||
///
|
||||
/// **Off the MOVER's descriptor, not ours.** The producer tests the object being traced, so a
|
||||
/// possessed creature is not a player and never sets the bit; `player_is_ghost` reading a field a
|
||||
@@ -1042,8 +1040,8 @@ type ScannedGo = (
|
||||
);
|
||||
|
||||
/// Fire the event keyframes an animated GameObject's playing clip crossed this frame — the GO
|
||||
/// half of the M2 event-kernel surface (wow-re `go-display-sound-events.md`, the 1086 fold-back
|
||||
/// record): the reference registers an event callback per **family-A** GO at create
|
||||
/// half of the M2 event-kernel surface (decision 1086): the reference registers an event callback
|
||||
/// per **family-A** GO at create
|
||||
/// (`0x5f7d1f` → vtable `+0x30` → dispatcher `0x5f3e20`), which is exactly the [`GoAnim`]
|
||||
/// population — a loader-idle family-B GO has no dispatcher and stays silent. The events flow
|
||||
/// into the same [`AnimSoundEvent`] stream the creature scanner feeds: the generic `$SND`/`$DSO`/
|
||||
@@ -1203,7 +1201,7 @@ mod tests {
|
||||
assert!(matches!(resolve(Some(2), 1), Some(Play::Motion(0x98))));
|
||||
}
|
||||
|
||||
/// The custom-anim channel's wire mapping (step 8, decision 1086): indices 0..3 arm
|
||||
/// The custom-anim channel's wire mapping (`0x5f8930`, decision 1086): indices 0..3 arm
|
||||
/// Custom0..3 (AnimationData 153..156); anything else is the reference handler's reject.
|
||||
/// The fishing bobber's bite is index 0 → 153 — exactly the second sequence
|
||||
/// `G_FishingBobber.m2` authors.
|
||||
@@ -1386,7 +1384,7 @@ mod tests {
|
||||
///
|
||||
/// The whole door family is `flags` bit 0 clear, so a driver that arms a transition by the
|
||||
/// clip's loop bit arms Close on `Forever`: the lid jumps back to 75° every 667 ms. What ends
|
||||
/// a transition in the reference is the object layer's §2d advance (slot 14 `0x5f4120`:
|
||||
/// a transition in the reference is the object layer's advance (slot 14 `0x5f4120`:
|
||||
/// substate 4 Close → 1 Closed), driven by the completion callback at the arm's baked window
|
||||
/// — the loop bit ignored. This runs the whole click-to-settle cycle through the real systems
|
||||
/// and real clips, and then keeps running: two seconds past the swing, three Close windows
|
||||
@@ -1405,7 +1403,7 @@ mod tests {
|
||||
// Streamed in closed: the rest pose, snapped. It is ONE baked window like everything else
|
||||
// — `0x5f4167` re-arms a resting substate through `0x5f3930` at its own completion, so the
|
||||
// pose repeats by re-arming (and re-rolling its variation), not by the kernel's loop bit
|
||||
// (wow-re `gameobject-anim-arm.md` §6; this corrects 1151's "nothing advances off a held
|
||||
// (this corrects 1151's "nothing advances off a held
|
||||
// pose"). What must hold across those windows is the armed ID, asserted below.
|
||||
app.update();
|
||||
assert_eq!(armed(&app, go), Some((0x93, RepeatAnimation::Never)));
|
||||
@@ -1454,8 +1452,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The arm rolls a VARIATION** — `variationIdx = -1` (wow-re `gameobject-anim-arm.md` §2c,
|
||||
/// `0x5f3aee: push -1`), not the id's head. The §1 loader seed under it takes an explicit
|
||||
/// **The arm rolls a VARIATION** — `variationIdx = -1` (`0x5f3aee: push -1`), not the id's
|
||||
/// head. The loader seed under it takes an explicit
|
||||
/// variation 0 (`0x710189`), so a model's second and later takes are reachable ONLY through
|
||||
/// this arm; resolving the id with `find()` made them unreachable everywhere.
|
||||
///
|
||||
@@ -1527,7 +1525,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **A rest pose is ONE window, and its completion re-arms it — with a FRESH roll, for ever**
|
||||
/// (wow-re `gameobject-anim-arm.md` §6c/§6d, correcting 1151's "nothing advances off a held
|
||||
/// (correcting 1151's "nothing advances off a held
|
||||
/// pose"). `0x719370` fires the completion at `span × R` without consulting the loop bit
|
||||
/// (`0x719503` tests it only afterwards), and slot 14's resting row `0x5f4167` calls
|
||||
/// `0x5f3930` directly — past slot 34's change guard — on an unchanged substate.
|
||||
@@ -1566,12 +1564,12 @@ mod tests {
|
||||
"over 300 windows the pose showed takes {seen:?} — a rest pose that never re-armed \
|
||||
would show exactly one, and Onyxia's floor would spurt once and go quiet"
|
||||
);
|
||||
// The pose itself never wanders off Closed: re-arming is not the §2d substate advance.
|
||||
// The pose itself never wanders off Closed: re-arming is not the substate advance.
|
||||
assert!(matches!(armed(&app, go), Some((0x93, _))));
|
||||
}
|
||||
|
||||
/// **The state dispatch is an EDGE, and a rest pose re-arming itself is not one** (wow-re
|
||||
/// `go-display-sound-events.md` §6c: slot 14 sends a resting substate to `0x5f4167`, which
|
||||
/// **The state dispatch is an EDGE, and a rest pose re-arming itself is not one** (slot 14
|
||||
/// sends a resting substate to `0x5f4167`, which
|
||||
/// calls `0x5f3930` directly and never re-enters `0x5f3cb0`). That is the whole reason a
|
||||
/// brazier's `TorchLoop` — a looping display-slot kit registered by a `$GO2` on the Opened
|
||||
/// rest pose — hums continuously instead of being released and restarted every band pass,
|
||||
|
||||
@@ -74,8 +74,7 @@ pub(crate) struct GoTemplate {
|
||||
pub(crate) text_page: Option<TextPage>,
|
||||
/// The `PageTextMaterial.dbc` id `GetQuestBackgroundMaterial` answers for a quest sourced from
|
||||
/// this object — by `GAMEOBJECT_TYPE_ID`: QUESTGIVER (2) and GOOBER (9) read `data[2]`, CHEST
|
||||
/// (10) reads `data[9]`, every other type answers none (`0x5f5950`, wow-re
|
||||
/// quest-material-reward-spell-bindings.md §1).
|
||||
/// (10) reads `data[9]`, every other type answers none (`0x5f5950`).
|
||||
pub(crate) quest_material: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -241,8 +240,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The quest-giver material arm — by type: QUESTGIVER (2) and GOOBER (9) read `data[2]`,
|
||||
/// CHEST (10) reads `data[9]`, anything else none (`0x5f5950`, wow-re
|
||||
/// quest-material-reward-spell-bindings.md §1).
|
||||
/// CHEST (10) reads `data[9]`, anything else none (`0x5f5950`).
|
||||
#[test]
|
||||
fn insert_captures_the_quest_material_by_type() {
|
||||
let mut t = GameObjectTemplates::default();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user