feat(R3-W2): pending_motions lifecycle + the MotionDone consumer — the R2 seam lands (closes J1, J17)
MotionInterpreter implements IMotionDoneSink: pending_motions (LinkedList<MotionNode>, AD-34 wording) + add_to_queue 0x00527b80 + motions_pending 0x00527fe0 + MotionDone 0x00527ec0 verbatim (action-class head → UnstickFromObject no-op seam (→R5 StickyManager) + BOTH states' action-FIFO pops; head popped UNCONDITIONALLY — A7: never match-by-id, params unread in this build) + HandleExitWorld 0x00527f30 (the raw decompile's null-physics-obj branch would infinite-loop if translated literally — adjudicated as retail dead code, documented on the method) + is_standing_still 0x00527fa0 + MotionAllowsJump 0x005279e0 with the A1-pinned literal blocklist (28-case boundary table test: Fallen blocks, Falling passes — ACE's transposition NOT copied). Queue producers wired into the funnel per the decomp anchors (re-verified from the raw text): DispatchInterpretedMotion's success path computes jump_error_code via retail's double-check shape (motion_allows_jump(motion), then forward_command when non-action; the DisableJumpDuringLink params leg is TODO(W5) — no MovementParameters threaded yet) + add_to_queue; apply_interpreted_movement's turn-stop re-queues the Ready node (@305766-305785 — the plan's producer #2 and #3 are the SAME site; StopInterpretedMotion's internal add_to_queue @305657 is W5 scope). ContextId=0 with TODO(W5): retail's own compiled code reads an UNINITIALIZED local for it at this call site. GameWindow: MotionDoneTarget now binds the entity's REAL consumer — player via PlayerMovementController.Motion (new internal property), remotes via RemoteMotion.Motion — resolved AT FIRE TIME (a remote's RemoteMotion can be created after its first anim tick; an eager capture would drop completions forever). Despawn runs BOTH layers' exit-world drains (manager then interp) per §4. AD-36 narrowed (consumed for creature-class; doors/statics recorder-only until R5). 51 new conformance cases incl. the end-to-end chain test: MotionTableManager.AnimationDone → sink → interp pending head pops in step. 183-case observer-trace + funnel suites green unchanged (queue side effects additive). Full suite: 3,582 passed. Implemented by a dedicated agent (MotionInterpreter side) against the W0-pinned spec; producer placements + MotionAllowsJump branch algebra independently reviewed against the raw decomp; GameWindow wiring by the orchestrator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8664959152
commit
371679915e
5 changed files with 696 additions and 13 deletions
|
|
@ -4183,12 +4183,15 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_worldState.RemoveEntityByServerGuid(serverGuid);
|
||||
_worldGameState.RemoveById(existingEntity.Id);
|
||||
// R2-Q5: retail's exit-world drain — every pending queue entry fires
|
||||
// MotionDone(success:false) before the object goes away
|
||||
// (MotionTableManager::HandleExitWorld 0x0051bda0; R3 adds the
|
||||
// CMotionInterp-side drain alongside per r3-port-plan §4).
|
||||
// R2-Q5 + R3-W2: retail runs BOTH layers' exit-world drains
|
||||
// independently (r3-port-plan §4): the manager's (each pending
|
||||
// animation fires MotionDone(success:false) → the bound interp pops
|
||||
// in step) THEN the interp's own (flushes any remainder —
|
||||
// CMotionInterp::HandleExitWorld 0x00527f30).
|
||||
if (_animatedEntities.TryGetValue(existingEntity.Id, out var aeGone))
|
||||
aeGone.Sequencer?.Manager.HandleExitWorld();
|
||||
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmGone))
|
||||
rmGone.Motion.HandleExitWorld();
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
_classificationCache.InvalidateEntity(existingEntity.Id);
|
||||
_physicsEngine.ShadowObjects.Deregister(existingEntity.Id);
|
||||
|
|
@ -9876,17 +9879,37 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
|
||||
// R2-Q4: bind the MotionDone diagnostic recorder once per
|
||||
// sequencer (the IMotionDoneSink seam's interim consumer —
|
||||
// R3 binds MotionInterpreter.MotionDone here instead).
|
||||
// R3-W2: bind the MotionDone seam once per sequencer to the
|
||||
// entity's REAL consumer — the MotionInterpreter's
|
||||
// pending_motions pop (retail CPhysicsObj::MotionDone
|
||||
// 0x0050fdb0 → MovementManager → CMotionInterp::MotionDone
|
||||
// 0x00527ec0; r3-port-plan.md §4). Remotes bind their
|
||||
// RemoteMotion interp; the local player binds the
|
||||
// controller's; interp-less entities (doors, statics) keep
|
||||
// the diagnostic recorder only.
|
||||
if (ae.Sequencer.MotionDoneTarget is null)
|
||||
{
|
||||
uint mdGuid = serverGuid;
|
||||
// Resolve the interp AT FIRE TIME, not bind time: a
|
||||
// remote's RemoteMotion is created on its first UM/UP,
|
||||
// which can arrive AFTER the first anim tick — an
|
||||
// eagerly-captured null would silently drop completions
|
||||
// forever. MotionDone fires per completed motion (rare),
|
||||
// so the dictionary lookup is negligible.
|
||||
ae.Sequencer.MotionDoneTarget = (m, ok) =>
|
||||
{
|
||||
AcDream.Core.Physics.MotionInterpreter? interp = null;
|
||||
if (mdGuid != 0 && mdGuid == _playerServerGuid)
|
||||
interp = _playerController?.Motion;
|
||||
else if (mdGuid != 0
|
||||
&& _remoteDeadReckon.TryGetValue(mdGuid, out var rmFire))
|
||||
interp = rmFire.Motion;
|
||||
|
||||
interp?.MotionDone(m, ok);
|
||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
System.Console.WriteLine(
|
||||
$"[MOTIONDONE] guid={mdGuid:X8} motion=0x{m:X8} success={ok}");
|
||||
$"[MOTIONDONE] guid={mdGuid:X8} motion=0x{m:X8} success={ok} "
|
||||
+ $"pending={(interp?.MotionsPending() ?? false)}");
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue