From d2ccc80e59be0801fa488ec4396caba21b0f9569 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 4 Jul 2026 09:56:40 +0200 Subject: [PATCH] fix(#170): refresh remote body velocity from get_state_velocity (kill the attack glide) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (confirmed by a live ACDREAM_DUMP_MOTION capture of Mite Scamp 0x80000244 + the retail decomp): a chasing+attacking creature's attacks arrive as the ForwardCommand of frequent mt-0 InterpretedMotionState UMs (66 attack UMs 0x62/63/64 vs 2 mt-6 chase MoveTos in the capture). Retail's get_state_velocity (0x00527d50) computes the body's translation velocity from the current forward command: WalkForward→3.12×spd, RunForward→4.0×spd, and 0 for everything else (an attack) — so the creature plants its feet. acdream ALREADY has a faithful get_state_velocity (returns 0 for a non-locomotion command; cross-checked vs holtburger grounded_local_velocity's `_ => zero`), but it was never WIRED to the remote body for entities with an animation sink: apply_current_movement's sink path (all remotes have a DefaultSink) dispatches the animation and early-returns BEFORE the set_local_velocity(get_state_velocity()) write, which lives only in the no-sink fallback (MotionInterpreter ~1702). So a remote NPC's body.Velocity was never recomputed from its motion state and kept the STALE run velocity from the last chase — the body dead-reckoned forward at ~4 m/s while playing an idle+attack pose ("glides after me"). Fix: after apply_current_movement in the grounded remote-NPC dead-reckon path (GameWindow ~9992, restricted to remotes by serverGuid != player and to grounded by OnWalkable), refresh rm.Body via set_local_velocity(get_state_velocity()) — the exact write retail's apply_current_movement performs, reusing the verbatim ported get_state_velocity. An attack forward command now resolves to 0, so the creature stops and swings in place; RunForward still yields the run velocity. Narrowest safe seam: the local player (which also has a sink) is excluded by the loop's player guard, so its PlayerMovementController velocity path is untouched. The earlier suspicion (#159 combat-command numbering) was a red herring — the scamp's 0x62/63/64 were always in the correct block and the planner is unwired; the wire even carries full attack variety, so "uniform" was the visual artifact of rapid attacks over a gliding idle base, not a classification bug. Tests: MotionVelocityPipelineTests.AttackForwardCommand_ZeroVelocity (4 cases) pins get_state_velocity → 0 for the attack forward commands the fix depends on. Core suite 2503 green. Visual gate pending (retail side-by-side: creature should plant + step, not glide). Ref: docs/research/named-retail get_state_velocity 0x00527d50; ISSUES #170. Co-Authored-By: Claude Opus 4.8 --- src/AcDream.App/Rendering/GameWindow.cs | 29 ++++++++++++++++++ .../Physics/MotionVelocityPipelineTests.cs | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index f629da8a..d46ccf37 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9990,6 +9990,35 @@ public sealed class GameWindow : IDisposable // per-tick shape ordinary locomotion uses. TickRemoteMoveTo(rm); rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false); + + // #170 glide fix (2026-07-04): refresh the body's + // translation velocity from the current interpreted + // state, matching retail. apply_current_movement's + // SINK path (all remotes have a DefaultSink) only + // dispatches the ANIMATION — the + // get_state_velocity → set_local_velocity write + // that retail's apply_current_movement performs + // lives ONLY in acdream's no-sink fallback + // (MotionInterpreter.ApplyCurrentMovementInterpreted, + // ~1702), so a remote's body.Velocity is never + // recomputed and goes STALE. get_state_velocity + // (0x00527d50, already a verbatim port) returns + // WalkForward→3.12×spd, RunForward→4.0×spd, and 0 + // for every other forward command — so when a + // chasing creature's ForwardCommand switches to an + // attack (0x1000006x, action-class), the body + // stops instead of coasting at the last run + // velocity while playing an idle+attack pose (the + // user-reported "glides after me"). Grounded only + // (this branch already asserted Contact+OnWalkable); + // airborne velocity is owned by the jump arc. + // Confirmed vs a live ACDREAM_DUMP_MOTION capture of + // Mite Scamp 0x80000244 (66 mt-0 attack UMs carrying + // ForwardCommand=0x62/63/64 vs 2 mt-6 chase UMs). + if (rm.Body.OnWalkable) + rm.Body.set_local_velocity( + rm.Motion.get_state_velocity(), + rm.Body.LastMoveWasAutonomous); } } else diff --git a/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs b/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs index 45c71889..7df939fc 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs @@ -184,4 +184,34 @@ public sealed class MotionVelocityPipelineTests Assert.Equal(0f, v.X, 5); Assert.Equal(0f, v.Y, 5); } + + // ── attack / non-locomotion forward command (#170) ─────────────────────── + + [Theory] + [InlineData(0x10000062u)] // AttackHigh1 + [InlineData(0x10000063u)] // AttackMed1 + [InlineData(0x10000064u)] // AttackLow1 + [InlineData(0x10000186u)] // AttackHigh4 (shifted late block) + public void AttackForwardCommand_ZeroVelocity(uint attackCommand) + { + // #170: an ATTACK forward command (action-class 0x1000006x/0x100001xx) + // is neither WalkForward (0x45000005) nor RunForward (0x44000007), so + // retail's get_state_velocity (0x00527d50) hits its `else → 0` branch — + // the creature plants its feet instead of coasting at the last run + // velocity. Cross-checked against holtburger grounded_local_velocity + // (`_ => Vector3::zero()`) and the retail decomp. This is the invariant + // the #170 glide fix relies on: GameWindow's remote dead-reckon now + // refreshes the body velocity from get_state_velocity each tick, so a + // creature that switches from a run-chase (ForwardCommand=RunForward) + // to an attack (ForwardCommand=0x1000006x) resolves to zero velocity + // and stops gliding. + var interp = MakeInterp(RunRate); + interp.InterpretedState.ForwardCommand = attackCommand; + interp.InterpretedState.ForwardSpeed = 0.97f; + + var v = interp.get_state_velocity(); + Assert.Equal(0f, v.X, 5); + Assert.Equal(0f, v.Y, 5); + Assert.Equal(0f, v.Z, 5); + } }