Root cause (pinned by the worktree investigation + verified against source): CMotionInterp's dispatch tails strip link animations for DETACHED objects only (retail `if (physics_obj->cell == 0) RemoveLinkAnimations`, raw @305627, three ported sites: DoInterpretedMotion / StopInterpretedMotion / StopCompletely). The R3 port proxied retail's cell pointer with CellPosition.ObjCellId == 0 - but that #145 field is seeded ONLY by SnapToCell, whose single production caller is the LOCAL PLAYER's SetPosition. Every REMOTE body therefore read "detached" forever, and every dispatch stripped the transition link the motion table had just appended: a used door's swing link died the same tick (pose snapped closed<->open - the live symptom), and remote walk<->run link poses have been silently eaten since the guard landed (masked: locomotion cycles resemble each other; a door's two poses don't). The dispatcher itself (GetObjectSequence link path) was verified retail-verbatim end-to-end - not the bug. Fix: PhysicsBody.InWorld - an explicit "placed in the world" flag carrying exactly the guard's retail meaning (register row TS-40, replacing the previously UNREGISTERED ObjCellId proxy). Set by SnapToCell (retail: enter_world/set_cell assigns the cell) and by RemoteMotion construction (a RemoteMotion exists only for a world entity). All three guard sites now test !InWorld. Default false = retail's pre-enter_world detached state, preserving the guard for genuinely unplaced bodies (bare-harness interps unchanged). Tests: InWorldLinkGuardTests pins the polarity at all guard classes (in-world dispatch keeps links; detached strips; StopCompletely same). Full suite green: 3,961. If the door still snaps after this, the open question is DATA (does the door's table author an On<->Off link at all) - next step per the investigation: dump the real table's Links/StyleDefaults via a DoorSetupGfxObjInspectionTests extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.7 KiB
C#
78 lines
2.7 KiB
C#
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// R4-V5 door-swing fix (2026-07-03, register TS-40): CMotionInterp's
|
|
/// dispatch tails strip link animations for DETACHED objects only (retail
|
|
/// <c>if (physics_obj->cell == 0) RemoveLinkAnimations</c>, raw @305627).
|
|
/// The old proxy (<c>CellPosition.ObjCellId == 0</c>) was seeded only by
|
|
/// the local player's SnapToCell, so every REMOTE body read "detached" and
|
|
/// every dispatched transition link (door open/close swings, remote
|
|
/// walk↔run links) was stripped the same tick it was appended — the pose
|
|
/// snapped straight to the new cycle. These pin the corrected
|
|
/// <see cref="PhysicsBody.InWorld"/> guard polarity.
|
|
/// </summary>
|
|
public class InWorldLinkGuardTests
|
|
{
|
|
[Fact]
|
|
public void InWorldBody_DispatchKeepsTransitionLinks()
|
|
{
|
|
var body = new PhysicsBody
|
|
{
|
|
InWorld = true,
|
|
TransientState = TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable
|
|
| TransientStateFlags.Active,
|
|
};
|
|
var interp = new MotionInterpreter(body);
|
|
int strips = 0;
|
|
interp.RemoveLinkAnimations = () => strips++;
|
|
|
|
interp.DoInterpretedMotion(MotionCommand.WalkForward, new MovementParameters());
|
|
|
|
Assert.Equal(0, strips);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetachedBody_DispatchStripsLinks_RetailGuard()
|
|
{
|
|
var body = new PhysicsBody
|
|
{
|
|
// InWorld left false — retail's pre-enter_world detached state.
|
|
TransientState = TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable
|
|
| TransientStateFlags.Active,
|
|
};
|
|
var interp = new MotionInterpreter(body);
|
|
int strips = 0;
|
|
interp.RemoveLinkAnimations = () => strips++;
|
|
|
|
interp.DoInterpretedMotion(MotionCommand.WalkForward, new MovementParameters());
|
|
|
|
Assert.Equal(1, strips);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteShapedBody_StopCompletely_KeepsLinksToo()
|
|
{
|
|
// The other two guard sites (StopCompletely / StopInterpretedMotion)
|
|
// share the same InWorld polarity.
|
|
var body = new PhysicsBody
|
|
{
|
|
InWorld = true,
|
|
TransientState = TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable
|
|
| TransientStateFlags.Active,
|
|
};
|
|
var interp = new MotionInterpreter(body);
|
|
int strips = 0;
|
|
interp.RemoveLinkAnimations = () => strips++;
|
|
|
|
interp.StopCompletely();
|
|
|
|
Assert.Equal(0, strips);
|
|
}
|
|
}
|