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>
292 lines
11 KiB
C#
292 lines
11 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.App.Tests.Input;
|
|
|
|
public sealed class RetailLocalPlayerFrameControllerTests
|
|
{
|
|
[Fact]
|
|
public void F751BetweenPhases_CannotRelocateOrReplayCapturedOutboundMovement()
|
|
{
|
|
PlayerMovementController controller = CreateController();
|
|
var calls = new List<string>();
|
|
var preNetwork = new List<(PlayerState State, MovementResult Movement)>();
|
|
var postNetwork = new List<PlayerState>();
|
|
var local = new RetailLocalPlayerFrameController(
|
|
canPresentPlayer: () => true,
|
|
getController: () => controller,
|
|
captureInput: () => new MovementInput(Forward: true),
|
|
resolveLocalEntityId: () => 7u,
|
|
handleTargeting: () => calls.Add("target"),
|
|
isHidden: () => false,
|
|
project: (_, _, _) => calls.Add("project"),
|
|
sendPreNetwork: (owner, movement, _) =>
|
|
{
|
|
calls.Add("pre-outbound");
|
|
preNetwork.Add((owner.State, movement));
|
|
},
|
|
sendPostNetwork: (owner, _) =>
|
|
{
|
|
calls.Add("post-position");
|
|
postNetwork.Add(owner.State);
|
|
});
|
|
var live = new RetailLiveFrameCoordinator(
|
|
dt =>
|
|
{
|
|
calls.Add("objects");
|
|
local.AdvanceBeforeNetwork(dt);
|
|
},
|
|
() =>
|
|
{
|
|
calls.Add("f751");
|
|
controller.State = PlayerState.PortalSpace;
|
|
},
|
|
() =>
|
|
{
|
|
calls.Add("commands");
|
|
local.RunPostNetworkCommandPhase();
|
|
},
|
|
() => calls.Add("spatial"));
|
|
|
|
// 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(
|
|
[
|
|
"objects", "target", "project", "pre-outbound", "f751",
|
|
"commands", "project", "post-position", "spatial",
|
|
],
|
|
calls);
|
|
Assert.Single(preNetwork);
|
|
Assert.Equal(PlayerState.InWorld, preNetwork[0].State);
|
|
Assert.True(preNetwork[0].Movement.ShouldSendMovementEvent);
|
|
Assert.Equal([PlayerState.PortalSpace], postNetwork);
|
|
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
|
Assert.True(presentation.AdvancedBeforeNetwork);
|
|
}
|
|
|
|
[Fact]
|
|
public void OwnerCreatedByInboundPass_IsProjectedWithoutPhysicsOrOutboundTick()
|
|
{
|
|
PlayerMovementController? controller = null;
|
|
int projections = 0;
|
|
int preNetwork = 0;
|
|
int postNetwork = 0;
|
|
var local = new RetailLocalPlayerFrameController(
|
|
canPresentPlayer: () => controller is not null,
|
|
getController: () => controller,
|
|
captureInput: () => new MovementInput(Forward: true),
|
|
resolveLocalEntityId: () => 9u,
|
|
handleTargeting: () => { },
|
|
isHidden: () => false,
|
|
project: (_, _, _) => projections++,
|
|
sendPreNetwork: (_, _, _) => preNetwork++,
|
|
sendPostNetwork: (_, _) => postNetwork++);
|
|
|
|
local.AdvanceBeforeNetwork(1f / 60f);
|
|
controller = CreateController();
|
|
float timeBefore = controller.SimTimeSeconds;
|
|
|
|
Assert.True(local.TryGetPresentationAfterNetwork(out var presentation));
|
|
|
|
Assert.False(presentation.AdvancedBeforeNetwork);
|
|
Assert.Equal(timeBefore, controller.SimTimeSeconds);
|
|
Assert.Equal(1, projections);
|
|
Assert.Equal(0, preNetwork);
|
|
Assert.Equal(0, postNetwork);
|
|
Assert.False(presentation.Movement.ShouldSendMovementEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void OrdinaryInboundRootMutation_IsReconciledWithoutSecondPhysicsTick()
|
|
{
|
|
PlayerMovementController controller = CreateController();
|
|
Vector3 projectedRoot = default;
|
|
var local = new RetailLocalPlayerFrameController(
|
|
canPresentPlayer: () => true,
|
|
getController: () => controller,
|
|
captureInput: () => new MovementInput(),
|
|
resolveLocalEntityId: () => 7u,
|
|
handleTargeting: () => { },
|
|
isHidden: () => false,
|
|
project: (_, movement, _) => projectedRoot = movement.RenderPosition,
|
|
sendPreNetwork: (_, _, _) => { },
|
|
sendPostNetwork: (_, _) => { });
|
|
var live = new RetailLiveFrameCoordinator(
|
|
local.AdvanceBeforeNetwork,
|
|
() => projectedRoot = new Vector3(999f, 999f, 999f),
|
|
local.RunPostNetworkCommandPhase,
|
|
() => { });
|
|
|
|
live.Tick(1f / 60f);
|
|
float timeAfterOneTick = controller.SimTimeSeconds;
|
|
|
|
Assert.Equal(controller.RenderPosition, projectedRoot);
|
|
Assert.Equal(1f / 60f, timeAfterOneTick);
|
|
}
|
|
|
|
[Fact]
|
|
public void TargetManager_ObservesPostTransitionPlayerPosition()
|
|
{
|
|
PlayerMovementController controller = CreateController();
|
|
Vector3 initial = controller.Position;
|
|
Vector3 positionSeenByTargetManager = new(float.NaN);
|
|
var local = new RetailLocalPlayerFrameController(
|
|
canPresentPlayer: () => true,
|
|
getController: () => controller,
|
|
captureInput: () => new MovementInput(Forward: true),
|
|
resolveLocalEntityId: () => 7u,
|
|
handleTargeting: () => positionSeenByTargetManager = controller.Position,
|
|
isHidden: () => false,
|
|
project: (_, _, _) => { },
|
|
sendPreNetwork: (_, _, _) => { },
|
|
sendPostNetwork: (_, _) => { });
|
|
|
|
local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
|
|
|
Assert.NotEqual(initial, controller.Position);
|
|
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();
|
|
var heights = new byte[81];
|
|
Array.Fill(heights, (byte)50);
|
|
var heightTable = new float[256];
|
|
for (int i = 0; i < heightTable.Length; i++)
|
|
heightTable[i] = i;
|
|
|
|
engine.AddLandblock(
|
|
0xA9B4FFFFu,
|
|
new TerrainSurface(heights, heightTable),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f);
|
|
|
|
var clock = new RetailObjectQuantumClock();
|
|
var controller = new PlayerMovementController(engine, clock);
|
|
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001u);
|
|
Assert.True(clock.IsActive);
|
|
return controller;
|
|
}
|
|
}
|