fix(R4-V5): remote transition links stripped by the detached-object guard - door swings snapped (adds register TS-40)

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>
This commit is contained in:
Erik 2026-07-03 15:10:16 +02:00
parent 006cf6597a
commit 350fb5e3a5
5 changed files with 126 additions and 3 deletions

View file

@ -603,6 +603,12 @@ public sealed class GameWindow : IDisposable
TransientState = AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
| AcDream.Core.Physics.TransientStateFlags.Active,
// R4-V5 door-swing fix: a RemoteMotion exists only for a
// WORLD entity — retail's physics_obj->cell is non-null the
// moment the object is placed. Without this, the interp's
// detached-object guard stripped every dispatched
// transition link (see PhysicsBody.InWorld).
InWorld = true,
};
Motion = new AcDream.Core.Physics.MotionInterpreter(Body);
}

View file

@ -1247,7 +1247,14 @@ public sealed class MotionInterpreter : IMotionDoneSink
AddToQueue(contextId: 0, MotionCommand.Ready, (uint)jumpSnapshot);
if (PhysicsObj.CellPosition.ObjCellId == 0)
// R4-V5 door-swing fix (2026-07-03): retail's guard is
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
// only by the local player's SnapToCell) read every REMOTE body as
// detached, so every dispatch stripped the just-appended transition
// link (door swings snapped; remote walk↔run links died) — see
// PhysicsBody.InWorld (register row).
if (!PhysicsObj.InWorld)
RemoveLinkAnimations?.Invoke();
return WeenieError.None;
@ -3132,7 +3139,14 @@ public sealed class MotionInterpreter : IMotionDoneSink
result = WeenieError.NotGrounded; // 0x24 (A10) — blocked action-class motion
}
if (PhysicsObj.CellPosition.ObjCellId == 0)
// R4-V5 door-swing fix (2026-07-03): retail's guard is
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
// only by the local player's SnapToCell) read every REMOTE body as
// detached, so every dispatch stripped the just-appended transition
// link (door swings snapped; remote walk↔run links died) — see
// PhysicsBody.InWorld (register row).
if (!PhysicsObj.InWorld)
RemoveLinkAnimations?.Invoke();
return result;
@ -3235,7 +3249,14 @@ public sealed class MotionInterpreter : IMotionDoneSink
}
}
if (PhysicsObj.CellPosition.ObjCellId == 0)
// R4-V5 door-swing fix (2026-07-03): retail's guard is
// physics_obj->cell == 0 — DETACHED objects only (raw @305627).
// The old proxy (CellPosition.ObjCellId == 0, #145 machinery seeded
// only by the local player's SnapToCell) read every REMOTE body as
// detached, so every dispatch stripped the just-appended transition
// link (door swings snapped; remote walk↔run links died) — see
// PhysicsBody.InWorld (register row).
if (!PhysicsObj.InWorld)
RemoveLinkAnimations?.Invoke();
return result;

View file

@ -115,6 +115,22 @@ public sealed class PhysicsBody
/// </summary>
public Position CellPosition { get; private set; }
/// <summary>
/// R4-V5 (the door-swing snap fix, 2026-07-03): the retail
/// <c>physics_obj-&gt;cell != 0</c> "placed in the world" truth (register
/// row). CMotionInterp's dispatch tails strip link animations for
/// DETACHED objects only (<c>if (cell == 0) RemoveLinkAnimations</c>,
/// raw @305627); the previous proxy — <see cref="CellPosition"/>.<c>ObjCellId
/// == 0</c> — was seeded ONLY by the local player's <see cref="SnapToCell"/>
/// (#145 machinery), so every REMOTE body read "detached" forever and
/// every dispatched transition link (door swings, walk↔run links) was
/// stripped the same tick it was appended. Set by <see cref="SnapToCell"/>
/// (local player placement) and by RemoteMotion construction (remotes
/// exist only for world entities). Default false = detached, matching
/// retail's pre-enter_world state.
/// </summary>
public bool InWorld { get; set; }
/// <summary>
/// Placement: set the world position AND seed <see cref="CellPosition"/> from the
/// wire's (cell, local) — NO streaming center, NO delta. For an OUTDOOR cell the
@ -137,6 +153,7 @@ public sealed class PhysicsBody
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
LandDefs.AdjustToOutside(ref cell, ref local);
CellPosition = new Position(cell, new CellFrame(local, Orientation));
InWorld = true; // retail: enter_world / set_cell assigns physics_obj->cell
}
// Mirror a world-position translation into the cell-relative frame. Velocity is