the ghost world gets its own sky, and a corpse cannot steer

- a dead player's sky is DeathClouds now — `lightSkyboxID` was named in the schema and read by nobody and `LightSkybox.dbc` was never loaded at all, so a ghost kept the ordinary gradient dome; the shipped data has exactly 5 of 426 LightParams rows carrying a skybox id, every one of them DeathClouds.mdx, reached from the death profile (slot 4) of all 374 Light rows and gated on the ghost bit, with a hardcoded weight of 1.0 that skips the WMO skybox outright; it resolves off the same (map, camera position) seed the atmosphere uses, so the ghost sky and the death-profile fog it hangs over switch on the same frame — and the lane sheds its `wmo_sky` name for `skybox`, no longer being WMO-only
- a corpse cannot steer: the reference's two movement-input predicates share a precondition benilla never modelled — `UNIT_FIELD_HEALTH > 0` — so at zero health the keyboard turn and the right-drag close together, exactly as they do inside an Ice Block, while the ghost (health 1) keeps moving; the wow-re §5 round then corrected three byte trails (the mouse turn has its own eight-conjunct predicate, the supposed on-taxi test is the Knockdown animation lockout — set on exactly one of 208 AnimationData rows — and the "charm bit" is the far-sight latch) and added three verified health gates we lacked: the jump IS health-gated, sit and stand are refused while the body reads dead (a feigner too), and a follow tears down only when BOTH predicates drop — we had that wrong in both directions
- the walk-gait cross-check folds back confirming `min(walk, run)` and the speed-only animation channel (by a 2,304-function closure census), and finds the trap we lacked: every incoming spline re-authors the gait, and the spline's RUNMODE flag is an INVERSE — a spline marked as a run clears walk mode, one not so marked SETS it — so a charge, taxi, knockback or flee hand-off leaves the body in whatever gait its path was authored with, applied once on the spline's start edge; the post-worldport zero-walk fallback is retracted on a re-run (the ctor's zero is a transient thirty instructions wide, never observable), leaving the plain minimum the bytes compute — the better failure mode too, since a fallback makes a walker silently RUN if a walk speed ever goes missing, where the transcription makes them visibly stop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 20:18:16 +07:00
co-authored by Claude Fable 5
parent 67315143f9
commit 25c865c00a
43 changed files with 793 additions and 146 deletions
@@ -490,6 +490,7 @@ impl World {
stop,
duration_ms,
flying,
..
} => {
let mine = *guid == self.self_guid;
if mine {
+3
View File
@@ -343,6 +343,9 @@ pub enum SessionEvent {
/// `true` ⇒ a 3-D flight path (keep the spline's Z); `false` ⇒ a ground walk whose Z the app
/// re-derives from the terrain under the unit (see the renderer's creature ground-clamp).
flying: bool,
/// `SPLINEFLAG_RUNMODE` — the path is travelled at run speed. Its **absence** forces
/// `MOVEFLAG_WALK_MODE` on for the unit the spline moves (decision 1758).
run_mode: bool,
},
/// Same-map teleport (`MSG_MOVE_TELEPORT_ACK`): the app snaps our player + echoes the ack.
Teleport {
@@ -636,6 +636,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
stop,
duration_ms,
flying,
run_mode,
} => vec![SessionEvent::MonsterMove {
guid,
start: v3(start),
@@ -645,6 +646,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
stop,
duration_ms,
flying,
run_mode,
}],
ServerPacket::PlayerMove {
guid,
@@ -25,6 +25,13 @@ const MONSTER_MOVE_STOP: u8 = 0x1;
/// its Z. The client gates on this same bit (`0x6018f0: test ah,0x2`). It is also `Mask_CatmullRom` —
/// flying paths interpolate (and wire-encode) as a curve, ground paths as straight segments.
const SPLINE_FLAG_FLYING: u32 = 0x200;
/// `SPLINEFLAG_RUNMODE` — the path is travelled at run speed. Its **absence** is what matters:
/// the real client feeds this bit straight into `CMovement::SetRunMode 0x7c71c0`
/// (`0x7c6ac2 and edi,0x100` → `0x7c6acb call 0x7c71c0`, inside the `SMSG_MONSTER_MOVE` commit
/// `0x7c6a50`), and that setter's argument is *run* — so a spline **without** this bit **sets**
/// `MOVEFLAG_WALK_MODE` on the unit it moves. The two `0x100`s are inverses of each other
/// (wow-re `collision/scratch/walk-mode-law.md` §5.2; benilla decision 1758).
const SPLINE_FLAG_RUNMODE: u32 = 0x100;
/// Parse an `SMSG_MONSTER_MOVE` body into [`ServerPacket::MonsterMove`]. The head (mover, `start`, spline
/// id, `moveType` + its final facing) is always present; a **stop** (`moveType 1`) ends there (the client
@@ -62,11 +69,15 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
stop: true,
duration_ms: 0,
flying: false,
// The stop form carries no spline-flags dword at all, so there is no run/walk verdict
// in it — and it builds no path, so nothing downstream reads this.
run_mode: true,
}
} else {
let spline_flags = read_u32_le(r)?;
let duration_ms = read_u32_le(r)?;
let flying = spline_flags & SPLINE_FLAG_FLYING != 0;
let run_mode = spline_flags & SPLINE_FLAG_RUNMODE != 0;
// The decoded waypoints *after* the start (see [`read_monster_move_spline`]): a ground path's
// first packed point re-encodes `start` (quantized), so drop it and anchor the path with the
// exact wire `start`; a flying path sends only post-start points, kept whole. The result is the
@@ -89,6 +100,7 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
stop: false,
duration_ms,
flying,
run_mode,
}
})
}
@@ -146,6 +146,10 @@ pub enum ServerPacket {
/// The path is a 3-D **flight** (`SPLINE_FLAG_FLYING` = `Mask_CatmullRom`): keep the spline's own Z.
/// Clear ⇒ a ground walk whose Z the client re-derives from the terrain (the app terrain-clamps it).
flying: bool,
/// The path's `SPLINEFLAG_RUNMODE` bit — and its **absence** is the load-bearing half: a
/// spline without it forces `MOVEFLAG_WALK_MODE` **on** for the unit it moves (see
/// [`super::monster_move`]'s `SPLINE_FLAG_RUNMODE`).
run_mode: bool,
},
/// A relayed *player* movement packet (`MSG_MOVE_*`, see [`super::parse::is_movement_relay`]): the mover's
/// authoritative pose plus its live CMovement `moveFlags`. The app snaps the entity to `position`
@@ -49,6 +49,9 @@ const SPLINE_FLAG_FINAL_ANGLE: u32 = 0x4_0000;
/// (`MoveSplineFlag.h:48,77`), the same split [`crate::ServerPacket::MonsterMove`] reads: set ⇒ a 3-D
/// flight path that keeps its own Z, clear ⇒ a ground walk whose Z the client re-derives from terrain.
const SPLINE_FLAG_FLYING: u32 = 0x200;
/// `SPLINEFLAG_RUNMODE` — see [`super::super::monster_move`]'s copy for why its *absence* is what
/// matters: a spline without it forces walk mode on for the unit it moves.
const SPLINE_FLAG_RUNMODE: u32 = 0x100;
/// `MoveSplineFlag::Cyclic` (`MoveSplineFlag.h:59`) — the path loops back on itself forever.
const SPLINE_FLAG_CYCLIC: u32 = 0x10_0000;
@@ -86,6 +89,8 @@ pub struct CreateSpline {
/// `MoveSplineFlag::Cyclic` — the server loops this path forever. The app rides it once; a looping
/// follow is unbuilt (the same gap `MonsterMove`'s cyclic paths have).
pub cyclic: bool,
/// `MoveSplineFlag::Runmode` — and its absence forces walk mode on (decision 1758).
pub run_mode: bool,
}
/// The decoded fields a movement block carries: the pose (from its `LIVING`/`HAS_POSITION` flag),
@@ -199,6 +204,7 @@ impl MovementBlock {
duration_ms,
flying: spline_flags & SPLINE_FLAG_FLYING != 0,
cyclic: spline_flags & SPLINE_FLAG_CYCLIC != 0,
run_mode: spline_flags & SPLINE_FLAG_RUNMODE != 0,
});
}
} else if update_flag & UPDATE_FLAG_HAS_POSITION != 0 {