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
|
|
@ -0,0 +1,123 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
||||
public sealed class LocalPlayerProjectionControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Project_PreservesCompleteAuthoritativeBodyQuaternion()
|
||||
{
|
||||
const uint cellId = 0x01010001u;
|
||||
Quaternion complete = Quaternion.Normalize(
|
||||
Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.7f)
|
||||
* Quaternion.CreateFromAxisAngle(Vector3.UnitY, -0.4f));
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(Vector3.Zero, cellId, Vector3.Zero);
|
||||
movement.SetBodyOrientation(complete);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 7u,
|
||||
ServerGuid = 0x50000001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
() => entity,
|
||||
() => 1,
|
||||
() => 1,
|
||||
(_, _) => { },
|
||||
(_, _) => { },
|
||||
_ => true,
|
||||
_ => { });
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
movement.CapturePresentationResult(),
|
||||
hidden: false);
|
||||
|
||||
Assert.InRange(
|
||||
MathF.Abs(Quaternion.Dot(
|
||||
complete,
|
||||
Quaternion.Normalize(entity.Rotation))),
|
||||
0.99999f,
|
||||
1.00001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_AlreadyPendingChangedPose_DoesNotResurrectShadow()
|
||||
{
|
||||
const uint cellId = 0x02020001u;
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 8u,
|
||||
ServerGuid = 0x50000001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
var order = new List<string>();
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
() => entity,
|
||||
() => 2,
|
||||
() => 2,
|
||||
(_, _) => order.Add("shadow"),
|
||||
(_, _) => order.Add("rebucket"),
|
||||
_ => false,
|
||||
_ => order.Add("suspend"));
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
movement.CapturePresentationResult(),
|
||||
hidden: false);
|
||||
|
||||
Assert.Equal(["rebucket", "suspend"], order);
|
||||
Assert.Equal(movement.Position, entity.Position);
|
||||
Assert.Equal(cellId, entity.ParentCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Project_PendingToLoaded_RebucketsBeforeStationaryShadowRestore()
|
||||
{
|
||||
const uint cellId = 0x02020001u;
|
||||
var movement = new PlayerMovementController(new PhysicsEngine());
|
||||
movement.SetPosition(new Vector3(12f, 8f, 3f), cellId, Vector3.Zero);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 9u,
|
||||
ServerGuid = 0x50000001u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = movement.Position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
bool visible = false;
|
||||
var order = new List<string>();
|
||||
var projection = new LocalPlayerProjectionController(
|
||||
() => entity,
|
||||
() => 2,
|
||||
() => 2,
|
||||
(_, _) => order.Add("shadow"),
|
||||
(_, _) =>
|
||||
{
|
||||
order.Add("rebucket");
|
||||
visible = true;
|
||||
},
|
||||
_ => visible,
|
||||
_ => order.Add("suspend"));
|
||||
|
||||
projection.Project(
|
||||
movement,
|
||||
movement.CapturePresentationResult(),
|
||||
hidden: false);
|
||||
|
||||
Assert.Equal(["rebucket", "shadow"], order);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,12 @@ public sealed class PlayerMouseLookMovementTests
|
|||
|
||||
float yawBefore = controller.Yaw;
|
||||
controller.SubmitMouseTurnAdjustment(-0.4f, new MovementInput());
|
||||
MovementResult turning = controller.Update(1f / 60f, new MovementInput());
|
||||
// Rotation is authored/applied on an admitted retail object quantum,
|
||||
// not directly by the mouse sample. Stay below the outbound mouse
|
||||
// cadence while crossing the strict physics minimum.
|
||||
MovementResult turning = controller.Update(
|
||||
PhysicsBody.MinQuantum + 0.001f,
|
||||
new MovementInput());
|
||||
|
||||
Assert.Equal(MotionCommand.TurnRight, turning.TurnCommand);
|
||||
Assert.Equal(0.8f, turning.TurnSpeed);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
},
|
||||
() => calls.Add("spatial"));
|
||||
|
||||
live.Tick(1f / 60f);
|
||||
// CPhysicsObj::update_object admits manager time only after the strict
|
||||
// minimum quantum. Use one admitted object tick; the test's concern is
|
||||
// the network barrier, not render-fragment accumulation.
|
||||
live.Tick(PhysicsBody.MaxQuantum);
|
||||
Assert.True(local.TryGetPresentationAfterNetwork(out var presentation));
|
||||
|
||||
Assert.Equal(
|
||||
|
|
@ -150,6 +153,119 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
Assert.Equal(controller.Position, positionSeenByTargetManager);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrozenObject_IsPresentedWithoutPhysicsInputOrOutboundState()
|
||||
{
|
||||
PlayerMovementController controller = CreateController();
|
||||
Vector3 initial = controller.Position;
|
||||
int inputCaptures = 0;
|
||||
int projections = 0;
|
||||
int preNetwork = 0;
|
||||
int postNetwork = 0;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
captureInput: () =>
|
||||
{
|
||||
inputCaptures++;
|
||||
return new MovementInput(Forward: true);
|
||||
},
|
||||
resolveLocalEntityId: () => 7u,
|
||||
handleTargeting: () => throw new InvalidOperationException(
|
||||
"Frozen object advanced its manager tail."),
|
||||
isHidden: () => false,
|
||||
project: (_, _, _) => projections++,
|
||||
sendPreNetwork: (_, _, _) => preNetwork++,
|
||||
sendPostNetwork: (_, _) => postNetwork++,
|
||||
objectClockDisposition: () =>
|
||||
RetailObjectClockDisposition.Suspend);
|
||||
|
||||
local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
||||
local.RunPostNetworkCommandPhase();
|
||||
Assert.True(local.TryGetPresentationAfterNetwork(out var presentation));
|
||||
|
||||
Assert.Equal(initial, controller.Position);
|
||||
Assert.Equal(PhysicsBody.MaxQuantum, controller.SimTimeSeconds);
|
||||
Assert.Equal(0, inputCaptures);
|
||||
Assert.Equal(2, projections);
|
||||
Assert.Equal(0, preNetwork);
|
||||
Assert.Equal(0, postNetwork);
|
||||
Assert.False(presentation.AdvancedBeforeNetwork);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenPoseIsDirtyOnlyWhenACompleteObjectQuantumRuns()
|
||||
{
|
||||
PlayerMovementController controller = CreateController();
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
captureInput: () => new MovementInput(),
|
||||
resolveLocalEntityId: () => 7u,
|
||||
handleTargeting: () => { },
|
||||
isHidden: () => true,
|
||||
project: (_, _, _) => { },
|
||||
sendPreNetwork: (_, _, _) => { },
|
||||
sendPostNetwork: (_, _) => { });
|
||||
|
||||
local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
||||
Assert.True(local.HiddenPartPoseDirty);
|
||||
|
||||
local.AdvanceBeforeNetwork(PhysicsBody.MinQuantum / 2f);
|
||||
Assert.False(local.HiddenPartPoseDirty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidElapsed_PublishesSnapshotWithoutInputOrNetworkCallbacks()
|
||||
{
|
||||
PlayerMovementController controller = CreateController();
|
||||
int inputCaptures = 0;
|
||||
int targeting = 0;
|
||||
int projections = 0;
|
||||
int preNetwork = 0;
|
||||
int postNetwork = 0;
|
||||
var local = new RetailLocalPlayerFrameController(
|
||||
canPresentPlayer: () => true,
|
||||
getController: () => controller,
|
||||
captureInput: () =>
|
||||
{
|
||||
inputCaptures++;
|
||||
return new MovementInput(Forward: true);
|
||||
},
|
||||
resolveLocalEntityId: () => 7u,
|
||||
handleTargeting: () => targeting++,
|
||||
isHidden: () => false,
|
||||
project: (_, _, _) => projections++,
|
||||
sendPreNetwork: (_, _, _) => preNetwork++,
|
||||
sendPostNetwork: (_, _) => postNetwork++);
|
||||
float initialTime = controller.SimTimeSeconds;
|
||||
Vector3 initialPosition = controller.Position;
|
||||
|
||||
foreach (float elapsed in new[]
|
||||
{
|
||||
float.NaN,
|
||||
float.PositiveInfinity,
|
||||
float.NegativeInfinity,
|
||||
-1f,
|
||||
0f,
|
||||
})
|
||||
{
|
||||
local.AdvanceBeforeNetwork(elapsed);
|
||||
local.RunPostNetworkCommandPhase();
|
||||
Assert.True(local.TryGetPresentationAfterNetwork(out var frame));
|
||||
Assert.False(frame.AdvancedBeforeNetwork);
|
||||
Assert.False(local.HiddenPartPoseDirty);
|
||||
}
|
||||
|
||||
Assert.Equal(initialTime, controller.SimTimeSeconds);
|
||||
Assert.Equal(initialPosition, controller.Position);
|
||||
Assert.Equal(0, inputCaptures);
|
||||
Assert.Equal(0, targeting);
|
||||
Assert.Equal(0, preNetwork);
|
||||
Assert.Equal(0, postNetwork);
|
||||
Assert.Equal(10, projections);
|
||||
}
|
||||
|
||||
private static PlayerMovementController CreateController()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
|
|
@ -167,8 +283,10 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
|||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
|
||||
var controller = new PlayerMovementController(engine);
|
||||
var clock = new RetailObjectQuantumClock();
|
||||
var controller = new PlayerMovementController(engine, clock);
|
||||
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
||||
Assert.True(clock.IsActive);
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue