feat(R2-Q5): RemoteMotionSink DELETED — funnel dispatches straight into PerformMovement; AP-73 retired
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>
This commit is contained in:
parent
c072b73686
commit
d82f07d4e5
8 changed files with 399 additions and 395 deletions
|
|
@ -316,31 +316,12 @@ public sealed class AnimationSequencer
|
|||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the underlying MotionTable contains a cycle for the
|
||||
/// given (style, motion) pair. Useful for callers that want to fall
|
||||
/// back to a known-good motion (e.g. <c>WalkForward</c> →
|
||||
/// <c>Ready</c>) instead of triggering <see cref="SetCycle"/>'s
|
||||
/// unconditional cyclic-tail rebuild on a missing cycle — which leaves
|
||||
/// the body without any animation tail and snaps every part to the
|
||||
/// setup-default offset (visible as "torso on the ground" since most
|
||||
/// creatures' setup-default has limbs at the torso origin).
|
||||
/// </summary>
|
||||
public bool HasCycle(uint style, uint motion)
|
||||
{
|
||||
// adjust_motion remapping (mirrors the head of SetCycle):
|
||||
// TurnLeft, SideStepLeft, WalkBackward map to their right/forward
|
||||
// mirror cycles.
|
||||
uint adjustedMotion = motion;
|
||||
switch (motion & 0xFFFFu)
|
||||
{
|
||||
case 0x000E: adjustedMotion = (motion & 0xFFFF0000u) | 0x000Du; break;
|
||||
case 0x0010: adjustedMotion = (motion & 0xFFFF0000u) | 0x000Fu; break;
|
||||
case 0x0006: adjustedMotion = (motion & 0xFFFF0000u) | 0x0005u; break;
|
||||
}
|
||||
int cycleKey = (int)(((style & 0xFFFFu) << 16) | (adjustedMotion & 0xFFFFFFu));
|
||||
return _mtable.Cycles.ContainsKey(cycleKey);
|
||||
}
|
||||
// R2-Q5: HasCycle DELETED (caller-free). The missing-cycle hazard it
|
||||
// guarded ("torso on the ground": legacy SetCycle's unconditional
|
||||
// cyclic-tail drop) is structurally gone — the verbatim
|
||||
// GetObjectSequence checks the cycle BEFORE any list surgery and leaves
|
||||
// the sequence untouched on a miss, so the spawn/UM fallback chains
|
||||
// (Run→Walk→Ready) that probed it are retired with RemoteMotionSink.
|
||||
|
||||
/// <summary>
|
||||
/// Switch to a new cyclic motion, prepending any transition link frames
|
||||
|
|
@ -406,7 +387,11 @@ public sealed class AnimationSequencer
|
|||
if (style != 0 && style != _state.Style)
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(style, 1f));
|
||||
|
||||
uint dispatchResult = _manager.PerformMovement(
|
||||
// Motion via the PerformMovement passthrough (velocity synthesis on
|
||||
// success — same helper the R2-Q5 funnel sink path uses; the
|
||||
// adjusted motion + adjusted speed produce the identical synthesis
|
||||
// result as the original pair).
|
||||
uint dispatchResult = PerformMovement(
|
||||
MotionTableMovement.Interpreted(adjustedMotion, adjustedSpeed));
|
||||
|
||||
// K-fix18 (2026-04-26, register row survives → R3): instant-engage
|
||||
|
|
@ -423,74 +408,10 @@ public sealed class AnimationSequencer
|
|||
|
||||
// Failed dispatch (missing cycle for this style, is_allowed reject):
|
||||
// retail leaves sequence AND state untouched — no synthesis either.
|
||||
// (Velocity synthesis already ran inside PerformMovement on success.)
|
||||
if (dispatchResult != MotionTableManagerError.Success)
|
||||
return;
|
||||
|
||||
// ── Synthesize CurrentVelocity for locomotion cycles ──────────────
|
||||
// The Humanoid motion table ships every locomotion MotionData with
|
||||
// Flags=0x00 (no HasVelocity), so EnqueueMotionData leaves
|
||||
// CurrentVelocity at Vector3.Zero. That matches the literal retail
|
||||
// dat, but retail's body physics uses CMotionInterp::get_state_velocity
|
||||
// (FUN_00528960) which returns RunAnimSpeed × ForwardSpeed for
|
||||
// RunForward, independent of the dat's HasVelocity flag. The dat
|
||||
// velocity is a separate additive source (kick-off velocity, flying
|
||||
// creatures, etc) not the primary locomotion drive.
|
||||
//
|
||||
// For our sequencer's <see cref="CurrentVelocity"/> to be usable by
|
||||
// consumers (local-player get_state_velocity via Option B, remote
|
||||
// dead-reckoning in GameWindow) it must carry the retail-constant
|
||||
// locomotion value when the dat is silent. Synthesize it here,
|
||||
// post-EnqueueMotionData, only when the cycle is a locomotion cycle
|
||||
// AND the dat didn't populate it.
|
||||
//
|
||||
// Constants match <see cref="MotionInterpreter.RunAnimSpeed"/> etc —
|
||||
// decompiled from _DAT_007c96e0/e4/e8. The velocity is body-local
|
||||
// (+Y = forward, +X = right); consumers rotate into world space via
|
||||
// the owning entity's orientation.
|
||||
// For known locomotion cycles, ALWAYS overwrite CurrentVelocity with
|
||||
// the synthesized value — even if the transition link set
|
||||
// CurrentVelocity from its own HasVelocity flag. The link's velocity
|
||||
// is for the brief transition (e.g. small stride into run-pose); the
|
||||
// cycle's intended steady-state velocity is what consumers (remote
|
||||
// body translation in GameWindow.TickAnimations env-var path) need.
|
||||
// Without this, walking-to-running transitions left CurrentVelocity
|
||||
// at the link's slow pace, and the user reported "it just blips
|
||||
// forward walking" until another motion command (turn, etc) forced
|
||||
// a re-synth.
|
||||
{
|
||||
float yvel = 0f;
|
||||
float xvel = 0f;
|
||||
uint low = motion & 0xFFu;
|
||||
bool isLocomotion = false;
|
||||
switch (low)
|
||||
{
|
||||
case 0x05: // WalkForward
|
||||
yvel = WalkAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x06: // WalkBackward — adjust_motion remapped to WalkForward
|
||||
// with speedMod *= -0.65f.
|
||||
yvel = WalkAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x07: // RunForward
|
||||
yvel = RunAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x0F: // SideStepRight
|
||||
xvel = SidestepAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x10: // SideStepLeft — remapped to SideStepRight with
|
||||
// negated speed; same handling as backward walk.
|
||||
xvel = SidestepAnimSpeed * adjustedSpeed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
}
|
||||
if (isLocomotion)
|
||||
_core.SetVelocity(new Vector3(xvel, yvel, 0f));
|
||||
}
|
||||
|
||||
// ── Synthesize CurrentOmega for turn cycles ───────────────────────
|
||||
// Same story as velocity synthesis above: Humanoid turn MotionData
|
||||
// often ships without HasOmega. Retail clients turn the body via
|
||||
|
|
@ -525,6 +446,75 @@ public sealed class AnimationSequencer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q5: run retail's <c>enter_default_state</c> analog now if it
|
||||
/// hasn't run yet (idempotent). Spawn paths call this so an entity with
|
||||
/// no initial wire motion still plays the table default (retail: every
|
||||
/// CPhysicsObj entering the world runs initialize_state).
|
||||
/// </summary>
|
||||
public void InitializeState() => EnsureInitialized();
|
||||
|
||||
/// <summary>
|
||||
/// R2-Q5: the single dispatch entry — lazy initialize_state, then
|
||||
/// <see cref="MotionTableManager.PerformMovement"/>, then (on a
|
||||
/// successful InterpretedCommand) the locomotion velocity synthesis
|
||||
/// (register AP-75; the consumers are remote body translation via
|
||||
/// PositionManager.ComputeOffset and the local Option-B
|
||||
/// get_state_velocity — retire in R6 when root motion drives the body).
|
||||
/// Omega is deliberately NOT synthesized here: remote rotation is the
|
||||
/// ObservedOmega seam (MotionTableDispatchSink callbacks, retire R6);
|
||||
/// only the SetCycle path keeps the turn-omega fallback.
|
||||
/// </summary>
|
||||
public uint PerformMovement(MotionTableMovement movement)
|
||||
{
|
||||
EnsureInitialized();
|
||||
uint result = _manager.PerformMovement(movement);
|
||||
if (result == MotionTableManagerError.Success
|
||||
&& movement.Type == MovementType.InterpretedCommand)
|
||||
{
|
||||
SynthesizeLocomotionVelocity(movement.Motion, movement.Speed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite sequence velocity with the retail locomotion constant when
|
||||
/// the dispatched motion is a locomotion cycle. The Humanoid motion
|
||||
/// table ships every locomotion MotionData with a zero Velocity, so
|
||||
/// <c>add_motion</c>'s unconditional set leaves the sequence at zero —
|
||||
/// but retail's body physics uses <c>CMotionInterp::get_state_velocity</c>
|
||||
/// (0x00528960: RunAnimSpeed × ForwardSpeed etc., independent of the
|
||||
/// dat), so consumers of <see cref="CurrentVelocity"/> need the
|
||||
/// constant here. Velocity is body-local (+Y forward, +X right);
|
||||
/// consumers rotate into world space via the entity orientation.
|
||||
/// Constants decompiled from _DAT_007c96e0/e4/e8.
|
||||
/// </summary>
|
||||
private void SynthesizeLocomotionVelocity(uint motion, float speed)
|
||||
{
|
||||
float yvel = 0f;
|
||||
float xvel = 0f;
|
||||
bool isLocomotion = false;
|
||||
switch (motion & 0xFFu)
|
||||
{
|
||||
case 0x05: // WalkForward
|
||||
case 0x06: // WalkBackward (pre-adjust callers; adjusted = 0x05)
|
||||
yvel = WalkAnimSpeed * speed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x07: // RunForward
|
||||
yvel = RunAnimSpeed * speed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
case 0x0F: // SideStepRight
|
||||
case 0x10: // SideStepLeft (pre-adjust callers; adjusted = 0x0F)
|
||||
xvel = SidestepAnimSpeed * speed;
|
||||
isLocomotion = true;
|
||||
break;
|
||||
}
|
||||
if (isLocomotion)
|
||||
_core.SetVelocity(new Vector3(xvel, yvel, 0f));
|
||||
}
|
||||
|
||||
// Retail locomotion constants — mirror of MotionInterpreter.RunAnimSpeed
|
||||
// etc. Kept here to keep AnimationSequencer self-contained for the
|
||||
// synthesize-velocity path above. Values decompiled from _DAT_007c96e0/e4/e8.
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ public interface IInterpretedMotionSink
|
|||
/// The <c>CPhysicsObj::DoInterpretedMotion → … →
|
||||
/// CMotionTable::GetObjectSequence</c> backend — called for every motion
|
||||
/// that passes <c>contact_allows_move</c>, in retail dispatch order
|
||||
/// (style → forward-or-Falling → sidestep → turn → actions). The App
|
||||
/// implementation decides how the axes map onto the visible
|
||||
/// <c>AnimationSequencer</c> cycle (retaining the HasCycle fallback,
|
||||
/// overlay routing, etc.).
|
||||
/// (style → forward-or-Falling → sidestep → turn → actions). R2-Q5: the
|
||||
/// production implementation is <c>Motion.MotionTableDispatchSink</c>,
|
||||
/// which dispatches straight into the entity's motion-table stack
|
||||
/// (<c>PerformMovement</c> → GetObjectSequence + is_allowed decide) —
|
||||
/// no sink-side axis pick or fallback chain.
|
||||
/// </summary>
|
||||
void ApplyMotion(uint motion, float speed);
|
||||
|
||||
|
|
|
|||
70
src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs
Normal file
70
src/AcDream.Core/Physics/Motion/MotionTableDispatchSink.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue