The funnel's retail-ordered dispatches now go directly into the entity's motion-table stack via Motion/MotionTableDispatchSink (Core): ApplyMotion → PerformMovement(InterpretedCommand), StopMotion → PerformMovement(StopInterpretedCommand). No axis collection, no single-cycle priority pick, no Commit pass, no HasCycle probe, no Run→Walk→Ready fallback chain — GetObjectSequence 0x00522860 + is_allowed decide (closes H4, H5-callers, H11-callers, H15, H17-carry). Run-while-turning now blends for real: the turn is a Branch-4 modifier combined over the untouched run substate (AP-73 DELETED from the register). AnimationSequencer gains the public PerformMovement passthrough (lazy initialize_state + locomotion velocity synthesis on success — AP-75; remote body translation via PositionManager.ComputeOffset depends on CurrentVelocity) and InitializeState(); HasCycle DELETED (the miss hazard is structurally gone — GetObjectSequence checks the cycle before any surgery; new conformance test pins sequence+state untouched on a miss). GameWindow: sink swap with the TurnApplied/TurnStopped ObservedOmega callbacks (H17 carried verbatim — register AP-76, retire R6); spawn + door sites run retail's enter-world order (initialize_state installs the table default, the wire's initial motion dispatches unguarded — the L.1c fallback chains deleted); despawn drains the pending queue via HandleExitWorld (MotionDone success:false per entry, 0x0051bda0). 5 new MotionTableDispatchSink conformance tests (lazy init, Branch-4 turn blend + callback, Case-B stop unwind, Case-A stop re-drive, miss no-op). Full suite green: 3,462 passed. Live smoke vs ACE: in-world, NPC emote/Ready UMs dispatching through the new path, [MOTIONDONE] completions firing across entities — first live proof of the Q3→Q4→Q5 chain. Zero exceptions. Registers: AP-73 deleted; AP-76 added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
70 lines
2.7 KiB
C#
70 lines
2.7 KiB
C#
using System;
|
|
|
|
namespace AcDream.Core.Physics.Motion;
|
|
|
|
/// <summary>
|
|
/// R2-Q5 — the <see cref="IInterpretedMotionSink"/> that dispatches the
|
|
/// CMotionInterp funnel's retail-ordered motion events STRAIGHT into the
|
|
/// entity's motion-table stack via
|
|
/// <see cref="AnimationSequencer.PerformMovement"/> (lazy initialize_state +
|
|
/// <c>MotionTableManager::PerformMovement</c> 0x0051c0b0).
|
|
///
|
|
/// <para>
|
|
/// This replaces the App-side <c>RemoteMotionSink</c> (deleted): no axis
|
|
/// collection, no single-cycle priority pick, no Commit pass, no HasCycle
|
|
/// probe, no Run→Walk→Ready missing-cycle chain — retail's
|
|
/// <c>GetObjectSequence</c> (0x00522860) + <c>is_allowed</c> decide what
|
|
/// plays. Forward locomotion installs the substate cycle (Branch 2);
|
|
/// sidesteps resolve by the dat (cycle when authored, else modifier);
|
|
/// turns are Branch-4 physics-only modifiers blended over the substate via
|
|
/// <c>combine_motion</c>/<c>re_modify</c> — the composition the retired
|
|
/// AP-73 row approximated with one cycle.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The <see cref="TurnApplied"/>/<see cref="TurnStopped"/> callbacks are the
|
|
/// interim ObservedOmega seam: the App seeds the remote body's angular
|
|
/// velocity from the wire turn so rotation starts the same tick (retail
|
|
/// rotates the body from the sequence omega inside the per-tick
|
|
/// <c>apply_physics</c> chain — R6 scope; register row). They fire BEFORE
|
|
/// the dispatch so a consumer sees the seed even if the dat lacks the
|
|
/// modifier entry.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class MotionTableDispatchSink : IInterpretedMotionSink
|
|
{
|
|
private readonly AnimationSequencer _sequencer;
|
|
|
|
/// <summary>Turn-class dispatch observed: (motion, signedSpeed) —
|
|
/// TurnLeft arrives either as the explicit 0x0E command or as
|
|
/// TurnRight + negative speed (adjust_motion wire convention).</summary>
|
|
public Action<uint, float>? TurnApplied { get; set; }
|
|
|
|
/// <summary>Turn-class stop observed.</summary>
|
|
public Action? TurnStopped { get; set; }
|
|
|
|
public MotionTableDispatchSink(AnimationSequencer sequencer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(sequencer);
|
|
_sequencer = sequencer;
|
|
}
|
|
|
|
private static bool IsTurn(uint motion)
|
|
=> (motion & 0xFF000000u) == 0x65000000u && (motion & 0xFFu) is 0x0D or 0x0E;
|
|
|
|
public void ApplyMotion(uint motion, float speed)
|
|
{
|
|
if (IsTurn(motion))
|
|
TurnApplied?.Invoke(motion, speed);
|
|
|
|
_sequencer.PerformMovement(MotionTableMovement.Interpreted(motion, speed));
|
|
}
|
|
|
|
public void StopMotion(uint motion)
|
|
{
|
|
if (IsTurn(motion))
|
|
TurnStopped?.Invoke();
|
|
|
|
_sequencer.PerformMovement(MotionTableMovement.StopInterpreted(motion, 1f));
|
|
}
|
|
}
|