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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue