feat(R3-W5): DoMotion family verbatim + the ONE DoInterpretedMotion + per-op zero-tick flush (closes J3, J4, J9, J14)

The two parallel DoInterpretedMotion implementations MERGE into one
verbatim pair (0x00528360/@305639): contact gate, StandingLongJump
state-only branch, Dead → RemoveLinkAnimations, the jump_error_code
double-check WITH the DisableJumpDuringLink 0x20000 leg (W2's TODO(W5)
resolved — MovementParameters now flows), ModifyInterpretedState
gating, CurCell-null tail, AddToQueue with the real ContextId;
StopInterpretedMotion's post-stop Ready node + raw-mirror RemoveMotion.
Legacy overloads + ApplyMotionToInterpretedState DELETED. The funnel's
public surface unchanged (183-case suite compiles + passes as-is).

DoMotion 0x00528d20 verbatim: cancel_moveto interrupt; SetHoldKey(key,
cancelMoveToBit) BEFORE adjust_motion; params re-default for the
interpreted call; combat-stance gates on the ORIGINAL id (Crouch/Sit/
Sleep → 0x3f/0x40/0x41; & 0x2000000 → 0x42 outside NonCombat); action
depth cap ≥6 → 0x45; raw mirror ORIGINAL id. StopMotion 0x00528530
mirror shape. StopCompletely 0x00527e40 with the A9 snapshot quirk
(motion_allows_jump on the OLD forward command, stashed in the queued
node) + the J9 fix (sidestep/turn SPEEDS untouched — the 1.0 resets
were a divergence). PerformMovement flushes zero-tick completions after
every dispatched op via the new CheckForCompletedMotions seam
(0x0050fe30) — bound App-side per entity (remotes via
EnsureRemoteMotionBindings, player at the sequencer bind site).

PORT DISCOVERY (not in the W0 pins): retail's DoInterpretedMotion
RESULT gates both add_to_queue AND the state write — a void sink let
the style dispatch's apply-only failure clobber ForwardCommand before
the forward axis read it, regressing 74/183 live-trace cases.
IInterpretedMotionSink.ApplyMotion/StopMotion now return bool
(documented on the interface with the raw anchors); one funnel
assertion corrected with raw-line citations (airborne ForwardCommand
ends at Falling under the fully-wired verbatim algorithm).

62 new gate-table tests. Full suite: 3,729 passed.

Implemented by a dedicated agent against the W0-pinned spec; seam
bound + suite independently re-verified by the orchestrator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 08:57:33 +02:00
parent e214acdf23
commit df7b096d6f
7 changed files with 1522 additions and 241 deletions

View file

@ -19,10 +19,23 @@ public class MotionInterpreterFunnelTests
private sealed class RecordingSink : IInterpretedMotionSink
{
public readonly List<string> Calls = new();
public void ApplyMotion(uint motion, float speed)
=> Calls.Add($"DIM {motion:x8}@{speed:F2}");
public void StopMotion(uint motion)
=> Calls.Add($"STOP {motion:x8}");
public bool ApplyMotion(uint motion, float speed)
{
Calls.Add($"DIM {motion:x8}@{speed:F2}");
// R3-W5: a style/stance id (>= 0x80000000, i.e. negative as
// int32) has no locomotion MotionData entry in the dat — retail's
// real CMotionTable::DoObjectMotion genuinely fails for it
// (MotionTableManagerError.MotionFailed). The fake sink mirrors
// that so InterpretedMotionState.ForwardCommand isn't clobbered
// by ApplyMotion's negative-motion branch before the very next
// dispatch reads it — matches the live retail-observer trace.
return motion < 0x80000000u;
}
public bool StopMotion(uint motion)
{
Calls.Add($"STOP {motion:x8}");
return true;
}
}
private static MotionInterpreter GroundedInterp()
@ -164,8 +177,26 @@ public class MotionInterpreterFunnelTests
// (0x00528240 early-accept), so it reaches the sink; the style and
// forward dispatches are gate-blocked (apply-only path).
Assert.Equal(new[] { "DIM 40000015@1.00", "STOP 6500000d" }, sink.Calls);
// The interpreted STATE still flat-copies (velocity uses it on landing).
Assert.Equal(0x44000007u, mi.InterpretedState.ForwardCommand);
// R3-W5 correction: DoInterpretedMotion's verbatim body (raw
// 305575-305631) writes InterpretedState via
// InterpretedMotionState::ApplyMotion on BOTH the success path
// (raw 305609-305610, gated on the sink's own result) AND the
// blocked/apply-only `label_528440` path (raw 305616-305618,
// UNCONDITIONAL on the ModifyInterpretedState bit — no sink
// involved at all). This W5 slice is the first port to actually
// WIRE that state-write (the pre-W5 funnel never called
// InterpretedMotionState.ApplyMotion from dispatch at all — state
// was set only by MoveToInterpretedState's flat copy at the top of
// the call), so the blocked style dispatch DOES flip
// ForwardCommand to Ready (0x41000003) at step 1, which then makes
// the Falling substitution dispatch (step 2) ITself write
// ForwardCommand = Falling (0x40000015) via the SAME mechanism —
// this replaces the assertion this comment used to make ("state
// still flat-copies") with the verbatim end state. Only
// sink.Calls (dispatch ORDER) was ever cdb-verified by this file's
// module doc; this resulting-state assertion was an untested
// assumption from the pre-W5 architecture.
Assert.Equal(0x40000015u, mi.InterpretedState.ForwardCommand);
}
[Fact]