refactor(update): cut over the frame orchestrator
Reduce GameWindow.OnUpdate to one typed orchestration call and remove transitive window callbacks from teardown, liveness, teleport placement, live presentation, auto-entry, and entity packet routing. Preserve the frozen retail object/network order while making the production owner graph explicit and testable.
This commit is contained in:
parent
947c61d2d7
commit
e91f310279
18 changed files with 864 additions and 348 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -1594,9 +1595,14 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
public readonly RecordingOrigin Origin;
|
||||
public readonly RecordingNetworkSink Network = new();
|
||||
public readonly RecordingTeardownCoordinator Teardown = new();
|
||||
public readonly RecordingTimestampPublisher Timestamps;
|
||||
public readonly LiveEntityHydrationController Controller;
|
||||
|
||||
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction { get; set; }
|
||||
public Action<uint, AcceptedPhysicsTimestamps>? PublishTimestampsAction
|
||||
{
|
||||
get => Timestamps.PublishAction;
|
||||
set => Timestamps.PublishAction = value;
|
||||
}
|
||||
|
||||
public Fixture(
|
||||
bool originKnown,
|
||||
|
|
@ -1617,6 +1623,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Relationships = new RecordingRelationships(Operations);
|
||||
Ready = new RecordingReadyPublisher(Operations);
|
||||
Origin = new RecordingOrigin(originKnown);
|
||||
Timestamps = new RecordingTimestampPublisher(Operations);
|
||||
Network.ApplyAction = events =>
|
||||
{
|
||||
if (events.Position is not { } position)
|
||||
|
|
@ -1631,6 +1638,15 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
out _,
|
||||
out _);
|
||||
};
|
||||
var identity = new LocalPlayerIdentityState
|
||||
{
|
||||
ServerGuid = playerGuid,
|
||||
};
|
||||
var deletion = new LiveEntityDeletionController(
|
||||
Runtime,
|
||||
Objects,
|
||||
Teardown,
|
||||
identity);
|
||||
Controller = new LiveEntityHydrationController(
|
||||
Runtime,
|
||||
Objects,
|
||||
|
|
@ -1640,13 +1656,9 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Ready,
|
||||
Origin,
|
||||
Network,
|
||||
Teardown,
|
||||
(guid, timestamps) =>
|
||||
{
|
||||
Operations.Add($"timestamps:{timestamps.Instance}");
|
||||
PublishTimestampsAction?.Invoke(guid, timestamps);
|
||||
},
|
||||
() => playerGuid);
|
||||
Timestamps,
|
||||
identity,
|
||||
deletion);
|
||||
}
|
||||
|
||||
public LiveEntityRecord Record
|
||||
|
|
@ -1671,6 +1683,23 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingTimestampPublisher
|
||||
: IAcceptedLocalPhysicsTimestampPublisher
|
||||
{
|
||||
private readonly List<string> _operations;
|
||||
|
||||
public RecordingTimestampPublisher(List<string> operations) =>
|
||||
_operations = operations;
|
||||
|
||||
public Action<uint, AcceptedPhysicsTimestamps>? PublishAction { get; set; }
|
||||
|
||||
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
_operations.Add($"timestamps:{timestamps.Instance}");
|
||||
PublishAction?.Invoke(serverGuid, timestamps);
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordingResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public int RegisterCount { get; protected set; }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
|
@ -59,4 +60,17 @@ public sealed class LiveWorldOriginStateTests
|
|||
Assert.Equal(50, state.CenterX);
|
||||
Assert.Equal(51, state.CenterY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellLocalForSeed_UsesTheCurrentLandblockOrigin()
|
||||
{
|
||||
var state = new LiveWorldOriginState();
|
||||
state.SetPlaceholder(0x30, 0x32);
|
||||
|
||||
Vector3 local = state.CellLocalForSeed(
|
||||
new Vector3(200f, -300f, 5f),
|
||||
0x3130_0001u);
|
||||
|
||||
Assert.Equal(new Vector3(8f, 84f, 5f), local);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
[Fact]
|
||||
public void ConditionalReconciles_RemainAtTheirOwningEdges()
|
||||
{
|
||||
// Checkpoint A freezes only the outer ownership contract. Checkpoints
|
||||
// E and F must replace these probes with production teleport/camera
|
||||
// owner tests before the conditional edges can be claimed guarded.
|
||||
// The outer ownership oracle is complemented by the production
|
||||
// teleport and camera tests that pin both conditional reconcile edges.
|
||||
var calls = new List<string>();
|
||||
UpdateFrameOrchestrator frame = Create(
|
||||
calls,
|
||||
|
|
@ -228,19 +227,6 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
FieldInfo[] ownerFields = owner.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
||||
if (owner == typeof(AcDream.App.Input.RetailLocalPlayerFrameController))
|
||||
{
|
||||
// Checkpoint D replaced input capture with its typed owner.
|
||||
// Checkpoint F still owns the remaining player presentation
|
||||
// callback composition. No other phase owner may add one.
|
||||
Assert.Contains(
|
||||
ownerFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
Assert.DoesNotContain(
|
||||
ownerFields,
|
||||
field => field.FieldType == typeof(Func<AcDream.App.Input.MovementInput>));
|
||||
continue;
|
||||
}
|
||||
Assert.DoesNotContain(
|
||||
ownerFields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
|
|
@ -277,14 +263,24 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
[Fact]
|
||||
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
|
||||
{
|
||||
string root = FindRepoRoot();
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
string adapters = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Update",
|
||||
"UpdateFrameRuntimeAdapters.cs"));
|
||||
|
||||
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
||||
Assert.Equal(0, CountOccurrences(source, "PublishTime("));
|
||||
Assert.Equal(1, CountOccurrences(adapters, "_runner.PublishTime("));
|
||||
Assert.Contains("new AcDream.App.Update.PhysicsScriptClockPublisher(", source,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -363,12 +359,10 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"GameWindow.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Streaming.StreamingFrameController(", source);
|
||||
Assert.Equal(1, CountOccurrences(source, "_streamingFrame.Tick();"));
|
||||
AssertAppearsInOrder(
|
||||
Assert.DoesNotContain("_streamingFrame", source, StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_streamingFrame.Tick();",
|
||||
"_gameplayInputFrame!.Tick(frameTiming);",
|
||||
"_liveFrameCoordinator.Tick(frameDelta);");
|
||||
"_updateFrameOrchestrator.Tick("));
|
||||
Assert.DoesNotContain("_streamingController.Tick(observerCx", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("DungeonStreamingGate.Compute", source,
|
||||
|
|
@ -387,7 +381,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"GameWindow.cs"));
|
||||
|
||||
Assert.Contains("new AcDream.App.Input.GameplayInputFrameController(", source);
|
||||
Assert.Equal(1, CountOccurrences(source, "_gameplayInputFrame!.Tick(frameTiming);"));
|
||||
Assert.DoesNotContain("_gameplayInputFrame!.Tick", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_inputDispatcher?.Tick()", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("TryTakeRawSample", source,
|
||||
|
|
@ -468,9 +463,8 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
||||
source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_localPlayerTeleport!.Tick(frameDelta);"));
|
||||
Assert.DoesNotContain("_localPlayerTeleport!.Tick", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportTransit", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportAnim", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_teleportViewPlane", source, StringComparison.Ordinal);
|
||||
|
|
@ -486,9 +480,10 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_liveEntityLiveness?.Tick(ClientTimerNow());",
|
||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
||||
"_playerModeAutoEntry?.TryEnter();");
|
||||
"new AcDream.App.Update.LiveEntityLivenessFramePhase(",
|
||||
"_localPlayerTeleport,",
|
||||
"new AcDream.App.Update.PlayerModeAutoEntryFramePhase(",
|
||||
"cameraFrame);");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -649,7 +644,9 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
Assert.Equal(1, CountOccurrences(source, "_cameraFrame.Tick(frameTiming);"));
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_updateFrameOrchestrator.Tick("));
|
||||
Assert.DoesNotContain("CanAdvanceLocalPlayer", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("GetCombatCameraTargetPoint()", source,
|
||||
StringComparison.Ordinal);
|
||||
|
|
@ -657,11 +654,7 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_localPlayerFrame.TryGetPresentationAfterNetwork", source,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"_localPlayerTeleport!.Tick(frameDelta);",
|
||||
"_playerModeAutoEntry?.TryEnter();",
|
||||
"_cameraFrame.Tick(frameTiming);");
|
||||
Assert.DoesNotContain("_cameraFrame", source, StringComparison.Ordinal);
|
||||
|
||||
string cameraSource = File.ReadAllText(Path.Combine(
|
||||
root,
|
||||
|
|
@ -677,6 +670,123 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"retail?.Update(");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindow_OnUpdateOwnsOnlyProfilingAndOneOrchestratorTick()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
Assert.Equal(1, CountOccurrences(
|
||||
source,
|
||||
"_updateFrameOrchestrator.Tick("));
|
||||
Assert.DoesNotContain("_updateFrameClock.Advance(", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveFrameCoordinator", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_liveEntityLiveness?.Tick", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_playerModeAutoEntry?.TryEnter", source,
|
||||
StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"private void OnUpdate(double dt)",
|
||||
"_frameProfiler.BeginStage(",
|
||||
"_updateFrameOrchestrator.Tick(",
|
||||
"private void OnCameraModeChanged");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks()
|
||||
{
|
||||
Type[] adapters =
|
||||
[
|
||||
typeof(LiveEntityTeardownFramePhase),
|
||||
typeof(ConsoleUpdateFrameFailureSink),
|
||||
typeof(PhysicsScriptClockPublisher),
|
||||
typeof(LiveEntityLivenessFramePhase),
|
||||
typeof(PlayerModeAutoEntryFramePhase),
|
||||
];
|
||||
|
||||
foreach (Type adapter in adapters)
|
||||
{
|
||||
FieldInfo[] fields = adapter.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
||||
Assert.DoesNotContain(
|
||||
fields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
}
|
||||
|
||||
FieldInfo clock = Assert.Single(
|
||||
typeof(LiveEntityLivenessFramePhase).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.Name == "_clock");
|
||||
Assert.Equal(typeof(IClientMonotonicTimeSource), clock.FieldType);
|
||||
|
||||
FieldInfo teardownRuntime = Assert.Single(
|
||||
typeof(LiveEntityTeardownFramePhase).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
Assert.Equal(typeof(LiveEntityRuntime), teardownRuntime.FieldType);
|
||||
|
||||
Type[] typedProductionOwners =
|
||||
[
|
||||
typeof(LiveEntityLivenessController),
|
||||
typeof(AcDream.App.Input.LivePlayerModeAutoEntryContext),
|
||||
typeof(LiveSessionLocalPhysicsTimestampPublisher),
|
||||
typeof(AcDream.App.Physics.LiveEntityNetworkUpdateController),
|
||||
typeof(AcDream.App.Rendering.LiveEntityPartArrayLifecycle),
|
||||
typeof(AcDream.App.Physics.RemoteShadowPlacementSynchronizer),
|
||||
typeof(AcDream.App.Physics.RemoteTeleportPlacementPresentation),
|
||||
typeof(AcDream.App.Net.LiveEntitySessionController),
|
||||
];
|
||||
foreach (Type owner in typedProductionOwners)
|
||||
{
|
||||
FieldInfo[] fields = owner.GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
||||
Assert.DoesNotContain(
|
||||
fields,
|
||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||
}
|
||||
|
||||
FieldInfo originIdentity = Assert.Single(
|
||||
typeof(LiveEntityWorldOriginCoordinator).GetFields(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
field => field.Name == "_identity");
|
||||
Assert.Equal(
|
||||
typeof(AcDream.App.Input.ILocalPlayerIdentitySource),
|
||||
originIdentity.FieldType);
|
||||
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LoginWorldReady", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("candidate => _liveEntityHydration.OnPrune", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("CreateLiveEntitySessionSink", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("private System.Numerics.Vector3 CellLocalForSeed", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("OnPlayScriptReceived", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.GetCenter", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveWorldOrigin.CellLocalForSeed", source,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_liveEntitySessionEvents.CreateSink()", source,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static UpdateFrameOrchestrator Create(
|
||||
List<string> calls,
|
||||
RecordingTeardown? teardown = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue