feat(physics): R5-V4 behavioral slice — head stance dispatch (all mt) + #164 autonomy bit + mt-0 wire flags

Three unpack_movement parity items (facade deferred per the handoff's
own optional clause — see r5-wiring-handoff §V4 status):

1. HEAD style-on-change (0x00524440 @00524502-0052452c): both GameWindow
   routing heads (remote + player) now dispatch DoMotion(style, ctor
   defaults) when the UM's stance differs from the interp's current
   style, BEFORE the movement-type routing — for EVERY type. Previously
   style applied only through the mt-0 funnel copy, so a chase/turn UM
   (mt 6-9) carrying a stance change started the move in the OLD stance
   (a monster charged in NonCombat posture until the next mt-0). The
   RetailObserverTraceConformanceTests exclusion note updated — the
   trace filter stays (head calls can't appear in a
   MoveToInterpretedState replay) but the production gap it pointed at
   is closed.

2. #164 (closes): the action-replay loop threads each action's autonomy
   into the dispatch params (Autonomous = the 0x1000 splice, raw
   305982) — replayed actions enter the interpreted actions list with
   their real autonomy instead of ctor-default false.

3. mt-0 wire flags consumed (UpdateMotion parsed them since R4-V3):
   0x1 StickToObject → CPhysicsObj::stick_to_object port (0x005127e0:
   resolve target, PartArray radii — 0 when shapeless, guid-as-is for
   acdream's flat entity table — → PositionManager.StickTo; unresolvable
   target → no stick), at BOTH case-0 tails in retail order
   (@00524583-0052458e: funnel apply → stick → longjump flag);
   0x2 StandingLongJump → Motion.StandingLongJump, UNCONDITIONAL write
   (absent flag clears — retail @0052458e). ServerMotionState gains the
   StandingLongJump field.

Conformance: ChaseArm_WithStanceChange_AppliesStanceBeforeTheChase
(harness mirrors the routing-head dispatch) +
Actions_ReplayCarriesAutonomyIntoTheInterpretedList. Suite 4041 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-05 10:18:26 +02:00
parent 530453067a
commit f423884bd1
9 changed files with 213 additions and 20 deletions

View file

@ -251,11 +251,22 @@ internal sealed class RemoteChaseHarness
bool sticky = false,
bool useSpheres = false,
float targetRadius = 0f,
float targetHeight = 0f)
float targetHeight = 0f,
uint wireStance = 0u)
{
Interp.InterruptCurrentMovement?.Invoke();
Interp.UnstickFromObject?.Invoke();
// R5-V4a: the unpack_movement HEAD style-on-change (GameWindow's
// routing-head mirror — 0x00524440 @00524502-0052452c): fires for
// EVERY movement type, BEFORE the type routing, on CHANGE only.
if (wireStance != 0u)
{
uint wireStyle = 0x80000000u | wireStance;
if (Interp.InterpretedState.CurrentStyle != wireStyle)
Interp.DoMotion(wireStyle, new MovementParameters());
}
var mp = new MovementParameters
{
CanWalk = true,
@ -871,4 +882,47 @@ public sealed class RemoteChaseEndToEndHarnessTests
$"chase did not re-stick after the re-arm; mt={h.Mgr.MovementTypeState} " +
$"dist={h.DistToPlayer:F2}");
}
/// <summary>
/// R5-V4a: the unpack-head style-on-change — a chase arm (mt-6) whose UM
/// carries a CHANGED stance applies the stance FIRST (retail
/// unpack_movement @00524502-0052452c dispatches DoMotion(style) before
/// the movement-type switch), so the creature draws into Combat and THEN
/// runs — instead of chasing in the old NonCombat stance until the next
/// mt-0 UM. Closes the RetailObserverTraceConformanceTests "S3 wires the
/// unpack-level style-on-change" exclusion's production gap.
/// </summary>
[Fact]
public void ChaseArm_WithStanceChange_AppliesStanceBeforeTheChase()
{
var h = new RemoteChaseHarness(_out);
h.PlayerPos = new Vector3(0f, 12f, 0f);
h.PlayerVelocity = Vector3.Zero;
// settle in NonCombat — NO prior stance UM (the pre-V4 gap: nothing
// but an mt-0 could change the stance).
for (int i = 0; i < Seconds(0.5f); i++) h.Tick();
Assert.Equal(RemoteChaseHarness.NonCombat, h.Seq.Manager.State.Style);
// the arm carries the Combat stance in its UM header (mt-6).
h.UmMoveToObject(wireStance: RemoteChaseHarness.Combat & 0xFFFFu);
// the stance adopts at the head — before any chase motion completes.
Assert.Equal(RemoteChaseHarness.Combat, h.Interp.InterpretedState.CurrentStyle);
// ...and the chase still installs its forward cycle normally, now in
// the Combat style family.
int installTick = -1;
for (int i = 0; i < Seconds(6f) && installTick < 0; i++)
{
h.Tick();
if (h.Seq.Manager.State.Substate == RemoteChaseHarness.Run
|| h.Seq.Manager.State.Substate == RemoteChaseHarness.Walk)
installTick = i;
}
Assert.True(installTick >= 0,
$"forward cycle never installed after the stance-carrying arm; " +
$"mt={h.Mgr.MovementTypeState} substate=0x{h.Seq.Manager.State.Substate:X8}");
Assert.Equal(RemoteChaseHarness.Combat, h.Seq.Manager.State.Style);
}
}

View file

@ -340,6 +340,26 @@ public class MotionInterpreterFunnelTests
Assert.DoesNotContain(sink.Calls, c => c.StartsWith("DIM 10000062"));
}
[Fact]
public void Actions_ReplayCarriesAutonomyIntoTheInterpretedList()
{
// R5-V4 (#164): retail splices the ACTION's autonomy into the
// dispatch params (bit 0x1000 — raw 305982:
// `var_28 ^= ((action.autonomous << 0xc) ^ var_28) & 0x1000`), so a
// replayed action enters the interpreted actions list
// (InterpretedMotionState::AddAction consumes p.Autonomous) with its
// REAL autonomy. Pre-V4 the replay params were ctor-default →
// Autonomous always false.
var mi = GroundedInterp(); // remote posture: IsLocalPlayer = false
var sink = new RecordingSink();
mi.MoveToInterpretedState(
Ims(actions: new[] { new InboundMotionAction(0x10000062u, 5, Autonomous: true, 1f) }), sink);
var entry = Assert.Single(mi.InterpretedState.Actions, a => a.Command == 0x0062);
Assert.True(entry.Autonomous);
}
[Fact]
public void InboundState_Defaults_MatchRetailUnPack()
{

View file

@ -30,7 +30,14 @@ namespace AcDream.Core.Tests.Physics;
/// tests in MotionInterpreterFunnelTests instead.
/// - motion==0x80000000 entries (the unpack-level DoMotion(command_ids[0])
/// style call for wire style index 0) — outside
/// move_to_interpreted_state; S3 wires the unpack-level style-on-change.
/// move_to_interpreted_state, so it can never appear in a
/// MoveToInterpretedState replay; the filter stays for that trace-
/// mechanics reason. The PRODUCTION gap the old note pointed at ("S3
/// wires the unpack-level style-on-change") CLOSED in R5-V4: the
/// GameWindow routing heads dispatch DoMotion(style) on change for every
/// movement type (unpack_movement @00524502-0052452c; conformance:
/// RemoteChaseEndToEndHarnessTests.ChaseArm_WithStanceChange_
/// AppliesStanceBeforeTheChase).
/// </summary>
public class RetailObserverTraceConformanceTests
{