From dd7de2aa41e2e1da080b57192a9ac0075eaddd69 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2026 17:08:31 +0700 Subject: [PATCH] a corpse is not a shop, and Darnassus draws through its doorways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mounted, over a dead wolf, a right-click answered "That merchant doesn't like you." three times: the click was sending `CMSG_LIST_INVENTORY` — a vendor packet — at the corpse, because the unit block was an if/else-if chain whose LAST arm was the NPC-service dispatch, and the mounted-loot fall-through gave it a newly reachable state — dead target, no leg taken, so the click fell into the arm that means "live friendly service NPC", which dispatched on the CURSOR, and a lootable corpse wears the same pouch a vendor does; the reference splits on death first and never rejoins (`0x60bf80`), so the fork is now `Attack / Dead / Service`, matched exhaustively, with a test over all sixteen combinations of its inputs - Darnassus draws through its doorways: the reference's Pass 2 ends in the portal RECURSION (`0x6b3d39 call 0x6b41c0`), not a mark — a doorway admits everything the walk reaches through it, and in a city whose outdoor areas are stitched together by portals that is most of it (standing in a shop, 36 groups the marking pass had culled); the narrowness reject turns out to be the open-world walk's entry gate alone, not the shared volume builder (applying it to both had blanked the containing building's shell for every window in the [0.001, 0.02) NDC band), and the depth cap is `[0x86b6b8]` = 10, not a guessed 64; `WOW_PIN_SITE=darnassus` replays the report offline in 0.2 s - the tab clamp was reading a return the reference's `PanelTemplates_TabResize` does not have, so since the panel kit came off the chain the rule that a tab never grows past its window's drawn right edge raised `attempt to compare number with nil` once per tab per width change on all seven windows built from `CharacterFrameTabButtonTemplate`; it reads the fit back off the art the reference's own verb just wrote (a Texture's width is its authored size, no resolved rect consulted) - footfalls have one gate, one origin, one kernel: the 50 yd camera-distance cut (`0x5fc00a`) is measured from the CAMERA EYE, unconditional (no self exemption — an earlier slice reused a predicate it had just derived for the ring pools, and no RE note ever recorded one), rounded once with a strict compare — and it sits above the decal, the camera shake AND the 25 yd spray, not copied per consumer; the shake's second suspend arm is the shared fly-or-swim spline bit read off the FOLLOWED unit, so under Mind Vision or Eye of Kilrogg the shake frames on the right body and suspends on the right swim state Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/assets/ui/UiPanels.xml | 28 +- crates/benilla-app/src/camera_shake.rs | 132 ++++-- crates/benilla-app/src/creature_anim.rs | 3 +- .../benilla-app/src/creature_anim/events.rs | 101 ++++ crates/benilla-app/src/footprints.rs | 36 +- crates/benilla-app/src/player.rs | 3 + crates/benilla-app/src/player/view_subject.rs | 12 +- crates/benilla-app/src/target/click.rs | 306 ++++++++---- .../src/ui_script/panel_template_tests.rs | 58 +++ crates/benilla-world/src/exterior_cull.rs | 99 +++- .../benilla-world/src/wmo_portal/audit/mod.rs | 12 + .../benilla-world/src/wmo_portal/audit/pin.rs | 167 ++++++- crates/benilla-world/src/wmo_portal/mod.rs | 434 ++++++++++++------ crates/benilla-world/src/wmo_portal/probe.rs | 6 +- 14 files changed, 1063 insertions(+), 334 deletions(-) diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml index 2e366710..a1a330dd 100644 --- a/crates/benilla-app/assets/ui/UiPanels.xml +++ b/crates/benilla-app/assets/ui/UiPanels.xml @@ -2437,16 +2437,36 @@ end function BenillaTabButton_OnUpdate(tab) - local text = getglobal(tab:GetName() .. "Text") + local name = tab:GetName() + local text = getglobal(name .. "Text") local w = (text and text:GetStringWidth()) or 0 if w <= 0 then return end local pad = tab.benillaTabPadding or 0 local room = BenillaTab_Room(tab) if tab.benillaFitWidth == w and tab.benillaFitRoom == room then return end tab.benillaFitWidth, tab.benillaFitRoom = w, room - local fitted = PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth) - if room and fitted > room then - PanelTemplates_TabResize(pad, tab, room) + PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth) + -- THE CLAMP READS THE FIT BACK OFF THE ART, not off a return value and not off the + -- button. `PanelTemplates_TabResize` is the REFERENCE's now (1837 put + -- `UIPanelTemplates.lua` on the chain below this file, so its definition wins) and it + -- returns nothing; our own copy's benilla-only `return tabWidth` had already stopped + -- running, which is what took 1002's clamp off the air silently. `tab:GetWidth()` + -- cannot serve either — it reads the last RESOLVED rect, this frame's stale input + -- inside a settle, which is the whole reason 1002 added that return. + -- + -- `$parentMiddle` can. `PanelTemplates_TabResize` sets it on its way out, a Texture's + -- width is its AUTHORED size, and a region's GetWidth reports the authored value + -- without consulting a resolved rect (region::virtual_span) — so it is the number the + -- verb just computed, same tick. The tab is that plus its two end caps, which is the + -- reference's own `tabWidth = width + sideWidths`. + if room then + local middle = getglobal(name .. "Middle") + local left = getglobal(name .. "Left") + local fitted = (middle and middle:GetWidth() or 0) + + 2 * (left and left:GetWidth() or 0) + if fitted > room then + PanelTemplates_TabResize(pad, tab, room) + end end -- No highlight sizing here on purpose — BOTH tab templates anchor their highlight to -- their own two side edges, so it IS the tab by construction (decision 1004). diff --git a/crates/benilla-app/src/camera_shake.rs b/crates/benilla-app/src/camera_shake.rs index 2ef7fc15..284a28ba 100644 --- a/crates/benilla-app/src/camera_shake.rs +++ b/crates/benilla-app/src/camera_shake.rs @@ -90,11 +90,12 @@ //! //! **Lifetime.** A record holds a *snapshot* position and no reference to its source, so a shake //! fully outlives the creature that spawned it. The whole evaluation is skipped — offset zero, -//! nothing expiring — while the followed unit is **swimming**. +//! nothing expiring — while the **followed unit** (the far-sight subject when there is one, our own +//! body otherwise) is **swimming** or riding a **fly-or-swim spline**: the reference's two suspend +//! gates, both of which jump the same block. //! //! Not modelled, deliberately: the reference's second wholesale free on `SetTarget(nullptr)`, and -//! its destructor's unlink-without-free (a shipped leak — our `Vec` cannot reproduce it). The -//! flying-spline (taxi) arm of the skip is noted in [`CameraShakes::evaluate`]. +//! its destructor's unlink-without-free (a shipped leak — our `Vec` cannot reproduce it). use std::f32::consts::TAU; @@ -103,10 +104,11 @@ use bevy::prelude::*; use benilla_formats::{CameraShake, CameraShakeCatalog, SpellShakeGroup}; use crate::creature_anim::{ - footfall_side, move_flags, AnimSoundEvent, MovementState, SpellKitShake, + footfall_culls, footfall_side, move_flags, AnimSoundEvent, MovementState, SpellKitShake, }; use crate::entities::{BoneAttach, Creatures}; -use crate::net::{Embodied, NetEntity, ObjectStore}; +use crate::net::{Embodied, NetEntity, ObjectStore, Spline}; +use crate::player::ViewSubject; use benilla_assets::{AssetSet, LockRecover, WorldAssets}; use benilla_protocol::EntityKind; use benilla_world::schedule::WorldStage; @@ -119,9 +121,6 @@ const FULL_DISTANCE_SQ: f32 = 81.0; /// The falloff's half-life base and its per-9-yd exponent divisor. const FALLOFF_BASE: f32 = 0.7; const FALLOFF_SPAN: f32 = 9.0; -/// The footstep emitter's own radius, camera→footplant (`0x5fc00a`, `2500 yd²` = 50 yd). **The -/// camera, not the player** — the same correction wow-re applied to `dist2-gate.md` this round. -const EMIT_DISTANCE_SQ: f32 = 2500.0; /// `CameraShakes.Amplitude` is authored in inches; the client scales it at spawn (`0x511d78`). const INCHES_TO_YARDS: f32 = 1.0 / 36.0; @@ -171,13 +170,15 @@ impl CameraShakes { /// Retire what has expired and compose this frame's offset. /// - /// `swimming` is the reference's skip (`0x50ea87`/`0x50ea8b`): while the followed unit swims, - /// the whole block is bypassed — the offset is zero **and nothing expires**, so a shake - /// resumes mid-flight on surfacing. The sibling arm of that skip, a flying (taxi) spline, is - /// not modelled here: we have no flying-spline predicate at the camera seat, and the case is a - /// heavy creature's footfall within 50 yd of a taxi path. Recorded rather than guessed. - fn evaluate(&mut self, eye: Vec3, facing_yaw: f32, now: f32, swimming: bool) -> Vec3 { - if swimming { + /// `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 + /// 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 + /// **and expires nothing**: a shake resumes mid-flight on surfacing or landing, rather than + /// having quietly aged out while the camera was not reading it. + fn evaluate(&mut self, eye: Vec3, facing_yaw: f32, now: f32, suspended: bool) -> Vec3 { + if suspended { return Vec3::ZERO; } // Retire on the *unattenuated* clock: distance decides contribution, never lifetime. @@ -386,7 +387,7 @@ fn fire_shakes( // derives it. No marker/joint = the unit origin. let foot = event_point(attach, pose, &joints, &ev.ident) .unwrap_or_else(|| transform.translation()); - if eye.distance_squared(foot) > EMIT_DISTANCE_SQ { + if footfall_culls(eye, foot) { continue; } live.add(*row, foot, now); @@ -461,9 +462,35 @@ fn fire_kit_shakes( } } -/// The applier's read of the followed unit: its facing (the shake's body frame) and whether it is -/// swimming (the reference's skip). -type FollowedUnit = (&'static Transform, Option<&'static MovementState>); +/// The applier's read of the followed unit: its facing (the shake's body frame), its move flags +/// (the swim half of the reference's skip) and its live path (the spline half). +type FollowedUnit = ( + &'static Transform, + Option<&'static MovementState>, + Option<&'static Spline>, +); + +/// Is the whole shake block skipped this frame? The reference's two suspend gates, OR'd — both +/// `jmp` past the same block (`0x50ea87` / `0x50ea8b` → `0x50eb01`), on the **followed unit**. +/// +/// **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 +/// `[[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`) — +/// 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 +/// walk, all of which install the same component — does not suspend anything. +fn suspended(mv: Option<&MovementState>, spline: Option<&Spline>) -> bool { + mv.is_some_and(|m| m.flags & move_flags::SWIMMING != 0) || spline.is_some_and(|s| !s.grounded) +} /// Add this frame's shake to the camera's seated eye. /// @@ -476,22 +503,40 @@ type FollowedUnit = (&'static Transform, Option<&'static MovementState>); /// the base pose every frame, so the transform this reads is the un-shaken eye rather than last /// frame's shaken one. A zero offset writes nothing at all, preserving the camera's bit-equality /// no-op gate (decision 1362) — a still camera stays bit-stable and its propagation stays quiet. +/// +/// **The body frame and both suspend gates come from the FOLLOWED unit** — `[cam+0x88/0x8c]` in +/// the reference, never `0x468550` (the active player). Ordinarily they are the same object; under +/// far sight (Mind Vision, Eye of Kilrogg, and possession, which rides the same field) they are +/// not, and the camera orbits the subject while our body walks on somewhere else. Reading our own +/// body there would frame the shake on the wrong facing and suspend it on the wrong unit's swim +/// state. [`ViewSubject`] already resolves exactly this object for the rig. +/// +/// The reference additionally requires the followed object to be **TYPEMASK_UNIT** +/// (`0x50e90d`–`0x50e92c`) before reading its `CMovement` at all. That falls out here: the +/// fallback is our own body, and a resolved far-sight subject with no [`MovementState`] and no +/// [`Spline`] — a GameObject or a DynamicObject — reads as "not suspended", which is what a +/// unit-only gate on a non-unit yields. pub(crate) fn apply_camera_shake( mut live: ResMut, time: Res