fix(runtime): align portal and movement presentation
Port retail portal viewport projection and reveal behavior, preserve outbound combat style, drive remote and local grounded movement from authored CSequence root frames, and reuse the local prepared pose so animation hooks advance once. User-verified portal, observer movement, combat stance, and short-tap locomotion gates. Release build passed with 5,767 tests and five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
e95f55f25b
commit
124e046976
30 changed files with 1362 additions and 348 deletions
|
|
@ -29,9 +29,11 @@ public interface IAnimationLoader
|
|||
// public member's signature (the API-migration table in the gap map is the
|
||||
// contract) and re-expresses each one over the core. Invented mechanisms that
|
||||
// were R2/R3 scope (K-fix18 skipTransitionLink DELETED in R3-W4, Fix B locomotion
|
||||
// link-skip, stop-anim fallback, GetLink's reversed branch, velocity/omega
|
||||
// synthesis constants) SURVIVE unchanged at the adapter level per the gap
|
||||
// map's "Invented behaviors NOT in the gap list" note.
|
||||
// link-skip, stop-anim fallback, GetLink's reversed branch, turn-omega
|
||||
// synthesis) survive at the adapter level per the gap map's
|
||||
// "Invented behaviors NOT in the gap list" note. Locomotion-velocity
|
||||
// synthesis was retired when remote physics began consuming the literal
|
||||
// CSequence root-motion Frame (issue #41, 2026-07-17).
|
||||
//
|
||||
// Primary references (docs/research/2026-07-02-r1-csequence/r1-csequence-decomp.md):
|
||||
// append_animation 0x00525510 (§24)
|
||||
|
|
@ -380,10 +382,9 @@ public sealed class AnimationSequencer
|
|||
if (style != 0 && style != _state.Style)
|
||||
_manager.PerformMovement(MotionTableMovement.Interpreted(style, 1f));
|
||||
|
||||
// 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).
|
||||
// Motion via the PerformMovement passthrough. CMotionTable/add_motion
|
||||
// owns the literal sequence velocity/omega writes; command-derived
|
||||
// body speed is not copied into CSequence.
|
||||
uint dispatchResult = PerformMovement(
|
||||
MotionTableMovement.Interpreted(adjustedMotion, adjustedSpeed));
|
||||
|
||||
|
|
@ -394,16 +395,15 @@ public sealed class AnimationSequencer
|
|||
// RemoveAllLinkAnimations) when the body leaves the ground.
|
||||
|
||||
// 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.)
|
||||
// retail leaves sequence AND state untouched.
|
||||
if (dispatchResult != MotionTableManagerError.Success)
|
||||
return;
|
||||
|
||||
// ── Synthesize CurrentOmega for turn cycles ───────────────────────
|
||||
// Same story as velocity synthesis above: Humanoid turn MotionData
|
||||
// often ships without HasOmega. Retail clients turn the body via
|
||||
// the baked omega, but if the dat is silent we fall back to the
|
||||
// retail turn-rate constant. Decompile references:
|
||||
// Humanoid turn MotionData often ships without HasOmega. Until the
|
||||
// remaining R6 rotation path consumes CSequence's complete Frame
|
||||
// orientation, the remote ObservedOmega seam needs the retail
|
||||
// turn-rate fallback. Decompile references:
|
||||
// FUN_00529210 apply_current_movement (writes Omega)
|
||||
// chunk_00520000.c TurnRate globals (~π/2 rad/s for speed=1)
|
||||
// The ACE port uses `omega.z = ±(π/2) × turnSpeed` for right/left
|
||||
|
|
@ -455,72 +455,18 @@ public sealed class AnimationSequencer
|
|||
|
||||
/// <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
|
||||
/// RemoteMotionCombiner.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.
|
||||
/// <see cref="MotionTableManager.PerformMovement"/>. The resulting
|
||||
/// <see cref="CurrentVelocity"/> remains the literal retail
|
||||
/// <c>MotionData.Velocity * speed</c> written by <c>add_motion</c>;
|
||||
/// command-derived body speed belongs to
|
||||
/// <c>CMotionInterp::get_state_velocity</c>, not to CSequence.
|
||||
/// </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;
|
||||
return _manager.PerformMovement(movement);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
private const float WalkAnimSpeed = 3.12f;
|
||||
private const float RunAnimSpeed = 4.0f;
|
||||
private const float SidestepAnimSpeed = 1.25f;
|
||||
|
||||
// R2-Q4: MultiplyCyclicFramerate DELETED (zero external callers). The
|
||||
// gap-map-G13 composite stand-in (framerate scale + velocity/omega
|
||||
// rescale) is retired — same-motion re-speeds now route through the
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@ using System.Numerics;
|
|||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame combiner for remote-entity motion: animation root motion
|
||||
/// + InterpolationManager catch-up correction. Pure function — no
|
||||
/// side effects, no hidden state.
|
||||
/// Per-frame combiner for remote-entity motion: the root-motion delta emitted
|
||||
/// by <c>CSequence::update</c> + InterpolationManager catch-up correction.
|
||||
/// Pure function — no side effects or hidden state.
|
||||
///
|
||||
/// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730):
|
||||
/// rootOffset = CPartArray::Update(dt) // animation
|
||||
/// PositionManager::adjust_offset(rootOffset) // adds correction
|
||||
/// frame.origin += rootOffset
|
||||
///
|
||||
/// In acdream the animation root motion is sourced from
|
||||
/// AnimationSequencer.CurrentVelocity (body-local velocity from the
|
||||
/// active locomotion cycle). We rotate that by the body's orientation
|
||||
/// to get a world-space delta, then add the InterpolationManager's
|
||||
/// world-space correction.
|
||||
/// The animation root motion is the complete body-local <c>Frame.Origin</c>
|
||||
/// accumulated by <c>CPartArray::Update</c>: authored PosFrames plus the
|
||||
/// literal sequence velocity. We rotate that delta by the body's orientation
|
||||
/// to get world space. It must not be reconstructed from the interpreted
|
||||
/// command; the retail Humanoid table carries zero sequence velocity for Walk
|
||||
/// and Run, and retail moves observed characters through the interpolation
|
||||
/// queue instead.
|
||||
///
|
||||
/// <para><b>Renamed R5</b> (was <c>PositionManager</c>): this class is only the
|
||||
/// InterpolationManager-composition portion of retail's
|
||||
|
|
@ -33,13 +35,12 @@ public sealed class RemoteMotionCombiner
|
|||
/// </summary>
|
||||
/// <param name="dt">Per-frame delta time, seconds.</param>
|
||||
/// <param name="currentBodyPosition">Body's current world-space position.</param>
|
||||
/// <param name="seqVel">
|
||||
/// Body-local velocity from the active animation cycle
|
||||
/// (from <c>AnimationSequencer.CurrentVelocity</c>); pass
|
||||
/// <c>Vector3.Zero</c> if the entity has no sequencer or is on a
|
||||
/// non-locomotion cycle.
|
||||
/// <param name="rootMotionLocalDelta">
|
||||
/// Complete body-local displacement accumulated by this frame's
|
||||
/// <c>CSequence::update</c>. This is already a delta for the current
|
||||
/// quantum, not a velocity to multiply by <paramref name="dt"/>.
|
||||
/// </param>
|
||||
/// <param name="ori">Body orientation; used to rotate seqVel from body-local to world.</param>
|
||||
/// <param name="ori">Body orientation; used to rotate root motion from body-local to world.</param>
|
||||
/// <param name="interp">The remote's InterpolationManager (for AdjustOffset call).</param>
|
||||
/// <param name="maxSpeed">From <c>MotionInterpreter.GetMaxSpeed()</c> — passed to AdjustOffset for the catch-up clamp.</param>
|
||||
/// <param name="terrainNormal">
|
||||
|
|
@ -59,7 +60,7 @@ public sealed class RemoteMotionCombiner
|
|||
public Vector3 ComputeOffset(
|
||||
double dt,
|
||||
Vector3 currentBodyPosition,
|
||||
Vector3 seqVel,
|
||||
Vector3 rootMotionLocalDelta,
|
||||
Quaternion ori,
|
||||
InterpolationManager interp,
|
||||
float maxSpeed,
|
||||
|
|
@ -93,8 +94,7 @@ public sealed class RemoteMotionCombiner
|
|||
if (correction.LengthSquared() > 0f)
|
||||
return correction;
|
||||
|
||||
Vector3 rootMotionLocal = seqVel * (float)dt;
|
||||
Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocal, ori);
|
||||
Vector3 rootMotionWorld = Vector3.Transform(rootMotionLocalDelta, ori);
|
||||
|
||||
// Slope projection (queue-empty fallback only). Locomotion cycles
|
||||
// bake Z=0 in body-local, so without projection the body's Z stays
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue