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
|
|
@ -360,6 +360,15 @@ public sealed class PlayerMovementController
|
|||
_weenie.SetSkills(runSkill, jumpSkill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-W2 (r3-port-plan.md §4): the player's <see cref="MotionInterpreter"/>
|
||||
/// — GameWindow binds the player sequencer's MotionDone seam to it so the
|
||||
/// pending_motions queue pops in step with animation completion, same
|
||||
/// path remotes use. R3-W6 widens this into the full local-player
|
||||
/// unification.
|
||||
/// </summary>
|
||||
internal MotionInterpreter Motion => _motion;
|
||||
|
||||
/// <summary>
|
||||
/// Wire the player's AnimationSequencer current cycle velocity into
|
||||
/// <see cref="MotionInterpreter.GetCycleVelocity"/>. When attached,
|
||||
|
|
|
|||
|
|
@ -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)}");
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -450,8 +450,21 @@ public interface IWeenieObject
|
|||
///
|
||||
/// Owns the raw and interpreted motion states for a physics object and
|
||||
/// translates network movement commands into PhysicsBody velocity calls.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>R3-W2 (closes J1, J17):</b> implements <see cref="IMotionDoneSink"/> —
|
||||
/// the entity's <c>MotionTableManager</c> (R2-Q3) binds its animation-done
|
||||
/// callback here, standing in for retail's null-guarded relay chain
|
||||
/// <c>CPhysicsObj::MotionDone</c> 0x0050fdb0 → <c>MovementManager::MotionDone</c>
|
||||
/// 0x005242d0 → <c>CMotionInterp::MotionDone</c> 0x00527ec0 (r3-port-plan.md
|
||||
/// §4). This adds the <c>pending_motions</c> queue (retail
|
||||
/// <c>CMotionInterp::pending_motions</c>, a singly-linked <c>LList</c> —
|
||||
/// ported as a managed <see cref="LinkedList{T}"/> per the AD-34 managed-list
|
||||
/// register wording) that the funnel's dispatch/stop producers populate and
|
||||
/// <see cref="MotionDone"/>/<see cref="HandleExitWorld"/> drain.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class MotionInterpreter
|
||||
public sealed class MotionInterpreter : IMotionDoneSink
|
||||
{
|
||||
// ── animation speed constants (from ACE / confirmed by decompile globals) ─
|
||||
/// <summary>Walk animation base speed. Retail-exact (.rdata 0x007c891c =
|
||||
|
|
@ -540,6 +553,38 @@ public sealed class MotionInterpreter
|
|||
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
||||
public bool StandingLongJump;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W2 — retail <c>CMotionInterp::pending_motions</c> (offset last in
|
||||
/// the struct, a singly-linked <c>LList<MotionNode></c>). Ported as
|
||||
/// a managed <see cref="LinkedList{T}"/> — the AD-34 register wording
|
||||
/// ("retail's intrusive DLList/LList is ported as a managed LinkedList of
|
||||
/// value nodes; node identity semantics are preserved via
|
||||
/// LinkedListNode<T> references rather than raw prev/next
|
||||
/// pointers") applies here exactly as it does to
|
||||
/// <c>MotionTableManager._pendingAnimations</c> — this is the OTHER,
|
||||
/// never-merged queue (movement side, waiting for a MotionDone callback,
|
||||
/// vs the manager's animation side, waiting for a tick countdown; see
|
||||
/// r3-port-plan.md §4 rule 1). Exposed read-only; mutated only through
|
||||
/// <see cref="AddToQueue"/> / <see cref="MotionDone"/> /
|
||||
/// <see cref="HandleExitWorld"/>.
|
||||
/// </summary>
|
||||
private readonly LinkedList<MotionNode> _pendingMotions = new();
|
||||
|
||||
/// <summary>Read-only inspection surface (tests + future callers): the
|
||||
/// pending motion queue in head-to-tail order.</summary>
|
||||
public IEnumerable<MotionNode> PendingMotions => _pendingMotions;
|
||||
|
||||
/// <summary>
|
||||
/// R3-W2 no-op seam standing in for retail <c>CPhysicsObj::unstick_from_object</c>
|
||||
/// (called from <see cref="MotionDone"/>/<see cref="HandleExitWorld"/> when
|
||||
/// the popped queue head is action-class, i.e. <c>Motion & 0x10000000</c>
|
||||
/// is set). Register row: releases a "stuck to object" sticky-manager
|
||||
/// attachment — R5 wires the real StickyManager; until then this is an
|
||||
/// optional callback the App layer may bind, matching the existing
|
||||
/// <c>Action?</c> seam convention (see <c>MotionTableDispatchSink.TurnStopped</c>).
|
||||
/// </summary>
|
||||
public Action? UnstickFromObject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional accessor for the owning entity's current animation cycle
|
||||
/// velocity (AnimationSequencer.CurrentVelocity, i.e. MotionData.Velocity
|
||||
|
|
@ -1356,6 +1401,203 @@ public sealed class MotionInterpreter
|
|||
return true;
|
||||
}
|
||||
|
||||
// ── R3-W2 — pending_motions lifecycle ─────────────────────────────────
|
||||
// docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md §1.
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::add_to_queue</c> (0x00527b80, decomp §1a @305032):
|
||||
/// allocate a <see cref="MotionNode"/> and append it to the tail of
|
||||
/// <see cref="PendingMotions"/> (retail: append at tail; if the queue was
|
||||
/// empty, head and tail both point at the new node — the C#
|
||||
/// <see cref="LinkedList{T}"/> gives this for free via
|
||||
/// <c>AddLast</c>).
|
||||
/// </summary>
|
||||
/// <param name="contextId">Retail <c>arg2</c> — <c>MotionNode.context_id</c>.</param>
|
||||
/// <param name="motion">Retail <c>arg3</c> — <c>MotionNode.motion</c>.</param>
|
||||
/// <param name="jumpErrorCode">Retail <c>arg4</c> — <c>MotionNode.jump_error_code</c>.</param>
|
||||
public void AddToQueue(uint contextId, uint motion, uint jumpErrorCode)
|
||||
{
|
||||
_pendingMotions.AddLast(new MotionNode(contextId, motion, jumpErrorCode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::motions_pending</c> (0x00527fe0, decomp §1b
|
||||
/// @305322): <c>pending_motions.head_ != null</c>.
|
||||
/// </summary>
|
||||
public bool MotionsPending() => _pendingMotions.First is not null;
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::MotionDone</c> (0x00527ec0, decomp §1c @305238,
|
||||
/// FULL BODY). A7 (W0-pins.md): <paramref name="motion"/> and
|
||||
/// <paramref name="success"/> are read into locals by the decompiled
|
||||
/// relay chain but NEVER actually used by this build's body — the queue
|
||||
/// head is popped UNCONDITIONALLY, never matched by motion id. Params
|
||||
/// are kept for R5 signature parity with the real
|
||||
/// <c>MovementManager::MotionDone</c> relay.
|
||||
///
|
||||
/// <para>
|
||||
/// Body (verbatim): no-op if <see cref="PhysicsObj"/> is null or the
|
||||
/// queue is empty. Peek the HEAD: if <c>head.Motion & 0x10000000</c>
|
||||
/// (the action-class bit) is set, fire
|
||||
/// <see cref="UnstickFromObject"/> then pop the head of BOTH
|
||||
/// <see cref="InterpretedState"/>'s and <see cref="RawState"/>'s action
|
||||
/// FIFOs. Then unconditionally dequeue the pending_motions head.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void MotionDone(uint motion, bool success)
|
||||
{
|
||||
if (PhysicsObj is null)
|
||||
return;
|
||||
|
||||
var head = _pendingMotions.First;
|
||||
if (head is null)
|
||||
return;
|
||||
|
||||
if ((head.Value.Motion & 0x10000000u) != 0)
|
||||
{
|
||||
UnstickFromObject?.Invoke();
|
||||
InterpretedState.RemoveAction();
|
||||
RawState.RemoveAction();
|
||||
}
|
||||
|
||||
// Re-peek per retail's structure (head__1 = this->pending_motions.head_,
|
||||
// a fresh read after the action-class branch above) — functionally
|
||||
// the same node since nothing else can mutate the queue mid-call.
|
||||
var head1 = _pendingMotions.First;
|
||||
if (head1 is not null)
|
||||
_pendingMotions.RemoveFirst();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::HandleExitWorld</c> (0x00527f30, decomp §1d):
|
||||
/// identical body to <see cref="MotionDone"/>'s single-pop logic, looped
|
||||
/// until <see cref="PendingMotions"/> drains — world exit flushes all
|
||||
/// pending motion callbacks unconditionally, since no more
|
||||
/// animation-completion events will arrive.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Ambiguity resolved (not in W0-pins.md — found while porting):</b>
|
||||
/// the raw decompile's loop condition is <c>head_ != 0</c>
|
||||
/// (re-read every iteration), but the pop only happens inside a NESTED
|
||||
/// <c>if (physics_obj != 0 && head_ != 0)</c> guard — a literal
|
||||
/// translation would infinite-loop if <see cref="PhysicsObj"/> were null
|
||||
/// with a non-empty queue (the loop variable never advances). This is
|
||||
/// dead code in retail: a live <c>CMotionInterp::HandleExitWorld</c> call
|
||||
/// only ever happens on an interp already bound to a <c>physics_obj</c>
|
||||
/// (retail's <c>CMotionInterp</c> lifetime is physics-object-scoped) —
|
||||
/// the null check exists defensively, not as a real "still drain, but
|
||||
/// skip the pop" branch. Porting a genuine infinite loop is not
|
||||
/// "faithful", it's a bug retail never actually exercises; this port
|
||||
/// drains unconditionally regardless of <see cref="PhysicsObj"/> (same
|
||||
/// choice <see cref="AddToQueue"/>/<see cref="MotionsPending"/> already
|
||||
/// make — they too are called before a physics_obj may exist during
|
||||
/// interp construction/testing).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void HandleExitWorld()
|
||||
{
|
||||
while (_pendingMotions.First is not null)
|
||||
{
|
||||
var head = _pendingMotions.First!;
|
||||
if ((head.Value.Motion & 0x10000000u) != 0)
|
||||
{
|
||||
UnstickFromObject?.Invoke();
|
||||
InterpretedState.RemoveAction();
|
||||
RawState.RemoveAction();
|
||||
}
|
||||
|
||||
_pendingMotions.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::is_standing_still</c> (0x00527fa0, decomp @305309):
|
||||
/// on-ground (Contact + OnWalkable) AND ForwardCommand == Ready AND
|
||||
/// SideStepCommand == 0 AND TurnCommand == 0.
|
||||
/// </summary>
|
||||
public bool IsStandingStill()
|
||||
{
|
||||
if (PhysicsObj is null)
|
||||
return false;
|
||||
|
||||
bool grounded = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
|
||||
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
|
||||
if (!grounded)
|
||||
return false;
|
||||
|
||||
return InterpretedState.ForwardCommand == MotionCommand.Ready
|
||||
&& InterpretedState.SideStepCommand == 0
|
||||
&& InterpretedState.TurnCommand == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>CMotionInterp::motion_allows_jump</c> (0x005279e0, decomp §3a
|
||||
/// @304908, <c>__pure</c>). A1 (W0-pins.md, adversarially verified):
|
||||
/// PINNED as a BLOCKLIST — <c>0</c> = jump allowed (pass), <c>0x48</c> =
|
||||
/// jump BLOCKED. Ported as literal uint range comparisons mirroring the
|
||||
/// decomp's exact branch algebra (NOT enum-ordinal ranges — the whole
|
||||
/// point of A1 is that ACE's enum-order dependence is fragile).
|
||||
///
|
||||
/// <para>
|
||||
/// Blocklist (definitive table, W0-pins.md §A1):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>[0x1000006f, 0x10000078]</c> — MagicPowerUp01..MagicPowerUp10.</item>
|
||||
/// <item><c>[0x10000128, 0x10000131]</c> — TripleThrustLow..MagicPowerUp07Purple.</item>
|
||||
/// <item><c>0x40000008</c> exact — Fallen (NOT Falling; ACE mis-transcribed this).</item>
|
||||
/// <item><c>[0x40000016, 0x40000018]</c> — Reload, Unload, Pickup.</item>
|
||||
/// <item><c>[0x4000001e, 0x40000039]</c> — AimLevel..MagicPray.</item>
|
||||
/// <item><c>[0x41000012, 0x41000014]</c> — Crouch, Sitting, Sleeping.</item>
|
||||
/// </list>
|
||||
/// Everything else — including Falling <c>0x40000015</c>, Ready
|
||||
/// <c>0x41000003</c>, Dead <c>0x40000011</c>, all turn/sidestep ids — PASSES.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This is a pure function (no instance state) — retail's <c>this</c>
|
||||
/// parameter is unused in the body (the decomp's <c>__pure</c>
|
||||
/// attribute confirms it doesn't read <c>this</c>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="motion">Retail <c>arg2</c> — the motion id to test.</param>
|
||||
public static WeenieError MotionAllowsJump(uint motion)
|
||||
{
|
||||
// Verbatim branch algebra from raw 304908-304931 (W0-pins.md §A1):
|
||||
// if (arg2 > 0x40000018) {
|
||||
// if (arg2 > 0x41000014) return 0;
|
||||
// if (arg2 < 0x41000012 && (arg2 < 0x4000001e || arg2 > 0x40000039)) return 0;
|
||||
// } else if (arg2 < 0x40000016) {
|
||||
// if (arg2 > 0x10000131) {
|
||||
// if (arg2 != 0x40000008) return 0;
|
||||
// } else if (arg2 < 0x10000128 && (arg2 < 0x1000006f || arg2 > 0x10000078)) return 0;
|
||||
// }
|
||||
// return 0x48;
|
||||
//
|
||||
// The middle band [0x40000016, 0x40000018] satisfies NEITHER outer
|
||||
// branch (not > 0x40000018, not < 0x40000016) and falls straight to
|
||||
// `return 0x48` — a genuine "no early return" gap in the middle.
|
||||
if (motion > 0x40000018u)
|
||||
{
|
||||
if (motion > 0x41000014u)
|
||||
return WeenieError.None;
|
||||
if (motion < 0x41000012u && (motion < 0x4000001eu || motion > 0x40000039u))
|
||||
return WeenieError.None;
|
||||
}
|
||||
else if (motion < 0x40000016u)
|
||||
{
|
||||
if (motion > 0x10000131u)
|
||||
{
|
||||
if (motion != 0x40000008u)
|
||||
return WeenieError.None;
|
||||
}
|
||||
else if (motion < 0x10000128u && (motion < 0x1000006fu || motion > 0x10000078u))
|
||||
{
|
||||
return WeenieError.None;
|
||||
}
|
||||
}
|
||||
|
||||
return WeenieError.YouCantJumpFromThisPosition;
|
||||
}
|
||||
|
||||
// ── FUN_00529710 — LeaveGround ────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1560,6 +1802,23 @@ public sealed class MotionInterpreter
|
|||
/// then dispatch style / forward-or-Falling / sidestep-or-stop /
|
||||
/// turn-or-stop in retail order. A non-zero turn EARLY-RETURNS (no
|
||||
/// turn-stop, no idle bookkeeping).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>R3-W2 producer (closes J1's apply_interpreted_movement leg):</b>
|
||||
/// the tail turn-stop call (raw 305766-305785) is retail's
|
||||
/// <c>CPhysicsObj::StopInterpretedMotion(physics_obj, 0x6500000d, &var_2c)</c>
|
||||
/// — our <see cref="IInterpretedMotionSink.StopMotion"/> call below IS
|
||||
/// that dispatch. On success (<c>eax_10 == 0</c>, i.e. the sink accepted
|
||||
/// the stop — this port has no failure signal from
|
||||
/// <see cref="IInterpretedMotionSink.StopMotion"/>, so it is treated as
|
||||
/// always-succeeding, matching every real sink implementation which is
|
||||
/// void), retail immediately re-queues the canonical "return to none"
|
||||
/// node: <c>add_to_queue(this, var_c /*uninitialized local in the raw
|
||||
/// decompile — see the final-report contextId note*/, 0x41000003, eax_10)</c>
|
||||
/// where <c>eax_10</c> (the stop's own return value) becomes the queued
|
||||
/// node's <c>jump_error_code</c> — since the stop succeeded, that value
|
||||
/// is <c>0</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void ApplyInterpretedMovement(uint currentStyle, IInterpretedMotionSink? sink)
|
||||
{
|
||||
|
|
@ -1596,9 +1855,13 @@ public sealed class MotionInterpreter
|
|||
InterpretedState.TurnCommand, InterpretedState.TurnSpeed, sink);
|
||||
return; // retail early return — no idle-stop this call
|
||||
}
|
||||
|
||||
sink?.StopMotion(MotionCommand.TurnRight);
|
||||
// Idle add_to_queue(Ready) bookkeeping lands with S3
|
||||
// (pending_motions / MotionDone).
|
||||
// add_to_queue(context, Ready, 0) — R3-W2 (closes J1). TODO(W5):
|
||||
// real ContextId once MovementParameters flows through this funnel
|
||||
// (retail's own local here is READ UNINITIALIZED in the decompile —
|
||||
// see the final-report contextId note).
|
||||
AddToQueue(contextId: 0, MotionCommand.Ready, jumpErrorCode: 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1610,6 +1873,35 @@ public sealed class MotionInterpreter
|
|||
/// NO cycle change — this is retail's real mechanism behind "airborne
|
||||
/// remotes keep their Falling cycle"). Blocked action-class motions
|
||||
/// (0x10000000 bit) are rejected outright (0x24).
|
||||
///
|
||||
/// <para>
|
||||
/// <b>R3-W2 producer (closes J1's DoInterpretedMotion leg):</b> on the
|
||||
/// success path (raw 305591-305610), retail computes the queue node's
|
||||
/// <c>jump_error_code</c> via a DOUBLE-CHECK shape before calling
|
||||
/// <c>add_to_queue(context_id, arg2, eax_5)</c>:
|
||||
/// <code>
|
||||
/// if ((params->bitfield & 0x20000) == 0) { // NOT disable_jump_during_link
|
||||
/// eax_5 = motion_allows_jump(arg2);
|
||||
/// if (eax_5 == 0 && (arg2 & 0x10000000) == 0) // passed AND not action-class
|
||||
/// eax_5 = motion_allows_jump(interpreted_state.forward_command);
|
||||
/// } else {
|
||||
/// eax_5 = 0x48; // DisableJumpDuringLink forces BLOCKED
|
||||
/// }
|
||||
/// add_to_queue(context_id, arg2, eax_5);
|
||||
/// </code>
|
||||
/// This funnel has no <c>MovementParameters</c> threaded through yet
|
||||
/// (<see cref="DispatchInterpretedMotion"/> takes only <c>motion</c>/
|
||||
/// <c>speed</c> — the params-carrying overloads land in W5 when
|
||||
/// <c>DoMotion</c>/<c>StopMotion</c>/the legacy overloads merge into
|
||||
/// this backend). TODO(W5): thread the real <c>MovementParameters</c>
|
||||
/// through and restore the <c>0x20000</c> (DisableJumpDuringLink) leg;
|
||||
/// until then this always takes the "bit not set" branch (the common
|
||||
/// case — retail's own ctor default for the bit is CLEAR, W0-pins A4).
|
||||
/// <c>ContextId</c> is <c>0</c> pending the same W5 plumbing (no
|
||||
/// <c>MovementParameters.ContextId</c> reaches this call site yet) —
|
||||
/// see the final-report contextId note for the evidence this funnel
|
||||
/// genuinely has nothing else to pass.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public WeenieError DispatchInterpretedMotion(uint motion, float speed, IInterpretedMotionSink? sink)
|
||||
{
|
||||
|
|
@ -1620,7 +1912,15 @@ public sealed class MotionInterpreter
|
|||
// 0x40000011 (Dead) flushes queued link animations in retail
|
||||
// (RemoveLinkAnimations) — S4 wires the sequencer hook.
|
||||
sink?.ApplyMotion(motion, speed);
|
||||
// add_to_queue(context, motion, jumpAllowed) — S3 bookkeeping.
|
||||
|
||||
// add_to_queue(context, motion, jumpAllowed) — R3-W2 (closes J1).
|
||||
// TODO(W5): params-bit 0x20000 (DisableJumpDuringLink) leg + real
|
||||
// ContextId once MovementParameters flows through this funnel.
|
||||
WeenieError jumpErr = MotionAllowsJump(motion);
|
||||
if (jumpErr == WeenieError.None && (motion & 0x10000000u) == 0)
|
||||
jumpErr = MotionAllowsJump(InterpretedState.ForwardCommand);
|
||||
AddToQueue(contextId: 0, motion, (uint)jumpErr);
|
||||
|
||||
return WeenieError.None;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue