From d8f4f31ea1c9fd391da94d20b415c7741f94a6c7 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:06:42 +0700 Subject: [PATCH] Comments in benilla-app, the player, entities, animation and targeting: one line of fact each, no history, no record numbers (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #41: the third of five `benilla-app` pieces. This one is the player, the entities, animation and targeting: the mover, the camera, movement on the wire, the body and creature animation drivers, spell visuals, equipment and corpses, and the target, cursor and by-name layers. Their comments are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 26,066 comment lines become 9,042. Fixes #104. Fixes #108. Fixes #109. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - The follow gate's `0x606ba0` is `CanCooperate` (a faction-group-mask test), not `CanAssist` (`0x6066f0`). - The facing report is sent from the move-command drain (`0x615fc6`); `0x617100`/`0x617170` are the transport-GUID heartbeat. - vmangos turns a corpse into bones under the corpse's own guid (`Map.cpp:3608-3649`), and the bones get new flags and zeroed item slots. - Two claims about the item models are refuted by the shipped data: not every helm stem ships all sixteen race and sex files, and some item emitters sit under a keyed billboard chain. - Docs sitting on the wrong item, stale function names, and disassembler labels, which become client addresses. - vmangos line citations are checked against the pinned vmangos revision. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour, or one whose reference side is untraced, says so plainly. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Six second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the shipped data, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#293, #294, #301, #308, #316 and #317 among them). The comments here state what the code does. String literals are untouched; they are #92. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-app/src/aura_visual.rs | 496 ++---- crates/benilla-app/src/aura_visual/tests.rs | 172 +- crates/benilla-app/src/creature_anim.rs | 1003 +++-------- crates/benilla-app/src/creature_anim/blood.rs | 77 +- .../benilla-app/src/creature_anim/breath.rs | 169 +- .../benilla-app/src/creature_anim/driver.rs | 947 +++------- .../src/creature_anim/driver/grip.rs | 16 +- .../src/creature_anim/driver/mode.rs | 327 +--- .../src/creature_anim/driver/play.rs | 244 +-- .../src/creature_anim/driver/tests.rs | 877 +++------ .../src/creature_anim/driver/wound.rs | 85 +- .../src/creature_anim/emote_anim.rs | 107 +- .../src/creature_anim/env_damage.rs | 62 +- .../benilla-app/src/creature_anim/events.rs | 281 +-- .../benilla-app/src/creature_anim/gesture.rs | 105 +- .../benilla-app/src/creature_anim/impact.rs | 196 +-- crates/benilla-app/src/creature_anim/lod.rs | 202 +-- crates/benilla-app/src/creature_anim/net.rs | 144 +- .../benilla-app/src/creature_anim/select.rs | 878 ++------- .../src/creature_anim/select/tests.rs | 308 ++-- .../benilla-app/src/creature_anim/sheath.rs | 245 +-- .../src/creature_anim/spell_visual.rs | 953 +++------- .../src/creature_anim/spell_visual/tests.rs | 299 ++-- crates/benilla-app/src/creature_anim/twist.rs | 123 +- crates/benilla-app/src/entities.rs | 773 +++----- .../src/entities/attach/char_skin.rs | 217 +-- .../benilla-app/src/entities/attach/dress.rs | 338 +--- .../benilla-app/src/entities/attach/merge.rs | 101 +- crates/benilla-app/src/entities/attach/mod.rs | 691 ++------ .../src/entities/attach/preview.rs | 486 ++--- .../src/entities/attach/redress.rs | 168 +- .../benilla-app/src/entities/carried_light.rs | 57 +- crates/benilla-app/src/entities/chain_beam.rs | 398 ++--- .../src/entities/collision_height.rs | 136 +- crates/benilla-app/src/entities/conform.rs | 95 +- crates/benilla-app/src/entities/corpse.rs | 198 +-- crates/benilla-app/src/entities/dest_fx.rs | 139 +- crates/benilla-app/src/entities/display.rs | 453 ++--- .../benilla-app/src/entities/equipment/mod.rs | 245 +-- .../src/entities/equipment/resolve.rs | 461 ++--- .../src/entities/equipment/spawn.rs | 616 ++----- crates/benilla-app/src/entities/item_glow.rs | 169 +- .../benilla-app/src/entities/live_display.rs | 145 +- crates/benilla-app/src/entities/missile.rs | 480 ++--- crates/benilla-app/src/entities/mount.rs | 183 +- .../benilla-app/src/entities/mount/tests.rs | 60 +- .../src/entities/spell_fx/lifecycle.rs | 166 +- .../benilla-app/src/entities/spell_fx/mod.rs | 622 ++----- crates/benilla-app/src/entities/wmo_props.rs | 271 +-- crates/benilla-app/src/go_anim.rs | 1002 +++-------- crates/benilla-app/src/player.rs | 365 ++-- crates/benilla-app/src/player/arc.rs | 164 +- crates/benilla-app/src/player/body_pose.rs | 91 +- crates/benilla-app/src/player/camera.rs | 1400 ++++----------- .../benilla-app/src/player/camera_channel.rs | 161 +- .../benilla-app/src/player/camera_dynamics.rs | 620 ++----- crates/benilla-app/src/player/camera_saved.rs | 158 +- crates/benilla-app/src/player/camera_view.rs | 273 +-- crates/benilla-app/src/player/camera_water.rs | 219 +-- crates/benilla-app/src/player/controller.rs | 770 ++------ crates/benilla-app/src/player/drunk.rs | 81 +- crates/benilla-app/src/player/embody.rs | 120 +- crates/benilla-app/src/player/flags.rs | 190 +- crates/benilla-app/src/player/follow.rs | 305 +--- crates/benilla-app/src/player/gait.rs | 56 +- crates/benilla-app/src/player/input.rs | 188 +- crates/benilla-app/src/player/land.rs | 48 +- crates/benilla-app/src/player/move_trace.rs | 217 +-- crates/benilla-app/src/player/movement_net.rs | 606 ++----- crates/benilla-app/src/player/mover.rs | 1442 ++++----------- crates/benilla-app/src/player/net.rs | 8 +- crates/benilla-app/src/player/posture.rs | 145 +- crates/benilla-app/src/player/ride.rs | 88 +- crates/benilla-app/src/player/scoped_view.rs | 61 +- crates/benilla-app/src/player/server_ride.rs | 241 +-- crates/benilla-app/src/player/setup.rs | 118 +- crates/benilla-app/src/player/state.rs | 1564 ++++------------- crates/benilla-app/src/player/step_probe.rs | 101 +- crates/benilla-app/src/player/swim.rs | 569 ++---- crates/benilla-app/src/player/view_subject.rs | 100 +- crates/benilla-app/src/player/walk.rs | 78 +- crates/benilla-app/src/player/wire_in.rs | 567 ++---- crates/benilla-app/src/player/world_focus.rs | 200 +-- crates/benilla-app/src/quest_markers/mod.rs | 353 ++-- crates/benilla-app/src/quest_markers/query.rs | 441 ++--- crates/benilla-app/src/target/by_name.rs | 352 ++-- crates/benilla-app/src/target/click.rs | 1162 +++--------- crates/benilla-app/src/target/cursor_mode.rs | 769 ++------ crates/benilla-app/src/target/flash.rs | 79 +- crates/benilla-app/src/target/highlight.rs | 55 +- crates/benilla-app/src/target/hover.rs | 499 ++---- crates/benilla-app/src/target/hover_probe.rs | 111 +- crates/benilla-app/src/target/lock.rs | 326 ++-- crates/benilla-app/src/target/mod.rs | 415 ++--- crates/benilla-app/src/target/relations.rs | 139 +- crates/benilla-app/src/target/reticle.rs | 120 +- crates/benilla-app/src/target/ring.rs | 666 ++----- crates/benilla-app/src/target/scan.rs | 766 ++------ 98 files changed, 8757 insertions(+), 25796 deletions(-) diff --git a/crates/benilla-app/src/aura_visual.rs b/crates/benilla-app/src/aura_visual.rs index 7f4c01f2..177ace8b 100644 --- a/crates/benilla-app/src/aura_visual.rs +++ b/crates/benilla-app/src/aura_visual.rs @@ -1,101 +1,21 @@ -//! **The aura-state CharProc layer** β€” what an aura does to the *body itself*: its translucency, its -//! tint and its animation clock, for exactly as long as the aura lives. This is the half of a stage-2 -//! "state kit" that -//! isn't an attach-point emitter (those are `creature_anim::spell_visual`'s -//! [`arm_aura_state_fx`](crate::creature_anim::arm_aura_state_fx), decision 0393): where that -//! watcher hangs *models* on the unit, this one changes how the unit's own model **renders**. +//! The aura-state CharProc layer: what an aura does to its unit's own body (translucency, tint, +//! animation clock) while it lives; the kit's attach-point models are +//! [`arm_aura_state_fx`](crate::creature_anim::arm_aura_state_fx)'s. //! -//! B114 ("Stealth shows nothing on the character") was this whole layer missing. Stealth's state -//! 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. +//! In the reference an aura slot change (`0x604d00` β†’ `0x6123f0`) reaches `0x5ff350`, which plays +//! the spell's state kit (`SpellVisual` field 4) at stage 2, and the kit's tail (`0x60f35c`) runs +//! the CharProc dispatcher `0x60d7c0` over its four proc slots (`SpellVisualKit` fields 15-34). +//! Three procs act on the body: 14 installs an alpha node (`0x60d972`), 1 a tint node +//! (`0x60d840`), 11 an animation rate (`0x60db7e`). A node is keyed by spell id and linked at the +//! head of its list, only the head counts, and the aura's removal drops its spell's nodes +//! (`0x5ff290`). //! -//! ## The mechanism +//! `UNIT_FIELD_BYTES_1` byte 3's stealth and ghost bits drive no body render in the reference: its +//! readers (`0x5ff80d`, `0x607101`, `0x60f62e`) only suppress nameplates and markers. The body +//! keys on the aura, never on the flag. //! -//! `UNIT_FIELD_AURA` slot change β†’ the aura watcher `0x604d00` β†’ `0x6123f0` β†’ **`0x5ff350` -//! PlayAuraStateVisual**: the slot's spell β†’ `Spell.dbc` β†’ `SpellVisual` β†’ **field 4 = the state -//! kit** β†’ `PlaySpellVisualKit(kit, stage 2)`, whose tail (`0x60f358: push esi(kit); -//! 0x60f35c: call 0x60d7c0`) runs the **CharProc dispatcher** over the kit's four proc slots -//! (`SpellVisualKit` fields 15–34 β€” `benilla_formats::CharProc`). Two of the dispatcher's nine cases -//! act on the body: -//! -//! - **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 β€” `baseAlpha` from the -//! vtbl+0x6c getter `0x60d2d0` (`CreatureDisplayInfo+0x14 Γ— 1/255`, the SAME slot in the unit -//! 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` -//! (`0x614baa β†’ 0x710cb0`), which multiplies into the per-batch alpha (`0x707680`). -//! The recompute has a second, aura-free caller: the DISPLAYID watcher's refresh (`0x60abe0 β†’ -//! 0x60ad9f`), which is how a display whose row says `CreatureModelAlpha < 255` β€” Ghost Wolf's -//! 4613 = 102 β€” renders translucent by itself; [`refresh_base_alpha`] is that leg. -//! - **proc 1 = tint** (`0x60d840`): `round(param)` is a packed `0x00RRGGBB` OR'd with `0xff000000` -//! into a tint node (list `unit+0xce0`); per frame the head node goes `Γ—1/255` into -//! `model+0x184/188/18c`. -//! -//! Removal is the mirror image: the aura leaves the slot β†’ `0x5ff290` drops that spell's nodes and -//! recomputes, so the alpha **ramps back** to the new target over the same 1000 ms. -//! -//! 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 (`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) -//! -//! 185 of 1772 kits carry a CharProc, and the **state** stage is far and away their home: 103 state -//! kits, ~1200 (spell, proc) pairs. Proc 14 covers **11 kits / 135 spells** β€” kit 312 `0.3` is the -//! whole sneak family (Stealth 1784-1787, Prowl, Vanish, Shadowmeld, Hide, Sneak, Disguise), kit -//! 3450 `0.5` is Invisibility/Fade, kit 989 `0.5` the ghost aura, 3129 `0.65` Shadowform, 1286 `0.3` -//! Banish, 3989 `1.0` Possess, 5129 `0.0` a quest fade-out. Proc 1 covers 73 state kits (~800 -//! spells): Frostbolt's chill blue, Immolate's ember orange, the poison green. -//! -//! ## What is built here, and what is not -//! -//! **Proc 14 ships whole** β€” nodes, the `baseAlpha Γ— head node` target, the 1000 ms cubic ramp both ways, -//! and the render composition below. -//! -//! **Proc 1 ships too** (decision 0812): its nodes live here ([`AuraNodes::tint`], head-node-wins as -//! the reference's `unit+0xce0` list is) and [`apply_aura_tint`] publishes the head to -//! [`benilla_world::instance_tint`], the per-instance modulate channel keyed on the part's rig slot. Written -//! on change, never eased β€” the reference's own asymmetry (`unit+0xd04` change-detection straight to -//! `0x710cf0`, with none of the alpha's 1000 ms ramp). -//! -//! 0806 recorded this as blocked on a render-architecture call between per-unit material clones and a -//! tag-indexed palette. That framing was wrong on its own terms: the `MeshTag` rig field already IS a -//! per-instance slot into the shared light buffer, so the tint needed **no** new tag bits (which was -//! the whole stated blocker β€” the payload has 5 spare bits outdoors and zero indoors) and no material -//! churn. What it did need was reading our own renderer properly. -//! -//! The tint reaches a unit **whole** β€” the reference's `0x714000` recursion, where an attached model -//! composes onto its parent CM2's computed colours β€” by two routes, because benilla has two kinds of -//! attached model: -//! -//! - the ordinary one **borrows its wearer's slot**: an item's parts, cards and boneless geosets all -//! spawn carrying the unit's instance bits (decision 0820), so they read the unit's table entry; -//! - a **rigged** one carries its OWN slot, because the vertex stage indexes the skin palette with -//! that same field and cannot borrow it (a spell-effect instance; since 0841, the seven shoulder -//! models that weld geometry to a billboard bone). [`chained_tint`] walks such an instance's -//! [`benilla_world::model_fade::ParentModel`] link up to the unit β€” the recursion itself, and the same walk -//! `ModelAlphas` already does for alpha. -//! -//! **Proc 11 ships too** (decisions 0889 + 0891): the **freeze**. Its param is a playback *rate* -//! written onto the unit's own animation clocks (`0x60db7e` β†’ `SetBoneAnimSpeed 0x712910` on the -//! mount's bone 0, the body's key-bone 4 β€” the upper-body split β€” and the body's bone 0), old rates -//! saved in the node and written back when it expires (`0x6203e0`). Rate 0 is Ice Block: the pose -//! holds exactly where it was, *including* a cast one-shot that had just been armed, which is why the -//! caster never gets to raise their hand. [`apply_aura_anim_rate`] is that leg β€” and it expresses -//! rate 0 as a **pause**, not as a speed, because the reference's rate belongs to a bone while a speed -//! here belongs to a clip that other code copies from ([`AnimRateFreeze`] carries the story). -//! -//! 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 mechanism is known. +//! Proc types 2, 7 and 13 also appear on state kits; their mechanism is untraced, so [`node_for`] +//! ignores them. use benilla_protocol::EntityKind; use bevy::mesh::MeshTag; @@ -105,75 +25,51 @@ use benilla_assets::materials::WowModelMaterial; use benilla_world::interior::InteriorLit; use benilla_world::model_fade::{fade_alpha, FadeMaterials, PendingAppearFade, RenderFade}; -/// The reference's aura-alpha ramp: `StartAlphaFade(target, **1000 ms**)` (`0x614f80` from -/// `0x60d180`'s recompute), eased `clamp01(t)Β³` by `0x614a90` β€” [`fade_alpha`] is that curve. -/// -/// It is a **code constant, not a kit column**: proc 14's `params[1..]` are `[2.0, 1000.0, 0.0]` on -/// the sneak kit but `[0.0, 0.0, 0.0]` on the ghost's, and the ghost's ramp is byte-cited at the same -/// 1000 ms β€” so the sneak kit's literal `1000.0` cannot be the source (a data-sourced duration would -/// snap the ghost instantly). Across the whole shipped table proc 14's `params[2]` is only ever -/// `0.0`, `2.0` or `1000.0`, which is no duration scale in any unit. Read from `params[0]` only. +/// The aura-alpha ramp, `StartAlphaFade(target, 1000 ms)` (`0x614f80`) eased `clamp01(t)Β³` by +/// [`fade_alpha`] (`0x614a90`). A code constant, not a kit column: the ghost kit's proc 14 carries +/// no `1000.0` yet ramps over the same second. pub(crate) const AURA_ALPHA_FADE_SECS: f32 = 1.0; -/// Settled-ramp epsilon β€” under half a step of the tag's 6-bit alpha field (1/64), so a ramp that -/// has visually arrived is treated as arrived. +/// Under half a step of the tag's 6-bit alpha field (1/64): a ramp this close has arrived. const ALPHA_EPS: f32 = 1.0 / 128.0; -/// One CharProc node the dispatcher installed on a unit, keyed by the aura's spell id exactly as the -/// reference keys `node+0x18`. Held in [`AuraNodes`]; a `Reap` for that spell id removes it. +/// One CharProc node a kit installs on a unit, keyed by spell id as the reference keys `node+0x18`. #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum AuraNode { - /// Proc 14: this aura's alpha factor (`node+0x78`) β€” the target's node term while it is the head. + /// Proc 14: the alpha factor (`node+0x78`). Alpha(f32), - /// Proc 1: this aura's body tint, unpacked from the param's `0x00RRGGBB`. Modelled, not yet - /// rendered (module docs). + /// Proc 1: the body tint, unpacked from the param's `0x00RRGGBB`. Tint([u8; 3]), - /// Proc 11: this aura's animation playback **rate** for the unit's own model β€” `0.0` for every - /// member of the freeze family (Ice Block, Freeze, Petrify, Entangle, Stilled). + /// Proc 11: the playback rate of the unit's own clocks, `0.0` for the whole freeze family. AnimRate(f32), } -/// The CharProc node lists of one unit β€” the ECS twin of the reference's per-unit `unit+0xb50` -/// (alpha) and `unit+0xce0` (tint) lists, plus the ramp `0x614f80` drives from their heads. -/// -/// Lives on the **net entity root**, like [`benilla_world::entity_shade::GroundShade`]: the reference has one -/// of these per CGUnit and every attached model (held item, helm, shoulder) renders off the owner's, -/// which is also why [`apply_aura_alpha`] walks the root's whole descendant tree rather than its -/// direct children. +/// One unit's CharProc node lists (the reference's `unit+0xb50` alpha and `unit+0xce0` tint lists) +/// and its alpha ramp. On the net entity root: the reference keeps one per CGUnit, and every +/// attached model renders off its owner's. #[derive(Component, Debug, Default)] pub(crate) struct AuraNodes { - /// `(spell id, factor)` β€” proc-14 nodes, newest at the front (the reference links a fresh node - /// at the list head). The HEAD times [`Self::base`] is the target β€” never a product. + /// Proc-14 `(spell id, factor)` nodes, newest first, as the reference links at the head. alpha: Vec<(u32, f32)>, - /// `(spell id, rgb)` β€” proc-1 nodes, newest at the front like the alpha list; the head is what - /// the per-frame apply reads. + /// Proc-1 `(spell id, rgb)` nodes, newest first. tint: Vec<(u32, [u8; 3])>, - /// `(spell id, rate)` β€” proc-11 nodes, head-first like the other two. The head is the rate the - /// unit's animation clocks run at ([`apply_aura_anim_rate`]); empty = the clocks are the - /// driver's own again. + /// Proc-11 `(spell id, rate)` nodes, newest first; the head drives the unit's clocks. 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. - /// 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. + /// `baseAlpha`, the display row's `CreatureModelAlpha / 255` for every unit, players included + /// (`0x60d2d0`); kept current by [`refresh_base_alpha`]. base: f32, - /// Where the ramp is now β€” what the parts actually render at. + /// The ramp's live value, which the parts render at. current: f32, - /// The ramp: `from β†’ to` over [`AURA_ALPHA_FADE_SECS`] from `started` (`Time::elapsed_secs`). + /// The ramp, `from` to `to` over [`AURA_ALPHA_FADE_SECS`] from `started` (elapsed seconds). from: f32, to: f32, started: f32, - /// Whether [`apply_aura_alpha`] still owns this unit's parts. Set while the unit is translucent - /// and for the one frame the ramp latches back at opaque β€” that frame performs the **release** - /// (writes the settled alpha and hands the material back to the part's light law), the same - /// hand-back protocol the self-avatar feather uses (decision 0213). Without it, a unit whose - /// stealth drops stays latched on the blend twin at its last low alpha until something else - /// happens to rewrite the channel. + /// Whether [`apply_aura_alpha`] owns this unit's parts: while translucent, plus the one frame + /// at opaque that writes the settled alpha and hands the material back to its light law. authoring: bool, } impl AuraNodes { - /// A fresh node set for a unit whose `baseAlpha` is `base`, ramp settled at `base`. fn new(base: f32) -> Self { Self { base, @@ -184,18 +80,15 @@ 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. Nodes are linked at the head as they install, so the newest one is the term that - /// counts. + /// `0x60d180`: `baseAlpha` times the head alpha node's factor alone, never a product over the + /// list; an empty list leaves `baseAlpha` (`0x60d195`). fn target(&self) -> f32 { let head = self.alpha.first().map_or(1.0, |(_, f)| *f); (self.base * head).clamp(0.0, 1.0) } - /// Point the ramp at the current target, from wherever it is now. A no-op when the target - /// hasn't moved, so an aura refresh (or a node whose factor equals the live one) doesn't restart - /// the ease. + /// Aim the ramp at the target from where it is now; an unmoved target leaves it alone, so an + /// aura refresh does not restart the ease. fn retarget(&mut self, now: f32) { let target = self.target(); if (target - self.to).abs() <= f32::EPSILON { @@ -206,34 +99,27 @@ impl AuraNodes { self.started = now; } - /// Advance the ramp to `now` and return the live alpha. fn tick(&mut self, now: f32) -> f32 { let t = (now - self.started) / AURA_ALPHA_FADE_SECS; self.current = fade_alpha(self.from, self.to, t); self.current } - /// Whether the unit renders translucent right now (the ramp has arrived at, or is heading - /// anywhere below, opaque). fn translucent(&self) -> bool { self.current < 1.0 - ALPHA_EPS } - /// The head tint node's RGB β€” the reference's `unit+0xce0` head, which its per-frame apply reads - /// (`0x60cbd9`). `None` when the unit carries no tint aura. Published to the render channel by - /// [`apply_aura_tint`]. + /// The head tint node's RGB, which the reference's per-frame apply reads (`0x60cbd9`). pub(crate) fn head_tint(&self) -> Option<[u8; 3]> { self.tint.first().map(|(_, rgb)| *rgb) } - /// The head proc-11 node's rate β€” what this unit's animation clocks are being held at, or `None` - /// when no freeze aura is on it. Head-node-wins like the other two lists. + /// The head proc-11 node's rate, the one the unit's clocks run at. pub(crate) fn head_anim_rate(&self) -> Option { self.rate.first().map(|(_, r)| *r) } - /// A node list holding one proc-11 rate node β€” the state a freeze aura leaves on a unit, for - /// the driver's wound-refusal tenant (decision 2063) without running the aura-slot watcher. + /// A node set holding one proc-11 node, as a freeze aura leaves it, without the slot watcher. #[cfg(test)] pub(crate) fn with_rate_node_for_tests(spell_id: u32, rate: f32) -> Self { Self { @@ -243,20 +129,9 @@ impl AuraNodes { } } -/// Publish every rig's body tint to the per-instance channel ([`benilla_world::instance_tint`], decision -/// 0812): the head node's RGB packed the reference's way, keyed on the unit's rig slot β€” the slot -/// each of its skinned parts already carries in its `MeshTag`, and which the fragment stage reads to -/// index the table. -/// -/// **On change, not eased** β€” the reference's own asymmetry between the two node lists: the alpha -/// gets a 1000 ms cubic ramp (`0x614f80`), the tint goes straight in on change-detection -/// (`unit+0xd04` β†’ `0x710cf0`). `InstanceTints::set` is idempotent, so the steady state costs one -/// compare per rig and never touches the upload. -/// -/// Iterates every **`RigSkin`**, not every `AuraNodes`, deliberately: reasserting the whole table -/// each frame means no edge can leak a stale colour into a recycled slot β€” not an `AuraNodes` that -/// went away, not a unit whose last tint aura dropped. (The despawn/rebuild edge is covered -/// structurally too, in `RigSkin`'s free hook.) +/// Publish every rig's head tint to [`benilla_world::instance_tint`], keyed on its rig slot. The +/// reference writes the tint as it changes, with none of the alpha's ramp (`unit+0xd04` β†’ +/// `0x710cf0`). Every `RigSkin` is rewritten each frame so a recycled slot never keeps a colour. pub(crate) fn apply_aura_tint( rigs: Query<(Entity, &benilla_world::rig_palette::RigSkin)>, chain: Query<( @@ -282,15 +157,9 @@ pub(crate) fn apply_aura_tint( } } -/// The tint a rig instance renders with: its own head node, else the nearest one **up its -/// [`benilla_world::model_fade::ParentModel`] chain** β€” the reference's `0x714000` recursion, which composes -/// an attached model's colours onto its parent's computed ones (the same walk [`ModelAlphas`] does -/// for alpha, and the same law `mesh_tag` cites for why an item's parts carry their wearer's slot). -/// -/// It exists because a rigged **item** carries its own instance slot rather than its wearer's -/// (decision 0841 β€” the vertex stage indexes the palette with that field, so it cannot be borrowed): -/// without the walk, a dwarf's Stoneform tint would stop at exactly the pauldrons the rig was added -/// for. A despawned link ends the walk. +/// The tint a rig renders with: its own head node, else the nearest up its +/// [`benilla_world::model_fade::ParentModel`] chain, as the reference composes an attached model's +/// colours onto its parent's (`0x714000`); only this walk reaches a rigged item's own slot. fn chained_tint( chain: &Query<( Option<&AuraNodes>, @@ -311,16 +180,9 @@ fn chained_tint( None } -/// `WOW_TINT_PROBE=RRGGBB` β€” paint **every live rig slot** that colour, auras ignored. -/// -/// The one machine check for the GPU half of the channel: the region upload, the fragment stage's -/// slot lookup, the unpack, and the placement of the multiply. No headless test can reach any of -/// that, and the capture harness cannot stage a real aura to do it with β€” it sees no networked -/// entities at all (0799), so a tinted *unit* is unstageable there. Pointed at a scene with animated -/// (rigged) doodads it turns each of them the probe colour, which the visual harness then diffs. -/// -/// It logs the slot count once, so an unchanged capture reads as "nothing in this scene is rigged" -/// rather than as "the channel is broken" β€” the two are indistinguishable from the image alone. +/// `WOW_TINT_PROBE=RRGGBB` paints every live rig slot that colour, auras ignored: the machine check +/// for the GPU half of the tint channel. It logs the slot count once, since an unchanged capture +/// cannot tell "nothing is rigged" from "the channel is broken". fn tint_probe() -> Option<[u8; 3]> { static PROBE: std::sync::OnceLock> = std::sync::OnceLock::new(); *PROBE.get_or_init(|| { @@ -335,54 +197,21 @@ fn tint_probe() -> Option<[u8; 3]> { }) } -/// Marker: this rig's clocks are being held by a proc-11 aura, so the release knows to let them go. -/// -/// It carries **nothing**, and that is the fix for the first shipped version's bug. That one stored -/// the speed of every clip it took over and wrote the values back on release β€” the reference's own -/// `+0x60`/`+0x64`/`+0x68` save/restore, transliterated one level too low. It cannot work here, -/// because in the reference a rate belongs to a *bone* (one value, saved once, and clips armed later -/// never carry one) while ours would have to belong to a *clip*, and clips get armed under the freeze -/// carrying values copied from other, already-frozen clips. `transplant_up` is exactly that: moving a -/// one-shot to the torso overlay copies the source clip's speed, which under the freeze was `0`, so -/// the overlay was saved at `0`, restored to `0`, and never advanced again β€” a cast pose welded to the -/// upper body for the rest of the session, surviving every later cast (it never *finished*, so the -/// overlay never released). Pausing owns no value and so cannot poison one. +/// Marker: a proc-11 aura holds this rig's clocks. It saves no speeds, unlike the reference's +/// per-bone save and restore (`0x6203e0`): here a rate belongs to a clip, and `transplant_up` arms +/// clips under the freeze with speeds copied from frozen ones, so a saved speed restores as 0. #[derive(Component, Default, Debug)] pub(crate) struct AnimRateFreeze; -/// Hold the clocks of every unit whose head proc-11 node says so β€” its own rig and its mount's -/// (`[unit+0xd8]` and `[unit+0xdc]`, the two models `0x6201d0` writes) β€” and let them go when the aura -/// does. +/// Pause the clocks of every unit whose head proc-11 rate is 0, its own rig and its mount's (the +/// two models `0x6201d0` writes), and resume them when the aura goes; effect models and global +/// sequences keep running. The pause is reasserted every frame, since the reference's rate lives +/// on the bone (`+0xb0`, read by the clock at `0x71458e`) and arming leaves it alone +/// (`0x7121a0`), and after the animation driver, so a cast armed this frame freezes too. /// -/// **A rate of 0 is expressed as a PAUSE, not as `set_speed(0.0)`.** They are the same picture on -/// screen (Bevy evaluates a paused animation at its frozen seek, exactly as `0x712910`'s `bias` rebase -/// holds the reference's current frame) but not the same object: a pause is a bit the clock reads, -/// where a speed is a *value* other code copies. `transplant_up` copies a clip's speed when it moves a -/// one-shot to the torso overlay, and that is how the first version welded a cast pose to the upper -/// body permanently β€” see [`AnimRateFreeze`] and decision 0891. Nothing here reads or writes a speed, so nothing can -/// 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 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 -/// across every re-arm in between, and re-pausing each frame is how a per-clip flag says the same -/// thing β€” including for clips armed *under* the freeze, which the reference freezes by construction. -/// -/// It runs **after the driver**, so this frame's arms are already in β€” which is the whole Ice Block -/// observable: the cast one-shot is armed and then frozen at the frame it was on, so the caster never -/// gets to raise their hand. -/// -/// **A non-zero rate does nothing** (and only kit 1744's `8947848.0` is one β€” a packed grey that -/// landed in the rate column, on Stoned / Petrification / Thadius Spawn). Our clock is f32 seconds -/// where the reference's is integer milliseconds with a modulo, so a rate that large is noise there -/// and different noise here; there is no honest transliteration, and inventing one would be worse than -/// saying so. Recorded, not special-cased. -/// -/// What it deliberately does **not** touch: attached effect models (the ice block's own -/// `icebarrier_state.mdx` keeps shimmering β€” the reference writes the unit's two models only) and -/// global sequences (a different clock there, and a different writer here). +/// Deviation: a non-zero rate is ignored, because the only one shipped (`8947848.0` on kit 1744, a +/// packed grey in the rate column) is noise on the reference's integer-ms clock and has no faithful +/// form on our f32-second one. pub(crate) fn apply_aura_anim_rate( units: Query<( Entity, @@ -409,10 +238,7 @@ pub(crate) fn apply_aura_anim_rate( } } (false, true) => { - // Resume everything: the set we paused is "whatever was playing", and it grew - // every frame the freeze held. Nothing else on a unit rig pauses an animation - // (the portrait booth does, on its own rigs), so there is no third party's pause - // to trample. + // Resume every clip: nothing else pauses an animation on a unit rig. for (_, anim) in player.playing_animations_mut() { anim.resume(); } @@ -425,12 +251,8 @@ pub(crate) fn apply_aura_anim_rate( } } -/// The freeze's own instrument (`WOW_MOVE_TRACE`, tag `aur`): one line per rig at each edge, naming -/// the clips it took over and the speed each is *left* running at β€” the difference between "the clocks -/// stopped" and "nothing was playing anyway", which no count of *absent* anim events can tell apart. -/// -/// It prints the speeds precisely because the first version's bug was invisible without them: a `@0` -/// on the thaw line is a clip that will never advance again. +/// Trace (`WOW_MOVE_TRACE`, tag `aur`) one line per rig at each freeze edge, with each clip's +/// speed: a `@0` on a thaw line is a clip that will never advance again. fn trace_clock_edge(what: &str, rig: Entity, player: &AnimationPlayer) { if !benilla_assets::trace::enabled_for("aur") { return; @@ -445,9 +267,7 @@ fn trace_clock_edge(what: &str, rig: Entity, player: &AnimationPlayer) { ); } -/// One aura's CharProc edge, written by the aura-slot watcher -/// ([`crate::creature_anim::arm_aura_state_fx`], which owns the slot diff and the state-kit resolve -/// for both halves of a kit) and drained by [`drain_aura_procs`]. +/// One aura's CharProc edge, from [`crate::creature_anim::arm_aura_state_fx`]'s slot watch. #[derive(Message, Clone, Debug)] pub(crate) enum AuraProc { /// The aura landed in a slot: install this kit's body procs, keyed by its spell id. @@ -461,16 +281,14 @@ pub(crate) enum AuraProc { Reap { entity: Entity, spell_id: u32 }, } -/// Turn one `SpellVisualKit` CharProc into the node it installs, or `None` for a proc type we have -/// no verified mechanism for (module docs). **The single dispatch point** β€” the ECS twin of the -/// client's jump table `0x60dbfc`, and the one place a newly-REd proc type gets an arm. +/// The node one `SpellVisualKit` CharProc installs (the dispatcher's jump table `0x60dbfc`), or +/// `None` for a proc type whose mechanism is untraced. pub(crate) fn node_for(proc: benilla_formats::CharProc) -> Option { use benilla_formats::char_proc_type as ty; match proc.ty { ty::ALPHA => Some(AuraNode::Alpha(proc.params[0])), - // `round(param)` β†’ `0x00RRGGBB` (`0x60d8cc` then ORs the opaque byte on). The param is a - // float column holding an integral colour: the ghost's 9222653.0 is 0x8CB9FD, pale - // blue-white; the poison kit's 65280.0 is 0x00FF00. + // A float holding an integral `0x00RRGGBB`, rounded (`0x60d8cc`): the ghost's 9222653.0 + // is 0x8CB9FD. ty::TINT => { let packed = proc.params[0].round().clamp(0.0, 0xff_ffff as f32) as u32; Some(AuraNode::Tint([ @@ -479,22 +297,14 @@ pub(crate) fn node_for(proc: benilla_formats::CharProc) -> Option { packed as u8, ])) } - // `params[0]` is a playback rate, straight through β€” `0x60db7e` hands it to - // `SetBoneAnimSpeed` unexamined, so the one outlier (kit 1744's `8947848.0`, a packed grey - // that landed in the rate column) is passed through rather than special-cased. + // A playback rate, passed through: `0x60db7e` hands it to `SetBoneAnimSpeed` (`0x712910`) + // unexamined. ty::ANIM_RATE => Some(AuraNode::AnimRate(proc.params[0])), _ => None, } } -/// Apply this frame's [`AuraProc`] edges to the units' node lists, then re-aim every live ramp. -/// -/// `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`, 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). +/// Apply this frame's [`AuraProc`] edges to the units' node lists. pub(crate) fn drain_aura_procs( mut edges: MessageReader, time: Res