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:
parent
530453067a
commit
f423884bd1
9 changed files with 213 additions and 20 deletions
|
|
@ -398,7 +398,13 @@ retail; flat-ground landings unchanged; no micro-bounce death spiral
|
||||||
|
|
||||||
## #164 — UM action-replay dispatches drop the per-action Autonomous bit
|
## #164 — UM action-replay dispatches drop the per-action Autonomous bit
|
||||||
|
|
||||||
**Status:** OPEN (2026-07-03, filed during #161)
|
**Status:** DONE (2026-07-04, R5-V4) — `DispatchInterpretedMotion` now threads
|
||||||
|
the action's autonomy into the dispatch params (`Autonomous` = the 0x1000
|
||||||
|
splice, raw 305982); the stored `InterpretedState.Actions` node carries the
|
||||||
|
real autonomy. Conformance:
|
||||||
|
`MotionInterpreterFunnelTests.Actions_ReplayCarriesAutonomyIntoTheInterpretedList`.
|
||||||
|
(Move to Recently closed on next ISSUES tidy.) Original finding below.
|
||||||
|
**Filed:** OPEN (2026-07-03, filed during #161)
|
||||||
**Finding:** retail's `move_to_interpreted_state` action loop sets the
|
**Finding:** retail's `move_to_interpreted_state` action loop sets the
|
||||||
dispatch params' Autonomous bit (0x1000) from each action's autonomy flag
|
dispatch params' Autonomous bit (0x1000) from each action's autonomy flag
|
||||||
(raw 305982: `var_28 ^= ((autonomous << 0xc) ^ var_28) & 0x1000`), which
|
(raw 305982: `var_28 ^= ((autonomous << 0xc) ^ var_28) & 0x1000`), which
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,18 @@ commit.** **VISUAL GATE**: a sticky scenario (server /follow-style sticky moveto
|
||||||
or a moving-platform stick) HOLDS the target instead of stopping-and-drifting.
|
or a moving-platform stick) HOLDS the target instead of stopping-and-drifting.
|
||||||
|
|
||||||
### V4 (capstone) — MovementManager facade + #164 + head-stance dispatch + docs
|
### V4 (capstone) — MovementManager facade + #164 + head-stance dispatch + docs
|
||||||
|
|
||||||
|
> **STATUS 2026-07-04: the three BEHAVIORAL items SHIPPED** (same session as
|
||||||
|
> the V3 gates): head style-on-change at both GameWindow routing heads
|
||||||
|
> (@00524502-0052452c, all movement types), #164 action-replay Autonomous
|
||||||
|
> bit (raw 305982), mt-0 wire flags consumed (0x1 stick_to_object
|
||||||
|
> 0x005127e0 → PositionManager.StickTo at both case-0 tails; 0x2 →
|
||||||
|
> `Motion.StandingLongJump`, unconditional per @0052458e). Conformance:
|
||||||
|
> stance-on-arm harness scenario + the autonomy funnel test; suite 4041
|
||||||
|
> green. **The MovementManager facade DEFERRED to its own slice** (the
|
||||||
|
> handoff's own "optional if the arc runs long" clause — this arc ran two
|
||||||
|
> full gate cycles); it remains the structural capstone for a fresh
|
||||||
|
> session, unblocked, spec below.
|
||||||
- Collapse per-entity `Motion`+`MoveTo` into a `MovementManager` owner
|
- Collapse per-entity `Motion`+`MoveTo` into a `MovementManager` owner
|
||||||
(structural; keep 183-case/funnel/moveto green UNMODIFIED). Optional if the
|
(structural; keep 183-case/funnel/moveto green UNMODIFIED). Optional if the
|
||||||
arc runs long — the retirements above don't need it.
|
arc runs long — the retirements above don't need it.
|
||||||
|
|
|
||||||
|
|
@ -4474,6 +4474,27 @@ public sealed class GameWindow : IDisposable
|
||||||
+ delta.GetHeading());
|
+ delta.GetHeading());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// R5-V4: retail <c>CPhysicsObj::stick_to_object</c> (0x005127e0) — the
|
||||||
|
/// mt-0 WIRE sticky (unpack_movement case 0 @00524589, gated on the
|
||||||
|
/// motionFlags 0x1 trailer guid): resolve the target, read its PartArray
|
||||||
|
/// radius/height (0 when shapeless — retail's null-PartArray arm), then
|
||||||
|
/// <c>PositionManager::StickTo</c>. Unresolvable target → no stick at
|
||||||
|
/// all (retail's GetObjectA-null path). Retail resolves the target's
|
||||||
|
/// top-level PARENT first (<c>parent ?? self</c>); acdream's entity
|
||||||
|
/// table is flat (no client-side parenting), so the guid is used as-is —
|
||||||
|
/// the same convention as RouteServerMoveTo's TopLevelId.
|
||||||
|
/// </summary>
|
||||||
|
private void StickToObjectFromWire(EntityPhysicsHost? host, uint targetGuid)
|
||||||
|
{
|
||||||
|
if (host is null)
|
||||||
|
return;
|
||||||
|
if (!_entitiesByServerGuid.TryGetValue(targetGuid, out var tgtEnt))
|
||||||
|
return;
|
||||||
|
var (radius, height) = GetSetupCylinder(targetGuid, tgtEnt);
|
||||||
|
host.PositionManager.StickTo(targetGuid, radius, height);
|
||||||
|
}
|
||||||
|
|
||||||
private bool RemoveLiveEntityByServerGuid(uint serverGuid)
|
private bool RemoveLiveEntityByServerGuid(uint serverGuid)
|
||||||
{
|
{
|
||||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity))
|
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity))
|
||||||
|
|
@ -4936,6 +4957,24 @@ public sealed class GameWindow : IDisposable
|
||||||
_playerController.Motion.InterruptCurrentMovement?.Invoke();
|
_playerController.Motion.InterruptCurrentMovement?.Invoke();
|
||||||
_playerController.Motion.UnstickFromObject?.Invoke();
|
_playerController.Motion.UnstickFromObject?.Invoke();
|
||||||
|
|
||||||
|
// R5-V4a: unpack_movement HEAD style-on-change
|
||||||
|
// (0x00524440 @00524502-0052452c): wire style index →
|
||||||
|
// command word (command_ids[]; style-class indices map to
|
||||||
|
// 0x80000000|index — the funnel's S0-verified conversion)
|
||||||
|
// and `if (current style != wire style) DoMotion(style,
|
||||||
|
// ctor defaults)` BEFORE the movement-type switch — for
|
||||||
|
// EVERY type. acdream previously applied style only via
|
||||||
|
// the mt-0 funnel copy, so a chase/turn UM (mt 6-9)
|
||||||
|
// carrying a stance change started the move in the OLD
|
||||||
|
// stance (the RetailObserverTraceConformanceTests "S3
|
||||||
|
// wires the unpack-level style-on-change" exclusion —
|
||||||
|
// this is that wiring).
|
||||||
|
uint wireStylePlayer = stance != 0
|
||||||
|
? (0x80000000u | (uint)stance) : 0x8000003Du;
|
||||||
|
if (_playerController.Motion.InterpretedState.CurrentStyle != wireStylePlayer)
|
||||||
|
_playerController.Motion.DoMotion(wireStylePlayer,
|
||||||
|
new AcDream.Core.Physics.Motion.MovementParameters());
|
||||||
|
|
||||||
if (_playerController.MoveTo is { } playerMoveTo
|
if (_playerController.MoveTo is { } playerMoveTo
|
||||||
&& RouteServerMoveTo(playerMoveTo, _playerController.Motion,
|
&& RouteServerMoveTo(playerMoveTo, _playerController.Motion,
|
||||||
_playerController.CellId, update))
|
_playerController.CellId, update))
|
||||||
|
|
@ -4947,6 +4986,23 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// R5-V4: retail unpack_movement case-0 TAIL for the local
|
||||||
|
// player (@00524583-0052458e) — stick_to_object when the
|
||||||
|
// motionFlags 0x1 trailer carried a guid, then
|
||||||
|
// standing_longjump ← motionFlags 0x2 (UNCONDITIONAL —
|
||||||
|
// an absent flag clears it). The interpreted-state COPY
|
||||||
|
// stays skipped for the local player (the
|
||||||
|
// sequencer-authority posture above); these two writes
|
||||||
|
// are the only case-0 effects that apply here.
|
||||||
|
if (update.MotionState.MovementType == 0)
|
||||||
|
{
|
||||||
|
if (update.MotionState.StickyObjectGuid is { } playerSticky
|
||||||
|
&& playerSticky != 0)
|
||||||
|
StickToObjectFromWire(_playerHost, playerSticky);
|
||||||
|
_playerController.Motion.StandingLongJump =
|
||||||
|
update.MotionState.StandingLongJump;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -4996,6 +5052,18 @@ public sealed class GameWindow : IDisposable
|
||||||
remoteMot.Motion.InterruptCurrentMovement?.Invoke();
|
remoteMot.Motion.InterruptCurrentMovement?.Invoke();
|
||||||
remoteMot.Motion.UnstickFromObject?.Invoke();
|
remoteMot.Motion.UnstickFromObject?.Invoke();
|
||||||
|
|
||||||
|
// R5-V4a: unpack_movement head style-on-change — see the
|
||||||
|
// player-side comment (@00524502-0052452c). Runs BEFORE
|
||||||
|
// the type routing for every movement type; the mt-0
|
||||||
|
// funnel below still performs its own full style
|
||||||
|
// adoption (retail has BOTH — the head fires on CHANGE
|
||||||
|
// only, so an unchanged stance is a no-op here).
|
||||||
|
uint wireStyleRemote = stance != 0
|
||||||
|
? (0x80000000u | (uint)stance) : 0x8000003Du;
|
||||||
|
if (remoteMot.Motion.InterpretedState.CurrentStyle != wireStyleRemote)
|
||||||
|
remoteMot.Motion.DoMotion(wireStyleRemote,
|
||||||
|
new AcDream.Core.Physics.Motion.MovementParameters());
|
||||||
|
|
||||||
// R4-V5: the type-6..9 routing body is shared with the
|
// R4-V5: the type-6..9 routing body is shared with the
|
||||||
// local player (RouteServerMoveTo) — behavior identical
|
// local player (RouteServerMoveTo) — behavior identical
|
||||||
// to the R4-V4 inline blocks it was extracted from.
|
// to the R4-V4 inline blocks it was extracted from.
|
||||||
|
|
@ -5086,6 +5154,18 @@ public sealed class GameWindow : IDisposable
|
||||||
// motion-table stack (GetObjectSequence + is_allowed
|
// motion-table stack (GetObjectSequence + is_allowed
|
||||||
// decide) — mt-0 only post-V4 (types 6-9 returned above).
|
// decide) — mt-0 only post-V4 (types 6-9 returned above).
|
||||||
remoteMot.Motion.MoveToInterpretedState(ims, sink);
|
remoteMot.Motion.MoveToInterpretedState(ims, sink);
|
||||||
|
|
||||||
|
// R5-V4: retail unpack_movement case-0 TAIL order
|
||||||
|
// (@00524583-0052458e): move_to_interpreted_state FIRST
|
||||||
|
// (above), THEN stick_to_object when the motionFlags 0x1
|
||||||
|
// trailer carried a guid, THEN standing_longjump ←
|
||||||
|
// motionFlags 0x2 (UNCONDITIONAL — an absent flag
|
||||||
|
// CLEARS it).
|
||||||
|
if (update.MotionState.StickyObjectGuid is { } stickyGuid
|
||||||
|
&& stickyGuid != 0)
|
||||||
|
StickToObjectFromWire(remoteMot.Host, stickyGuid);
|
||||||
|
remoteMot.Motion.StandingLongJump =
|
||||||
|
update.MotionState.StandingLongJump;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,10 +259,16 @@ public static class CreateObject
|
||||||
// InterpretedMotionState::UnPack (r4-moveto-decomp.md §2f
|
// InterpretedMotionState::UnPack (r4-moveto-decomp.md §2f
|
||||||
// @0052455d: `if (header & 0x100) sticky_object_guid = read_dword()`
|
// @0052455d: `if (header & 0x100) sticky_object_guid = read_dword()`
|
||||||
// — bit 0x100 of the combined header word is motionFlags byte1&0x1).
|
// — bit 0x100 of the combined header word is motionFlags byte1&0x1).
|
||||||
// Carried unconsumed until R5's PositionManager::StickTo body binds
|
// R5-V4 consumes it: the GameWindow mt-0 tail routes it into
|
||||||
// it; parsing it here just keeps the buffer cursor honest past this
|
// CPhysicsObj::stick_to_object's port (target PartArray radii →
|
||||||
// field (deliverable C — no behavior).
|
// PositionManager.StickTo — decomp 0x005127e0, call @00524589).
|
||||||
uint? StickyObjectGuid = null)
|
uint? StickyObjectGuid = null,
|
||||||
|
// R5-V4 (closes the "documented but NOT consumed" note in
|
||||||
|
// UpdateMotion.cs): motionFlags & 0x2 — retail `unpack_movement`
|
||||||
|
// case 0 writes it onto `motion_interpreter->standing_longjump`
|
||||||
|
// UNCONDITIONALLY (@0052458e: absent flag CLEARS it). Consumed at
|
||||||
|
// the GameWindow mt-0 tails (remote + player).
|
||||||
|
bool StandingLongJump = false)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ACE/retail movement types 6 and 7 are server-controlled
|
/// ACE/retail movement types 6 and 7 are server-controlled
|
||||||
|
|
|
||||||
|
|
@ -151,13 +151,11 @@ public static class UpdateMotion
|
||||||
// `_motionFlags`. StickToObject now drives the sticky-guid
|
// `_motionFlags`. StickToObject now drives the sticky-guid
|
||||||
// trailer parse below (mt=0 only, per ACE MovementInvalid.Write
|
// trailer parse below (mt=0 only, per ACE MovementInvalid.Write
|
||||||
// + decomp §2f case 0 @0052455d). StandingLongJump (0x2) is
|
// + decomp §2f case 0 @0052455d). StandingLongJump (0x2) is
|
||||||
// DOCUMENTED here but NOT consumed — retail's
|
// carried on ServerMotionState.StandingLongJump (R5-V4) and
|
||||||
|
// consumed at the GameWindow mt-0 tails — retail's
|
||||||
// `unpack_movement` case 0 writes it onto
|
// `unpack_movement` case 0 writes it onto
|
||||||
// `motion_interpreter->standing_longjump` (§2f @0052458e), an
|
// `motion_interpreter->standing_longjump` (§2f @0052458e,
|
||||||
// R5 unpack_movement item (this parser has no MotionInterpreter
|
// UNCONDITIONAL — an absent flag clears it).
|
||||||
// reference to write it onto; the bit is simply not read past
|
|
||||||
// this comment, matching "not consumed" rather than
|
|
||||||
// mis-consumed).
|
|
||||||
byte motionFlags = body[pos]; pos += 1;
|
byte motionFlags = body[pos]; pos += 1;
|
||||||
ushort currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
ushort currentStyle = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos));
|
||||||
pos += 2;
|
pos += 2;
|
||||||
|
|
@ -323,7 +321,11 @@ public static class UpdateMotion
|
||||||
moveToRunRate,
|
moveToRunRate,
|
||||||
moveToPath,
|
moveToPath,
|
||||||
turnToPath,
|
turnToPath,
|
||||||
stickyObjectGuid),
|
stickyObjectGuid,
|
||||||
|
// R5-V4: MotionFlags 0x2 — consumed at the GameWindow mt-0
|
||||||
|
// tails (retail unpack_movement @0052458e writes it onto
|
||||||
|
// minterp->standing_longjump unconditionally).
|
||||||
|
(motionFlags & 0x2) != 0),
|
||||||
instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
instanceSequence, movementSequence, serverControlSequence, isAutonomous);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
|
||||||
|
|
@ -2860,7 +2860,7 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
if (IsLocalPlayer && a.Autonomous) continue;
|
if (IsLocalPlayer && a.Autonomous) continue;
|
||||||
|
|
||||||
ServerActionStamp = incoming;
|
ServerActionStamp = incoming;
|
||||||
DispatchInterpretedMotion(a.Command, a.Speed, sink);
|
DispatchInterpretedMotion(a.Command, a.Speed, a.Autonomous, sink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3291,10 +3291,16 @@ public sealed class MotionInterpreter : IMotionDoneSink
|
||||||
/// <c>ModifyInterpretedState = true</c>, so replayed actions DO enter the
|
/// <c>ModifyInterpretedState = true</c>, so replayed actions DO enter the
|
||||||
/// interpreted actions list via <c>InterpretedMotionState::AddAction</c>),
|
/// interpreted actions list via <c>InterpretedMotionState::AddAction</c>),
|
||||||
/// unlike the apply pass's rewritten params (see
|
/// unlike the apply pass's rewritten params (see
|
||||||
/// <see cref="ApplyInterpretedMovement"/>). Known residual gap: retail
|
/// <see cref="ApplyInterpretedMovement"/>). R5-V4 (#164): the params'
|
||||||
/// also sets the params' <c>Autonomous</c> bit (0x1000) per action (raw
|
/// <c>Autonomous</c> bit (0x1000) is spliced from the ACTION's autonomy
|
||||||
/// 305982) — not yet threaded (tracked in ISSUES, #161 follow-up note).
|
/// flag — retail raw 305982:
|
||||||
|
/// <c>var_28 ^= ((action.autonomous << 0xc) ^ var_28) & 0x1000</c>
|
||||||
|
/// — so a replayed action enters the state lists
|
||||||
|
/// (<c>AddAction(motion, speed, stamp, autonomous)</c>) with its real
|
||||||
|
/// autonomy instead of the ctor-default false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private WeenieError DispatchInterpretedMotion(uint motion, float speed, IInterpretedMotionSink? sink)
|
private WeenieError DispatchInterpretedMotion(
|
||||||
=> DoInterpretedMotion(motion, new MovementParameters { Speed = speed }, sink);
|
uint motion, float speed, bool autonomous, IInterpretedMotionSink? sink)
|
||||||
|
=> DoInterpretedMotion(
|
||||||
|
motion, new MovementParameters { Speed = speed, Autonomous = autonomous }, sink);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,11 +251,22 @@ internal sealed class RemoteChaseHarness
|
||||||
bool sticky = false,
|
bool sticky = false,
|
||||||
bool useSpheres = false,
|
bool useSpheres = false,
|
||||||
float targetRadius = 0f,
|
float targetRadius = 0f,
|
||||||
float targetHeight = 0f)
|
float targetHeight = 0f,
|
||||||
|
uint wireStance = 0u)
|
||||||
{
|
{
|
||||||
Interp.InterruptCurrentMovement?.Invoke();
|
Interp.InterruptCurrentMovement?.Invoke();
|
||||||
Interp.UnstickFromObject?.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
|
var mp = new MovementParameters
|
||||||
{
|
{
|
||||||
CanWalk = true,
|
CanWalk = true,
|
||||||
|
|
@ -871,4 +882,47 @@ public sealed class RemoteChaseEndToEndHarnessTests
|
||||||
$"chase did not re-stick after the re-arm; mt={h.Mgr.MovementTypeState} " +
|
$"chase did not re-stick after the re-arm; mt={h.Mgr.MovementTypeState} " +
|
||||||
$"dist={h.DistToPlayer:F2}");
|
$"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,26 @@ public class MotionInterpreterFunnelTests
|
||||||
Assert.DoesNotContain(sink.Calls, c => c.StartsWith("DIM 10000062"));
|
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]
|
[Fact]
|
||||||
public void InboundState_Defaults_MatchRetailUnPack()
|
public void InboundState_Defaults_MatchRetailUnPack()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,14 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
/// tests in MotionInterpreterFunnelTests instead.
|
/// tests in MotionInterpreterFunnelTests instead.
|
||||||
/// - motion==0x80000000 entries (the unpack-level DoMotion(command_ids[0])
|
/// - motion==0x80000000 entries (the unpack-level DoMotion(command_ids[0])
|
||||||
/// style call for wire style index 0) — outside
|
/// 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>
|
/// </summary>
|
||||||
public class RetailObserverTraceConformanceTests
|
public class RetailObserverTraceConformanceTests
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue