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
|
|
@ -298,10 +298,10 @@ public sealed class AnimationSequencerCutoverTraceTests
|
|||
// POST-adjust_motion substate (45000005, was 45000006) and
|
||||
// CurrentSpeedMod the signed mod (-0.65, was 1.00) - MotionState owns
|
||||
// the state and retail's interpreted state is post-adjustment. The
|
||||
// om=(-0.00,...) is IEEE negative zero from add_motion's
|
||||
// zero-omega x negative-speed multiply (numerically equal to 0).
|
||||
// vel/om=(-0.00,...) are IEEE negative zero from add_motion's
|
||||
// zero-components x negative-speed multiply (numerically equal to 0).
|
||||
Assert.Equal(
|
||||
"105@-19.5^,101@-19.5* | frame=3.0 vel=(0.00,-2.03,0.00) om=(-0.00,-0.00,-0.00) style=8000003D motion=45000005 mod=-0.65",
|
||||
"105@-19.5^,101@-19.5* | frame=3.0 vel=(-0.00,-2.03,-0.00) om=(-0.00,-0.00,-0.00) style=8000003D motion=45000005 mod=-0.65",
|
||||
Describe(seq, loader));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1220,6 +1220,29 @@ public sealed class AnimationSequencerTests
|
|||
Assert.Equal(new Vector3(0f, 4f, 0f), seq.CurrentVelocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentVelocity_ZeroLocomotionMotionData_IsNotSynthesizedFromCommand()
|
||||
{
|
||||
// Retail add_motion @ 0x005224B0 writes MotionData.Velocity * speed
|
||||
// unconditionally. It never substitutes CMotionInterp's body-speed
|
||||
// constants into CSequence. The installed Humanoid table uses this
|
||||
// zero-velocity shape for both Walk and Run.
|
||||
const uint Style = 0x8000003Du;
|
||||
const uint Motion = 0x40000007u;
|
||||
const uint AnimId = 0x03000406u;
|
||||
|
||||
var anim = Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity);
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = Fixtures.MakeMtable(Style, Motion, AnimId, framerate: 10f);
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(AnimId, anim);
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, Motion, speedMod: 2f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, seq.CurrentVelocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentVelocity_ScaledBySpeedMod()
|
||||
{
|
||||
|
|
@ -1454,8 +1477,9 @@ public sealed class AnimationSequencerTests
|
|||
{
|
||||
// A RunForward motion with MotionData.Velocity = (0,4,0) should
|
||||
// surface as (0,4,0) at speedMod=1.0, (0,6,0) at 1.5×, (0,2,0) at
|
||||
// 0.5×. The dead-reckoning integrator in TickAnimations reads
|
||||
// CurrentVelocity each tick, so this has to be accurate.
|
||||
// 0.5×.
|
||||
// CSequence stores that literal DAT velocity for retail
|
||||
// get_state_velocity consumers, so scaling must remain exact.
|
||||
// R2-Q4: retail-mandatory StyleDefaults + the 0x40000000 cycle-class
|
||||
// bit on Motion (see CurrentVelocity_ExposedFromMotionData_WhenHasVelocity).
|
||||
const uint Style = 0x8000003Du;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
using AcDream.Content.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Tests.Conformance;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the retail Humanoid motion table data consumed by CSequence root motion.
|
||||
/// The packet-driven remote mover must use these values, not a velocity guessed
|
||||
/// from the interpreted command.
|
||||
/// </summary>
|
||||
public sealed class HumanoidMotionTableRootMotionTests
|
||||
{
|
||||
private const uint HumanoidMotionTable = 0x09000001u;
|
||||
private const uint HumanoidSetup = 0x02000001u;
|
||||
private const uint NonCombatStyle = 0x8000003Du;
|
||||
private const uint WalkForward = 0x40000005u;
|
||||
private const uint RunForward = 0x40000007u;
|
||||
private const uint Ready = 0x41000003u;
|
||||
private const uint InterpretedWalkForward = 0x45000005u;
|
||||
|
||||
[Theory]
|
||||
[InlineData(WalkForward)]
|
||||
[InlineData(RunForward)]
|
||||
public void RetailLocomotionCycle_ExposesItsLiteralMotionDataVelocity(uint motion)
|
||||
{
|
||||
string? datDir = ConformanceDats.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
return;
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
MotionTable table = Assert.IsType<MotionTable>(dats.Get<MotionTable>(HumanoidMotionTable));
|
||||
int key = unchecked((int)((NonCombatStyle << 16) | (motion & 0x00FF_FFFFu)));
|
||||
MotionData cycle = Assert.IsType<MotionData>(table.Cycles[key]);
|
||||
|
||||
Assert.Equal(System.Numerics.Vector3.Zero, cycle.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailWalkSequence_ProducesAuthoredRootDisplacement()
|
||||
{
|
||||
string? datDir = ConformanceDats.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
return;
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
Setup setup = Assert.IsType<Setup>(dats.Get<Setup>(HumanoidSetup));
|
||||
MotionTable table = Assert.IsType<MotionTable>(dats.Get<MotionTable>(HumanoidMotionTable));
|
||||
var sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
table,
|
||||
new RetailAnimationLoader(dats));
|
||||
|
||||
sequencer.SetCycle(NonCombatStyle, Ready);
|
||||
sequencer.SetCycle(NonCombatStyle, InterpretedWalkForward);
|
||||
|
||||
float distance = 0f;
|
||||
for (int i = 0; i < 120; i++)
|
||||
{
|
||||
var root = new Frame
|
||||
{
|
||||
Origin = System.Numerics.Vector3.Zero,
|
||||
Orientation = System.Numerics.Quaternion.Identity,
|
||||
};
|
||||
sequencer.Advance(1f / 60f, root);
|
||||
distance += root.Origin.Length();
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
distance > 1f,
|
||||
$"Installed retail WalkForward should author root travel; got {distance:F3} m over 2 s.");
|
||||
}
|
||||
}
|
||||
|
|
@ -94,8 +94,9 @@ public class MotionTableDispatchSinkTests
|
|||
|
||||
Assert.Equal(Walk, seq.CurrentMotion);
|
||||
Assert.Equal(2.0f, seq.CurrentSpeedMod);
|
||||
// Locomotion velocity synthesis ran in the passthrough (AP-75).
|
||||
Assert.Equal(MotionInterpreter.WalkAnimSpeed * 2.0f, seq.CurrentVelocity.Y, 3);
|
||||
// add_motion preserves the literal zero MotionData velocity; body
|
||||
// speed belongs to CMotionInterp, not CSequence.
|
||||
Assert.Equal(Vector3.Zero, seq.CurrentVelocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace AcDream.Core.Tests.Physics;
|
|||
// from PositionManager; see RemoteMotionCombiner's class doc).
|
||||
//
|
||||
// Mirrors retail CPhysicsObj::UpdateObjectInternal (acclient @ 0x00513730).
|
||||
// Pure-function combiner: animation root motion (seqVel × dt, rotated by
|
||||
// Pure-function combiner: CSequence root-motion delta (rotated by
|
||||
// body orientation) + InterpolationManager.AdjustOffset correction.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: Vector3.Zero,
|
||||
rootMotionLocalDelta: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
|
@ -53,11 +53,11 @@ public sealed class RemoteMotionCombinerTests
|
|||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
||||
// seqVel = (0, 4, 0), dt = 0.1 → rootMotion = (0, 0.4, 0)
|
||||
// CSequence accumulated (0, 0.4, 0) for this 0.1-second quantum.
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
|
@ -83,7 +83,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f),
|
||||
ori: ori,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: Vector3.Zero,
|
||||
rootMotionLocalDelta: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
|
@ -146,7 +146,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
|
@ -170,12 +170,12 @@ public sealed class RemoteMotionCombinerTests
|
|||
// body-local +Y → world -X
|
||||
Quaternion ori = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
|
||||
|
||||
// seqVel = (0, 1, 0), dt = 1 → rootMotionLocal = (0, 1, 0)
|
||||
// CSequence accumulated rootMotionLocal = (0, 1, 0).
|
||||
// after Transform by ori → (-1, 0, 0) approximately
|
||||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 1.0,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 1f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(0f, 1f, 0f),
|
||||
ori: ori,
|
||||
interp: interp,
|
||||
maxSpeed: 0f);
|
||||
|
|
@ -196,7 +196,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_SeqVelFallback_SlopedTerrainNormal_ProjectsZOntoSlope()
|
||||
public void ComputeOffset_RootMotionFallback_SlopedTerrainNormal_ProjectsZOntoSlope()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp(); // queue empty → fallback path runs
|
||||
|
|
@ -213,7 +213,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 1.0,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(4f, 0f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(4f, 0f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 0f,
|
||||
|
|
@ -225,7 +225,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_SeqVelFallback_FlatTerrainNormal_NoZChange()
|
||||
public void ComputeOffset_RootMotionFallback_FlatTerrainNormal_NoZChange()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = EmptyInterp();
|
||||
|
|
@ -234,7 +234,7 @@ public sealed class RemoteMotionCombinerTests
|
|||
Vector3 offset = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
seqVel: new Vector3(0f, 4f, 0f),
|
||||
rootMotionLocalDelta: new Vector3(0f, 0.4f, 0f),
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 0f,
|
||||
|
|
@ -244,4 +244,33 @@ public sealed class RemoteMotionCombinerTests
|
|||
Assert.Equal(0.4f, offset.Y, precision: 4);
|
||||
Assert.Equal(0f, offset.Z, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeOffset_QueueHeadReached_WithLiteralZeroRootMotion_DoesNotOvershoot()
|
||||
{
|
||||
var pm = Make();
|
||||
var interp = new InterpolationManager();
|
||||
var target = new Vector3(0.4f, 0f, 0f);
|
||||
interp.Enqueue(target, heading: 0f, isMovingTo: false);
|
||||
|
||||
Vector3 catchUp = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: Vector3.Zero,
|
||||
rootMotionLocalDelta: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
Vector3 reachedPosition = catchUp;
|
||||
|
||||
Vector3 afterReach = pm.ComputeOffset(
|
||||
dt: 0.1,
|
||||
currentBodyPosition: reachedPosition,
|
||||
rootMotionLocalDelta: Vector3.Zero,
|
||||
ori: Quaternion.Identity,
|
||||
interp: interp,
|
||||
maxSpeed: 4f);
|
||||
|
||||
Assert.Equal(target, reachedPosition);
|
||||
Assert.Equal(Vector3.Zero, afterReach);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue