refactor(camera): extract the update frame

Move fly/chase publication, combat target tracking, and local player projection behind typed runtime owners. Preserve the inbound-created projection/reconcile barrier while removing GameWindow callbacks and duplicate shadow helpers.
This commit is contained in:
Erik 2026-07-22 03:08:58 +02:00
parent eeb0f6b45c
commit 947c61d2d7
19 changed files with 988 additions and 356 deletions

View file

@ -26,14 +26,14 @@ public sealed class LocalPlayerProjectionControllerTests
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
var projection = new LocalPlayerProjectionController(
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
() => entity,
() => 1,
() => 1,
(_, _) => { },
(_, _) => { },
_ => true,
_ => { });
_ => { }));
projection.Project(
movement,
@ -64,14 +64,14 @@ public sealed class LocalPlayerProjectionControllerTests
MeshRefs = Array.Empty<MeshRef>(),
};
var order = new List<string>();
var projection = new LocalPlayerProjectionController(
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
() => entity,
() => 2,
() => 2,
(_, _) => order.Add("shadow"),
(_, _) => order.Add("rebucket"),
_ => false,
_ => order.Add("suspend"));
_ => order.Add("suspend")));
projection.Project(
movement,
@ -100,7 +100,7 @@ public sealed class LocalPlayerProjectionControllerTests
};
bool visible = false;
var order = new List<string>();
var projection = new LocalPlayerProjectionController(
var projection = new LocalPlayerProjectionController(new TestProjectionRuntime(
() => entity,
() => 2,
() => 2,
@ -111,7 +111,7 @@ public sealed class LocalPlayerProjectionControllerTests
visible = true;
},
_ => visible,
_ => order.Add("suspend"));
_ => order.Add("suspend")));
projection.Project(
movement,
@ -120,4 +120,25 @@ public sealed class LocalPlayerProjectionControllerTests
Assert.Equal(["rebucket", "shadow"], order);
}
private sealed class TestProjectionRuntime(
Func<WorldEntity?> resolveEntity,
Func<int> liveCenterX,
Func<int> liveCenterY,
Action<WorldEntity, uint> syncShadow,
Action<uint, uint> rebucket,
Func<WorldEntity, bool> isCurrentVisibleProjection,
Action<WorldEntity> suspendShadow) : ILocalPlayerProjectionRuntime
{
public WorldEntity? ResolveEntity() => resolveEntity();
public int LiveCenterX => liveCenterX();
public int LiveCenterY => liveCenterY();
public void SyncShadow(WorldEntity entity, uint cellId) =>
syncShadow(entity, cellId);
public void Rebucket(uint serverGuid, uint landblockId) =>
rebucket(serverGuid, landblockId);
public bool IsCurrentVisibleProjection(WorldEntity entity) =>
isCurrentVisibleProjection(entity);
public void SuspendShadow(WorldEntity entity) => suspendShadow(entity);
}
}

View file

@ -15,7 +15,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
var calls = new List<string>();
var preNetwork = new List<(PlayerState State, MovementResult Movement)>();
var postNetwork = new List<PlayerState>();
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() => new MovementInput(Forward: true)),
@ -79,7 +79,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
int projections = 0;
int preNetwork = 0;
int postNetwork = 0;
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => controller is not null,
getController: () => controller,
movementInput: Input(() => new MovementInput(Forward: true)),
@ -109,7 +109,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
{
PlayerMovementController controller = CreateController();
Vector3 projectedRoot = default;
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() => new MovementInput()),
@ -140,7 +140,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
PlayerMovementController controller = CreateController();
Vector3 initial = controller.Position;
Vector3 positionSeenByTargetManager = new(float.NaN);
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() => new MovementInput(Forward: true)),
@ -166,7 +166,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
int projections = 0;
int preNetwork = 0;
int postNetwork = 0;
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() =>
@ -201,7 +201,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
public void HiddenPoseIsDirtyOnlyWhenACompleteObjectQuantumRuns()
{
PlayerMovementController controller = CreateController();
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() => new MovementInput()),
@ -228,7 +228,7 @@ public sealed class RetailLocalPlayerFrameControllerTests
int projections = 0;
int preNetwork = 0;
int postNetwork = 0;
var local = new RetailLocalPlayerFrameController(
var local = CreateFrame(
canPresentPlayer: () => true,
getController: () => controller,
movementInput: Input(() =>
@ -297,6 +297,65 @@ public sealed class RetailLocalPlayerFrameControllerTests
private static IMovementInputSource Input(Func<MovementInput> capture) =>
new TestMovementInputSource(capture);
private static RetailLocalPlayerFrameController CreateFrame(
Func<bool> canPresentPlayer,
Func<PlayerMovementController?> getController,
IMovementInputSource movementInput,
Func<uint> resolveLocalEntityId,
Action handleTargeting,
Func<bool> isHidden,
Action<PlayerMovementController, MovementResult, bool> project,
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
Action<PlayerMovementController, bool> sendPostNetwork,
Func<RetailObjectClockDisposition>? objectClockDisposition = null) =>
new(
new TestLocalPlayerFrameRuntime(
canPresentPlayer,
getController,
resolveLocalEntityId,
handleTargeting,
isHidden,
project,
sendPreNetwork,
sendPostNetwork,
objectClockDisposition),
movementInput);
private sealed class TestLocalPlayerFrameRuntime(
Func<bool> canPresentPlayer,
Func<PlayerMovementController?> getController,
Func<uint> resolveLocalEntityId,
Action handleTargeting,
Func<bool> isHidden,
Action<PlayerMovementController, MovementResult, bool> project,
Action<PlayerMovementController, MovementResult, bool> sendPreNetwork,
Action<PlayerMovementController, bool> sendPostNetwork,
Func<RetailObjectClockDisposition>? objectClockDisposition)
: ILocalPlayerFrameRuntime
{
public bool CanPresentPlayer => canPresentPlayer();
public PlayerMovementController? Controller => getController();
public uint ResolveLocalEntityId() => resolveLocalEntityId();
public void HandleTargeting() => handleTargeting();
public bool IsHidden => isHidden();
public RetailObjectClockDisposition ObjectClockDisposition =>
objectClockDisposition?.Invoke() ?? RetailObjectClockDisposition.Advance;
public void Project(
PlayerMovementController controller,
MovementResult movement,
bool hidden) => project(controller, movement, hidden);
public void SendPreNetwork(
PlayerMovementController controller,
MovementResult movement,
bool hidden) => sendPreNetwork(controller, movement, hidden);
public void SendPostNetwork(
PlayerMovementController controller,
bool hidden) => sendPostNetwork(controller, hidden);
}
private sealed class TestMovementInputSource(Func<MovementInput> capture)
: IMovementInputSource
{