feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
31a0889f08
commit
f961d70023
77 changed files with 12513 additions and 1871 deletions
|
|
@ -8,6 +8,8 @@ namespace AcDream.Core.Tests.Input;
|
|||
|
||||
public class PlayerMovementControllerTests
|
||||
{
|
||||
private static float ObjectTick => PhysicsBody.MinQuantum + 0.001f;
|
||||
|
||||
private static PhysicsEngine MakeFlatEngine()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
|
|
@ -21,6 +23,29 @@ public class PlayerMovementControllerTests
|
|||
return engine;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_ReactivatesCanonicalClock_ExceptForStaticObject()
|
||||
{
|
||||
var clock = new RetailObjectQuantumClock();
|
||||
var controller = new PlayerMovementController(MakeFlatEngine(), clock);
|
||||
clock.Deactivate();
|
||||
controller.ApplyPhysicsState(PhysicsStateFlags.None);
|
||||
|
||||
controller.Movement.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = (MovementType)99,
|
||||
});
|
||||
Assert.True(clock.IsActive);
|
||||
|
||||
clock.Deactivate();
|
||||
controller.ApplyPhysicsState(PhysicsStateFlags.Static);
|
||||
controller.Movement.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = (MovementType)99,
|
||||
});
|
||||
Assert.False(clock.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NoInput_PositionUnchanged()
|
||||
{
|
||||
|
|
@ -156,10 +181,10 @@ public class PlayerMovementControllerTests
|
|||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
controller.AttachAnimationRootMotionSource(_ => Vector3.Zero);
|
||||
controller.AttachAnimationRootMotionSource((_, _) => { });
|
||||
|
||||
MovementResult result = controller.Update(
|
||||
PhysicsBody.MinQuantum,
|
||||
ObjectTick,
|
||||
new MovementInput(Forward: true));
|
||||
|
||||
Assert.Equal(start, result.Position);
|
||||
|
|
@ -175,10 +200,11 @@ public class PlayerMovementControllerTests
|
|||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
controller.ObjectScale = 2f;
|
||||
controller.AttachAnimationRootMotionSource(_ => new Vector3(0f, 0.1f, 0f));
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
frame.Origin = new Vector3(0f, 0.1f, 0f));
|
||||
|
||||
MovementResult result = controller.Update(
|
||||
PhysicsBody.MinQuantum,
|
||||
ObjectTick,
|
||||
new MovementInput(Forward: true));
|
||||
|
||||
// Local +Y is forward. Yaw 0 maps it to world +X; m_scale doubles
|
||||
|
|
@ -188,24 +214,313 @@ public class PlayerMovementControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SubQuantumAnimationRootDeltas_AccumulateIntoOnePhysicsStep()
|
||||
public void Update_SubQuantumFrames_AdvanceAnimationOnceAtObjectThreshold()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
controller.AttachAnimationRootMotionSource(_ => new Vector3(0f, 0.05f, 0f));
|
||||
int advances = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
advances++;
|
||||
frame.Origin = new Vector3(0f, 0.1f, 0f);
|
||||
});
|
||||
|
||||
MovementResult first = controller.Update(
|
||||
PhysicsBody.MinQuantum * 0.5f,
|
||||
ObjectTick * 0.5f,
|
||||
new MovementInput(Forward: true));
|
||||
MovementResult second = controller.Update(
|
||||
PhysicsBody.MinQuantum * 0.5f,
|
||||
ObjectTick * 0.5f,
|
||||
new MovementInput(Forward: true));
|
||||
|
||||
Assert.Equal(start, first.Position);
|
||||
Assert.Equal(start.X + 0.1f, second.Position.X, precision: 3);
|
||||
Assert.Equal(start.Y, second.Position.Y, precision: 3);
|
||||
Assert.Equal(1, advances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AttachedAnimationFrame_ComposesTranslationBeforeTurn()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f; // body local +Y faces world +X
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
frame.Origin = new Vector3(0f, 0.1f, 0f);
|
||||
frame.Orientation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f);
|
||||
});
|
||||
|
||||
MovementResult result = controller.Update(
|
||||
ObjectTick,
|
||||
new MovementInput());
|
||||
|
||||
// Retail Frame::combine transforms the delta origin by the OLD body
|
||||
// orientation, then composes the delta orientation. The body therefore
|
||||
// moves east before finishing the quarter-turn to north.
|
||||
Assert.Equal(start.X + 0.1f, result.Position.X, precision: 3);
|
||||
Assert.Equal(start.Y, result.Position.Y, precision: 3);
|
||||
Assert.Equal(MathF.PI / 2f, controller.Yaw, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AttachedAnimationFrame_PreservesCompleteNonCommutingOrientation()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
Quaternion initial = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1.1f);
|
||||
Quaternion delta = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.9f);
|
||||
controller.SetBodyOrientation(initial);
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
frame.Orientation = delta);
|
||||
|
||||
controller.Update(ObjectTick, new MovementInput());
|
||||
|
||||
Quaternion expected = Quaternion.Normalize(initial * delta);
|
||||
float alignment = MathF.Abs(Quaternion.Dot(
|
||||
expected,
|
||||
Quaternion.Normalize(controller.BodyOrientation)));
|
||||
Assert.InRange(alignment, 0.99999f, 1.00001f);
|
||||
Assert.True(MathF.Abs(Quaternion.Dot(
|
||||
Quaternion.Normalize(delta * initial),
|
||||
Quaternion.Normalize(controller.BodyOrientation))) < 0.999f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultiQuantumAnimationFrames_ComposeInTemporalOrder()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
int sample = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
if (sample++ == 0)
|
||||
{
|
||||
frame.Orientation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
MathF.PI / 2f);
|
||||
}
|
||||
else
|
||||
{
|
||||
frame.Origin = new Vector3(0f, 0.1f, 0f);
|
||||
}
|
||||
});
|
||||
|
||||
MovementResult result = controller.Update(
|
||||
PhysicsBody.MaxQuantum * 2f,
|
||||
new MovementInput());
|
||||
|
||||
// The second local-forward displacement occurs after the first
|
||||
// quarter-turn, so it moves north. Adding Origins as bare vectors
|
||||
// would incorrectly move east.
|
||||
Assert.Equal(start.X, result.Position.X, precision: 3);
|
||||
Assert.Equal(start.Y + 0.1f, result.Position.Y, precision: 3);
|
||||
Assert.Equal(MathF.PI / 2f, controller.Yaw, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ExactMinQuantum_RetainsTimeWithoutAdvancingObject()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SetPosition(start, 0x0001);
|
||||
int advances = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
advances++;
|
||||
frame.Origin = new Vector3(0f, 1f, 0f);
|
||||
});
|
||||
|
||||
MovementResult atThreshold = controller.Update(
|
||||
PhysicsBody.MinQuantum,
|
||||
new MovementInput());
|
||||
|
||||
Assert.Equal(start, atThreshold.Position);
|
||||
Assert.Equal(0, advances);
|
||||
|
||||
controller.Update(0.001f, new MovementInput());
|
||||
Assert.Equal(1, advances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LargeFrame_MatchesSeparateRetailObjectQuanta()
|
||||
{
|
||||
var combined = new PlayerMovementController(MakeFlatEngine());
|
||||
var split = new PlayerMovementController(MakeFlatEngine());
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
combined.SetPosition(start, 0x0001);
|
||||
split.SetPosition(start, 0x0001);
|
||||
int combinedHooks = 0;
|
||||
int splitHooks = 0;
|
||||
|
||||
static void Advance(float dt, AcDream.Core.Physics.Motion.MotionDeltaFrame frame)
|
||||
{
|
||||
frame.Origin = new Vector3(0f, dt * 2f, 0f);
|
||||
frame.Orientation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
dt * 0.7f);
|
||||
}
|
||||
|
||||
combined.AttachAnimationRootMotionSource(Advance, () => combinedHooks++);
|
||||
split.AttachAnimationRootMotionSource(Advance, () => splitHooks++);
|
||||
|
||||
combined.Update(PhysicsBody.MaxQuantum * 2f, new MovementInput());
|
||||
split.Update(PhysicsBody.MaxQuantum, new MovementInput());
|
||||
split.Update(PhysicsBody.MaxQuantum, new MovementInput());
|
||||
|
||||
Assert.Equal(split.Position.X, combined.Position.X, precision: 5);
|
||||
Assert.Equal(split.Position.Y, combined.Position.Y, precision: 5);
|
||||
Assert.Equal(split.Position.Z, combined.Position.Z, precision: 5);
|
||||
float alignment = MathF.Abs(Quaternion.Dot(
|
||||
Quaternion.Normalize(split.BodyOrientation),
|
||||
Quaternion.Normalize(combined.BodyOrientation)));
|
||||
Assert.InRange(alignment, 0.99999f, 1.00001f);
|
||||
Assert.Equal(2, combinedHooks);
|
||||
Assert.Equal(2, splitHooks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TickHidden_DoesNotAdvancePartArrayButStillProcessesHooks()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
int advances = 0;
|
||||
int hookPasses = 0;
|
||||
controller.AttachAnimationRootMotionSource(
|
||||
(_, frame) =>
|
||||
{
|
||||
advances++;
|
||||
frame.Origin = new Vector3(0f, 1f, 0f);
|
||||
},
|
||||
() => hookPasses++);
|
||||
|
||||
controller.TickHidden(ObjectTick);
|
||||
|
||||
Assert.Equal(0, advances);
|
||||
Assert.Equal(1, hookPasses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidElapsed_VisibleFrameIsPurePresentationRead()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
float initialTime = controller.SimTimeSeconds;
|
||||
float initialYaw = controller.Yaw;
|
||||
Vector3 initialPosition = controller.Position;
|
||||
RawMotionState initialMotion = controller.Motion.RawState;
|
||||
var hostileInput = new MovementInput(
|
||||
Forward: true,
|
||||
TurnLeft: true,
|
||||
Jump: true,
|
||||
Run: true);
|
||||
|
||||
foreach (float elapsed in new[]
|
||||
{
|
||||
float.NaN,
|
||||
float.PositiveInfinity,
|
||||
float.NegativeInfinity,
|
||||
-0.1f,
|
||||
0f,
|
||||
})
|
||||
{
|
||||
MovementResult result = controller.Update(elapsed, hostileInput);
|
||||
Assert.False(result.ShouldSendMovementEvent);
|
||||
}
|
||||
|
||||
Assert.Equal(initialTime, controller.SimTimeSeconds);
|
||||
Assert.Equal(initialYaw, controller.Yaw);
|
||||
Assert.Equal(initialPosition, controller.Position);
|
||||
Assert.Equal(initialMotion, controller.Motion.RawState);
|
||||
|
||||
controller.Update(ObjectTick, new MovementInput());
|
||||
controller.Update(ObjectTick, new MovementInput());
|
||||
Assert.True(controller.AdvancedObjectQuantumLastTick);
|
||||
controller.Update(float.NaN, hostileInput);
|
||||
Assert.False(controller.AdvancedObjectQuantumLastTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidElapsed_HiddenFrameDoesNotAdvanceClockOrManagerTail()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
float initialTime = controller.SimTimeSeconds;
|
||||
int targetPasses = 0;
|
||||
|
||||
controller.TickHidden(float.NaN, () => targetPasses++);
|
||||
controller.TickHidden(float.PositiveInfinity, () => targetPasses++);
|
||||
controller.TickHidden(-0.1f, () => targetPasses++);
|
||||
|
||||
Assert.Equal(initialTime, controller.SimTimeSeconds);
|
||||
Assert.Equal(0, targetPasses);
|
||||
Assert.False(controller.AdvancedObjectQuantumLastTick);
|
||||
|
||||
controller.TickHidden(ObjectTick);
|
||||
controller.TickHidden(ObjectTick);
|
||||
Assert.True(controller.AdvancedObjectQuantumLastTick);
|
||||
controller.TickHidden(float.PositiveInfinity);
|
||||
Assert.False(controller.AdvancedObjectQuantumLastTick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AirbornePartArrayFrame_SuppressesOriginButPreservesOrientation()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.Update(1f, new MovementInput(Jump: true));
|
||||
|
||||
Vector3 beforeRelease = controller.Position;
|
||||
Quaternion beforeOrientation = controller.BodyOrientation;
|
||||
Quaternion delta = Quaternion.CreateFromAxisAngle(Vector3.UnitY, 0.4f);
|
||||
int advances = 0;
|
||||
controller.AttachAnimationRootMotionSource((_, frame) =>
|
||||
{
|
||||
advances++;
|
||||
frame.Origin = new Vector3(0f, 10f, 0f);
|
||||
frame.Orientation = delta;
|
||||
});
|
||||
|
||||
controller.Update(ObjectTick, new MovementInput(Jump: false));
|
||||
|
||||
Assert.True(controller.IsAirborne);
|
||||
Assert.Equal(beforeRelease.X, controller.Position.X, precision: 5);
|
||||
Assert.Equal(beforeRelease.Y, controller.Position.Y, precision: 5);
|
||||
Quaternion expected = Quaternion.Normalize(beforeOrientation * delta);
|
||||
float alignment = MathF.Abs(Quaternion.Dot(
|
||||
expected,
|
||||
Quaternion.Normalize(controller.BodyOrientation)));
|
||||
Assert.InRange(alignment, 0.99999f, 1.00001f);
|
||||
Assert.Equal(1, advances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AttachedAnimationTurn_IsNotAppliedByASecondYawIntegrator()
|
||||
{
|
||||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
controller.AttachAnimationRootMotionSource((dt, frame) =>
|
||||
{
|
||||
frame.Orientation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
-(MathF.PI / 2f) * dt);
|
||||
});
|
||||
|
||||
controller.Update(
|
||||
ObjectTick,
|
||||
new MovementInput(TurnRight: true));
|
||||
|
||||
Assert.Equal(
|
||||
-(MathF.PI / 2f) * ObjectTick,
|
||||
controller.Yaw,
|
||||
precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -217,7 +532,7 @@ public class PlayerMovementControllerTests
|
|||
controller.SetPosition(start, 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var firstTick = controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||
var firstTick = controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
Assert.True(firstTick.Position.X > start.X, "Physics tick should advance the authoritative body position");
|
||||
Assert.Equal(start.X, firstTick.RenderPosition.X, precision: 4);
|
||||
|
||||
|
|
@ -240,7 +555,7 @@ public class PlayerMovementControllerTests
|
|||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
|
||||
controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||
controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
controller.Update(PhysicsBody.MinQuantum * 0.5f, new MovementInput(Forward: true));
|
||||
|
||||
var snapped = new Vector3(120f, 80f, 50f);
|
||||
|
|
@ -257,7 +572,7 @@ public class PlayerMovementControllerTests
|
|||
var controller = new PlayerMovementController(MakeFlatEngine());
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||
controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
Vector3 velocity = controller.BodyVelocity;
|
||||
Assert.True(velocity.LengthSquared() > 0f);
|
||||
|
||||
|
|
@ -305,12 +620,25 @@ public class PlayerMovementControllerTests
|
|||
var controller = new PlayerMovementController(engine);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
|
||||
controller.Yaw = 0f;
|
||||
int animationAdvances = 0;
|
||||
int hookPasses = 0;
|
||||
controller.AttachAnimationRootMotionSource(
|
||||
(_, frame) =>
|
||||
{
|
||||
animationAdvances++;
|
||||
frame.Origin = new Vector3(0f, 0.1f, 0f);
|
||||
},
|
||||
() => hookPasses++);
|
||||
|
||||
var moved = controller.Update(PhysicsBody.MinQuantum, new MovementInput(Forward: true));
|
||||
var moved = controller.Update(ObjectTick, new MovementInput(Forward: true));
|
||||
int advancesBeforeDiscard = animationAdvances;
|
||||
int hooksBeforeDiscard = hookPasses;
|
||||
var stale = controller.Update(PhysicsBody.HugeQuantum + 0.1f, new MovementInput(Forward: true));
|
||||
|
||||
Assert.Equal(moved.Position.X, stale.Position.X, precision: 4);
|
||||
Assert.Equal(stale.Position, stale.RenderPosition);
|
||||
Assert.Equal(advancesBeforeDiscard, animationAdvances);
|
||||
Assert.Equal(hooksBeforeDiscard, hookPasses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue