diff --git a/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md b/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md
index 7cef3403..832c15ec 100644
--- a/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md
+++ b/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md
@@ -27,7 +27,7 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
- [x] F — extract the shared player-mode lifecycle plus fly/chase/player
camera presentation and cut production over to one
`UpdateFrameOrchestrator`.
-- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
+- [x] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
frame state; run focused corrected-diff reviews after each ownership edge.
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
resource soak, documentation, durable memory, and line/field/method closeout.
@@ -469,6 +469,19 @@ passed / 5 skipped. All three corrected-diff reviews are clean.
`GameWindow.cs` is 7,160 lines, down 8,563 lines (54.5%) from the 15,723-line
slice baseline.
+**Checkpoint G completed 2026-07-22.** `GameWindow.OnUpdate` now owns only the
+update profiler scope and one `UpdateFrameOrchestrator.Tick` call. Teardown,
+absolute liveness time, local timestamp publication, expiry deletion,
+player-mode auto-entry, live-origin initialization, Hidden PartArray
+boundaries, remote teleport placement, and all twelve live-entity session
+packet routes resolve through typed runtime owners; the former transitive
+window callbacks and obsolete frame fields are gone. The App suite is green
+at 2,823 passed / 3 skipped. Focused orchestration, lifecycle, presentation,
+teleport, origin, and session tests are green, and all three corrected-diff
+reviews are clean. `GameWindow.cs` is 7,026 raw lines / 241 fields / 108
+methods, down 1,785 lines and 45 methods from the Slice-6 baseline and 8,697
+lines (55.3%) from the 15,723-line pre-extraction class.
+
### H — release and connected gates
After all three independent corrected-diff reviews are clean:
diff --git a/src/AcDream.App/Input/PlayerModeAutoEntry.cs b/src/AcDream.App/Input/PlayerModeAutoEntry.cs
index a23b20cc..cdf0837a 100644
--- a/src/AcDream.App/Input/PlayerModeAutoEntry.cs
+++ b/src/AcDream.App/Input/PlayerModeAutoEntry.cs
@@ -1,11 +1,14 @@
using System;
+using AcDream.App.Net;
+using AcDream.App.Streaming;
+using AcDream.App.World;
namespace AcDream.App.Input;
///
/// Phase K.2 — one-shot guard that auto-enters player mode after a
-/// successful login once every prerequisite is satisfied. Owned by
-/// GameWindow and ticked each frame from OnUpdate.
+/// successful login once every prerequisite is satisfied. The update-frame
+/// orchestrator ticks it through a typed production context.
///
///
/// Why is this its own class? The auto-entry has four independent
@@ -33,20 +36,108 @@ namespace AcDream.App.Input;
///
///
///
-/// All preconditions are passed in as predicates so the class doesn't
-/// pull in WorldSession, PlayerMovementController, or
-/// any GameWindow-internal types — the unit test wires them to plain
-/// boolean fields.
+/// Production preconditions come from focused runtime owners rather than the
+/// window host. The public delegate constructor remains a narrow test seam for
+/// plain boolean fixtures.
///
///
+internal interface IPlayerModeAutoEntryContext
+{
+ bool IsLiveInWorld { get; }
+ bool IsPlayerEntityPresent { get; }
+ bool IsPlayerControllerReady { get; }
+ bool IsWorldReady { get; }
+ bool IsPlayerModeActive { get; }
+ void EnterPlayerMode();
+}
+
+/// Production auto-entry context over canonical runtime owners.
+internal sealed class LivePlayerModeAutoEntryContext
+ : IPlayerModeAutoEntryContext
+{
+ private readonly ILiveInWorldSource _session;
+ private readonly LiveEntityRuntime _liveEntities;
+ private readonly ILocalPlayerIdentitySource _identity;
+ private readonly WorldRevealCoordinator _worldReveal;
+ private readonly ILocalPlayerModeSource _mode;
+ private readonly PlayerModeController _playerMode;
+
+ public LivePlayerModeAutoEntryContext(
+ ILiveInWorldSource session,
+ LiveEntityRuntime liveEntities,
+ ILocalPlayerIdentitySource identity,
+ WorldRevealCoordinator worldReveal,
+ ILocalPlayerModeSource mode,
+ PlayerModeController playerMode)
+ {
+ _session = session ?? throw new ArgumentNullException(nameof(session));
+ _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
+ _worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
+ _mode = mode ?? throw new ArgumentNullException(nameof(mode));
+ _playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
+ }
+
+ public bool IsLiveInWorld => _session.IsInWorld;
+
+ public bool IsPlayerEntityPresent =>
+ _liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
+
+ public bool IsPlayerControllerReady => true;
+
+ public bool IsWorldReady =>
+ _liveEntities.TryGetSnapshot(
+ _identity.ServerGuid,
+ out AcDream.Core.Net.WorldSession.EntitySpawn player)
+ && player.Position is { LandblockId: not 0u } position
+ && _worldReveal.Evaluate(position.LandblockId).IsReady;
+
+ public bool IsPlayerModeActive => _mode.IsPlayerMode;
+
+ public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
+}
+
public sealed class PlayerModeAutoEntry
{
- private readonly Func _isLiveInWorld;
- private readonly Func _isPlayerEntityPresent;
- private readonly Func _isPlayerControllerReady;
- private readonly Func _isWorldReady;
- private readonly Func _isPlayerModeActive;
- private readonly Action _enterPlayerMode;
+ private sealed class DelegateContext : IPlayerModeAutoEntryContext
+ {
+ private readonly Func _isLiveInWorld;
+ private readonly Func _isPlayerEntityPresent;
+ private readonly Func _isPlayerControllerReady;
+ private readonly Func _isWorldReady;
+ private readonly Action _enterPlayerMode;
+ private readonly Func _isPlayerModeActive;
+
+ public DelegateContext(
+ Func isLiveInWorld,
+ Func isPlayerEntityPresent,
+ Func isPlayerControllerReady,
+ Func isWorldReady,
+ Action enterPlayerMode,
+ Func? isPlayerModeActive)
+ {
+ _isLiveInWorld = isLiveInWorld
+ ?? throw new ArgumentNullException(nameof(isLiveInWorld));
+ _isPlayerEntityPresent = isPlayerEntityPresent
+ ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
+ _isPlayerControllerReady = isPlayerControllerReady
+ ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
+ _isWorldReady = isWorldReady
+ ?? throw new ArgumentNullException(nameof(isWorldReady));
+ _enterPlayerMode = enterPlayerMode
+ ?? throw new ArgumentNullException(nameof(enterPlayerMode));
+ _isPlayerModeActive = isPlayerModeActive ?? (() => false);
+ }
+
+ public bool IsLiveInWorld => _isLiveInWorld();
+ public bool IsPlayerEntityPresent => _isPlayerEntityPresent();
+ public bool IsPlayerControllerReady => _isPlayerControllerReady();
+ public bool IsWorldReady => _isWorldReady();
+ public bool IsPlayerModeActive => _isPlayerModeActive();
+ public void EnterPlayerMode() => _enterPlayerMode();
+ }
+
+ private readonly IPlayerModeAutoEntryContext _context;
private bool _armed;
@@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
/// player transition). Must construct the controller + chase
/// camera and switch the active camera; the auto-entry doesn't
/// reach inside.
+ internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+
public PlayerModeAutoEntry(
Func isLiveInWorld,
Func isPlayerEntityPresent,
Func isPlayerControllerReady,
Func isWorldReady,
- Action enterPlayerMode,
+ Action enterPlayerMode,
Func? isPlayerModeActive = null)
+ : this(new DelegateContext(
+ isLiveInWorld,
+ isPlayerEntityPresent,
+ isPlayerControllerReady,
+ isWorldReady,
+ enterPlayerMode,
+ isPlayerModeActive))
{
- _isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
- _isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
- _isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
- _isWorldReady = isWorldReady ?? throw new ArgumentNullException(nameof(isWorldReady));
- _enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
- _isPlayerModeActive = isPlayerModeActive ?? (() => false);
}
/// True iff would still fire if the
@@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
public bool TryEnter()
{
if (!_armed) return false;
- if (_isPlayerModeActive())
+ if (_context.IsPlayerModeActive)
{
_armed = false;
return false;
}
- if (!_isLiveInWorld()) return false;
- if (!_isPlayerEntityPresent()) return false;
- if (!_isPlayerControllerReady()) return false;
- if (!_isWorldReady()) return false;
+ if (!_context.IsLiveInWorld) return false;
+ if (!_context.IsPlayerEntityPresent) return false;
+ if (!_context.IsPlayerControllerReady) return false;
+ if (!_context.IsWorldReady) return false;
_armed = false;
- _enterPlayerMode();
+ _context.EnterPlayerMode();
return true;
}
}
diff --git a/src/AcDream.App/Net/LiveEntitySessionController.cs b/src/AcDream.App/Net/LiveEntitySessionController.cs
new file mode 100644
index 00000000..473a08f7
--- /dev/null
+++ b/src/AcDream.App/Net/LiveEntitySessionController.cs
@@ -0,0 +1,97 @@
+using AcDream.App.Physics;
+using AcDream.App.Rendering.Vfx;
+using AcDream.App.Streaming;
+using AcDream.App.World;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.App.Net;
+
+///
+/// Routes one live session's entity packets through the non-reentrant retail
+/// inbound envelope without retaining the window host.
+///
+internal sealed class LiveEntitySessionController
+{
+ private readonly RetailInboundEventDispatcher _inbound;
+ private readonly LiveEntityHydrationController _hydration;
+ private readonly LiveEntityNetworkUpdateController _updates;
+ private readonly ILocalPlayerTeleportNetworkSink _teleport;
+ private readonly EntityEffectController _effects;
+
+ public LiveEntitySessionController(
+ RetailInboundEventDispatcher inbound,
+ LiveEntityHydrationController hydration,
+ LiveEntityNetworkUpdateController updates,
+ ILocalPlayerTeleportNetworkSink teleport,
+ EntityEffectController effects)
+ {
+ _inbound = inbound ?? throw new ArgumentNullException(nameof(inbound));
+ _hydration = hydration ?? throw new ArgumentNullException(nameof(hydration));
+ _updates = updates ?? throw new ArgumentNullException(nameof(updates));
+ _teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
+ _effects = effects ?? throw new ArgumentNullException(nameof(effects));
+ }
+
+ public LiveEntitySessionSink CreateSink() => new(
+ OnSpawned,
+ OnDeleted,
+ OnPickedUp,
+ OnMotionUpdated,
+ OnPositionUpdated,
+ OnVectorUpdated,
+ OnStateUpdated,
+ OnParentUpdated,
+ OnTeleportStarted,
+ OnAppearanceUpdated,
+ OnPlayPhysicsScript,
+ OnPlayPhysicsScriptType);
+
+ private void OnSpawned(WorldSession.EntitySpawn value) =>
+ _inbound.Run(_hydration, value,
+ static (owner, message) => owner.OnCreate(message));
+
+ private void OnDeleted(DeleteObject.Parsed value) =>
+ _inbound.Run(_hydration, value,
+ static (owner, message) => owner.OnDelete(message));
+
+ private void OnPickedUp(PickupEvent.Parsed value) =>
+ _inbound.Run(_hydration, value,
+ static (owner, message) => owner.OnPickup(message));
+
+ private void OnMotionUpdated(WorldSession.EntityMotionUpdate value) =>
+ _inbound.Run(_updates, value,
+ static (owner, message) => owner.OnMotion(message));
+
+ private void OnPositionUpdated(WorldSession.EntityPositionUpdate value) =>
+ _inbound.Run(_updates, value,
+ static (owner, message) => owner.OnPosition(message));
+
+ private void OnVectorUpdated(VectorUpdate.Parsed value) =>
+ _inbound.Run(_updates, value,
+ static (owner, message) => owner.OnVector(message));
+
+ private void OnStateUpdated(SetState.Parsed value) =>
+ _inbound.Run(_updates, value,
+ static (owner, message) => owner.OnState(message));
+
+ private void OnParentUpdated(ParentEvent.Parsed value) =>
+ _inbound.Run(_hydration, value,
+ static (owner, message) => owner.OnParent(message));
+
+ private void OnTeleportStarted(uint value) =>
+ _inbound.Run(_teleport, value,
+ static (owner, message) => owner.OnTeleportStarted(message));
+
+ private void OnAppearanceUpdated(ObjDescEvent.Parsed value) =>
+ _inbound.Run(_hydration, value,
+ static (owner, message) => owner.OnAppearance(message));
+
+ private void OnPlayPhysicsScript(PlayPhysicsScript value) =>
+ _inbound.Run(_effects, value,
+ static (owner, message) => owner.HandleDirect(message));
+
+ private void OnPlayPhysicsScriptType(PlayPhysicsScriptType value) =>
+ _inbound.Run(_effects, value,
+ static (owner, message) => owner.HandleTyped(message));
+}
diff --git a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs
index 41d65415..603e22db 100644
--- a/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs
+++ b/src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs
@@ -1,6 +1,7 @@
using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
+using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
@@ -50,18 +51,18 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport;
- private readonly Func _playerControllerSource;
+ private readonly ILocalPlayerControllerSource _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
- private readonly Func _playerHostSource;
- private readonly Func _playerGuid;
+ private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
+ private readonly ILocalPlayerIdentitySource _playerIdentity;
private readonly IPhysicsScriptTimeSource _gameTime;
- private readonly Func _session;
+ private readonly ILiveWorldSessionSource _session;
private readonly LiveEntityInboundAuthorityGate _authorityGate;
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
- private PlayerMovementController? _playerController => _playerControllerSource();
- private EntityPhysicsHost? _playerHost => _playerHostSource();
- private uint _playerServerGuid => _playerGuid();
+ private PlayerMovementController? _playerController => _playerControllerSource.Controller;
+ private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
+ private uint _playerServerGuid => _playerIdentity.ServerGuid;
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
private IReadOnlyDictionary _entitiesByServerGuid =>
_liveEntities.MaterializedWorldEntities;
@@ -96,12 +97,12 @@ internal sealed class LiveEntityNetworkUpdateController
CombatTargetController? combatTargetController,
LiveWorldOriginState origin,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
- Func playerControllerSource,
+ ILocalPlayerControllerSource playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound,
- Func playerHostSource,
- Func playerGuid,
+ ILocalPlayerPhysicsHostSource playerHostSource,
+ ILocalPlayerIdentitySource playerIdentity,
IPhysicsScriptTimeSource gameTime,
- Func session,
+ ILiveWorldSessionSource session,
Action publishTimestamps,
IMovementTruthDiagnosticSink movementTruthDiagnostics)
{
@@ -131,7 +132,7 @@ internal sealed class LiveEntityNetworkUpdateController
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
- _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
+ _playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity));
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
_session = session ?? throw new ArgumentNullException(nameof(session));
_authorityGate = new LiveEntityInboundAuthorityGate(
@@ -1029,7 +1030,7 @@ internal sealed class LiveEntityNetworkUpdateController
p.PositionY,
p.PositionZ)),
() => _localPlayerOutbound.SendImmediatePosition(
- _session(),
+ _session.CurrentSession,
_playerController)))
return;
diff --git a/src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs b/src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
new file mode 100644
index 00000000..891f4690
--- /dev/null
+++ b/src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
@@ -0,0 +1,49 @@
+using AcDream.App.World;
+using AcDream.Core.Physics;
+using AcDream.Core.World;
+
+namespace AcDream.App.Physics;
+
+/// Synchronizes a resolved remote placement in the current live origin.
+internal sealed class RemoteShadowPlacementSynchronizer
+{
+ private readonly RemotePhysicsUpdater _remotePhysics;
+ private readonly LiveWorldOriginState _origin;
+
+ public RemoteShadowPlacementSynchronizer(
+ RemotePhysicsUpdater remotePhysics,
+ LiveWorldOriginState origin)
+ {
+ _remotePhysics = remotePhysics
+ ?? throw new ArgumentNullException(nameof(remotePhysics));
+ _origin = origin ?? throw new ArgumentNullException(nameof(origin));
+ }
+
+ public void Sync(WorldEntity entity, PhysicsBody body, uint cellId) =>
+ _remotePhysics.SyncRemoteShadowToBody(
+ entity.Id,
+ body,
+ _origin.CenterX,
+ _origin.CenterY,
+ cellId);
+}
+
+/// Bridges remote placement ownership to live presentation state.
+internal sealed class RemoteTeleportPlacementPresentation
+{
+ private readonly LiveEntityPresentationController _presentation;
+
+ public RemoteTeleportPlacementPresentation(
+ LiveEntityPresentationController presentation) =>
+ _presentation = presentation
+ ?? throw new ArgumentNullException(nameof(presentation));
+
+ public void Complete(uint serverGuid, ushort generation, bool deferShadowRestore) =>
+ _presentation.CompleteAuthoritativePlacement(
+ serverGuid,
+ generation,
+ deferShadowRestore);
+
+ public void Begin(uint serverGuid, ushort generation) =>
+ _presentation.BeginAuthoritativePlacement(serverGuid, generation);
+}
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 3e8f2ded..87a119c0 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -134,7 +134,6 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Streaming.StreamingController? _streamingController;
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
_streamingOriginRecenter;
- private AcDream.App.Update.IStreamingFramePhase _streamingFrame = null!;
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
_localPlayerTeleportSink = new();
@@ -173,10 +172,7 @@ public sealed class GameWindow : IDisposable
private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics;
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
- private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!;
- private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
- private AcDream.App.Update.LiveSpatialPresentationReconciler
- _liveSpatialReconciler = null!;
+ private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
// Step 7 projectile presentation. The controller owns no identity map;
@@ -186,6 +182,7 @@ public sealed class GameWindow : IDisposable
_liveEntityProjectionWithdrawal;
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
+ private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
_liveEntityMotionBindings = new();
@@ -2170,36 +2167,32 @@ public sealed class GameWindow : IDisposable
_entityEffects = entityEffects;
entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}");
+ var partArrayLifecycle =
+ new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
+ _animatedEntities);
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
_liveEntities,
_physicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition,
_equippedChildRenderer.SetDirectChildrenNoDraw,
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
- () => (_liveCenterX, _liveCenterY),
- handlePartArrayEnterWorld: localEntityId =>
- {
- if (_animatedEntities.TryGetValue(localEntityId, out var animated))
- animated.Sequencer?.Manager.HandleEnterWorld();
- });
+ _liveWorldOrigin.GetCenter,
+ handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
+ var remoteShadowPlacement =
+ new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
+ _remotePhysicsUpdater,
+ _liveWorldOrigin);
+ var remoteTeleportPresentation =
+ new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
+ _liveEntityPresentation);
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
_physicsEngine,
_liveEntities,
_liveEntityMotionBindings.GetSetupCylinder,
- CellLocalForSeed,
- (entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody(
- entity.Id,
- remote,
- _liveCenterX,
- _liveCenterY,
- cellId),
- (guid, generation, deferShadowRestore) =>
- _liveEntityPresentation.CompleteAuthoritativePlacement(
- guid,
- generation,
- deferShadowRestore),
- (guid, generation) =>
- _liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
+ _liveWorldOrigin.CellLocalForSeed,
+ remoteShadowPlacement.Sync,
+ remoteTeleportPresentation.Complete,
+ remoteTeleportPresentation.Begin);
_equippedChildRenderer.ProjectionPoseReady += guid =>
_liveEntityLights.OnAttachedPoseReady(guid);
_hookRouter.Register(entityEffects);
@@ -2484,9 +2477,14 @@ public sealed class GameWindow : IDisposable
_streamingController,
_worldState,
_worldReveal,
- () => _playerServerGuid,
+ _localPlayerIdentity,
sealedDungeonCells,
Console.WriteLine);
+ _liveSessionController = new AcDream.App.Net.LiveSessionController();
+ var localPhysicsTimestamps =
+ new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
+ _localPlayerIdentity,
+ _liveSessionController);
var liveEntityTeardown =
new AcDream.App.World.LiveEntityRuntimeTeardownController(
_liveEntities,
@@ -2503,8 +2501,15 @@ public sealed class GameWindow : IDisposable
_physicsEngine.ShadowObjects,
_liveEntityLights!,
_classificationCache,
- () => _playerServerGuid);
+ _localPlayerIdentity);
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
+ var liveEntityDeletion =
+ new AcDream.App.World.LiveEntityDeletionController(
+ _liveEntities,
+ Objects,
+ liveEntityTeardown,
+ _localPlayerIdentity,
+ _options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
_liveEntities,
Objects,
@@ -2518,9 +2523,9 @@ public sealed class GameWindow : IDisposable
_liveEntityPresentation!),
originCoordinator,
networkUpdateBridge,
- liveEntityTeardown,
- PublishLocalPhysicsTimestamps,
- () => _playerServerGuid,
+ localPhysicsTimestamps,
+ _localPlayerIdentity,
+ liveEntityDeletion,
_options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityNetworkUpdates =
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
@@ -2546,18 +2551,24 @@ public sealed class GameWindow : IDisposable
_combatTargetController,
_liveWorldOrigin,
_localPlayerTeleportSink,
- () => _playerController,
+ _playerControllerSlot,
_localPlayerOutbound,
- () => _playerHost,
- () => _playerServerGuid,
+ _playerHostSlot,
+ _localPlayerIdentity,
_updateFrameClock,
- () => LiveSession,
- PublishLocalPhysicsTimestamps,
+ _liveSessionController,
+ localPhysicsTimestamps.Publish,
_movementTruthDiagnostics);
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
_liveEntities,
- () => _playerServerGuid,
- candidate => _liveEntityHydration.OnPrune(candidate));
+ _localPlayerIdentity,
+ liveEntityDeletion);
+ _liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
+ _inboundEntityEvents,
+ _liveEntityHydration,
+ _liveEntityNetworkUpdates,
+ _localPlayerTeleportSink,
+ _entityEffects!);
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
_equippedChildRenderer.EntityReady += candidate =>
@@ -2573,7 +2584,6 @@ public sealed class GameWindow : IDisposable
// CreateObject messages into _worldGameState as they arrive. Entirely
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
- _liveSessionController = new AcDream.App.Net.LiveSessionController();
_combatAttackOperations.Bind(
new AcDream.App.Combat.LiveCombatAttackOperations(
Combat,
@@ -2609,7 +2619,7 @@ public sealed class GameWindow : IDisposable
mouseLookController,
new AcDream.App.Input.CombatAttackInputFrameAdapter(
_combatAttackController!));
- _streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
+ var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
_options.LiveMode,
_localPlayerMode,
_playerControllerSlot,
@@ -2638,7 +2648,7 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Update.SettingsParticleRangeSource(
_settingsVm,
_persistedDisplay.ParticleRange));
- _liveSpatialReconciler =
+ var liveSpatialReconciler =
new AcDream.App.Update.LiveSpatialPresentationReconciler(
_entityEffects!,
_equippedChildRenderer!,
@@ -2721,16 +2731,15 @@ public sealed class GameWindow : IDisposable
_localPlayerSkills,
_viewportAspect);
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
- isLiveInWorld: () => _liveSessionController.IsInWorld,
- isPlayerEntityPresent: () =>
- _liveEntities.MaterializedWorldEntities.ContainsKey(
- _playerServerGuid),
- isPlayerControllerReady: () => true,
- // Retail SmartBox::UseTime @ 0x00455410 completes player
- // position only after the destination cell load clears.
- isWorldReady: LoginWorldReady,
- enterPlayerMode: _playerModeController.EnterFromAutoEntry,
- isPlayerModeActive: () => _localPlayerMode.IsPlayerMode);
+ new AcDream.App.Input.LivePlayerModeAutoEntryContext(
+ _liveSessionController,
+ _liveEntities,
+ _localPlayerIdentity,
+ _worldReveal
+ ?? throw new InvalidOperationException(
+ "World reveal was not composed before player auto-entry."),
+ _localPlayerMode,
+ _playerModeController));
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
var localTeleportPresentation =
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
@@ -2758,7 +2767,7 @@ public sealed class GameWindow : IDisposable
_playerHostSlot,
_chaseCameraInput,
_liveWorldOrigin,
- _liveSpatialReconciler),
+ liveSpatialReconciler),
new AcDream.App.Streaming.LocalPlayerTeleportSession(
_liveSessionController),
localTeleportPresentation);
@@ -2767,25 +2776,41 @@ public sealed class GameWindow : IDisposable
// remains only as the staged-startup fallback used by shutdown when
// composition fails before the transfer.
_portalTunnel = null;
- _liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
+ var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame,
_worldState,
_liveSessionController,
localPlayerFrame,
- _liveSpatialReconciler);
- _cameraFrame = new AcDream.App.Rendering.CameraFrameController(
+ liveSpatialReconciler);
+ var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
_cameraController!,
_inputCapture,
_cameraInput,
localPlayerFrameRuntime,
_chaseCameraInput,
localPlayerFrame,
- _liveSpatialReconciler,
+ liveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
_gameplaySettings,
Combat,
_selection,
_worldSelectionQuery!));
+ _updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
+ new AcDream.App.Update.LiveEntityTeardownFramePhase(
+ _liveEntities),
+ new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
+ _updateFrameClock,
+ new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
+ streamingFrame,
+ _gameplayInputFrame,
+ liveFrameCoordinator,
+ new AcDream.App.Update.LiveEntityLivenessFramePhase(
+ _liveEntityLiveness,
+ new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
+ _localPlayerTeleport,
+ new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
+ _playerModeAutoEntry),
+ cameraFrame);
AcDream.App.Net.LiveSessionStartResult liveStart =
_liveSessionController.Start(
_options,
@@ -2958,7 +2983,7 @@ public sealed class GameWindow : IDisposable
{
events = new AcDream.App.Net.LiveSessionEventRouter(
session,
- CreateLiveEntitySessionSink(),
+ _liveEntitySessionEvents.CreateSink(),
new AcDream.App.Net.LiveEnvironmentSessionSink(
OnEnvironChanged,
ticks =>
@@ -2987,44 +3012,6 @@ public sealed class GameWindow : IDisposable
}
}
- private AcDream.App.Net.LiveEntitySessionSink CreateLiveEntitySessionSink() => new(
- Spawned: spawn => _inboundEntityEvents.Run(
- _liveEntityHydration!,
- spawn,
- static (hydration, value) => hydration.OnCreate(value)),
- Deleted: deletion => _inboundEntityEvents.Run(
- _liveEntityHydration!, deletion,
- static (hydration, value) => hydration.OnDelete(value)),
- PickedUp: pickup => _inboundEntityEvents.Run(
- _liveEntityHydration!, pickup,
- static (hydration, value) => hydration.OnPickup(value)),
- MotionUpdated: motion => _inboundEntityEvents.Run(
- _liveEntityNetworkUpdates!, motion,
- static (updates, value) => updates.OnMotion(value)),
- PositionUpdated: position => _inboundEntityEvents.Run(
- _liveEntityNetworkUpdates!, position,
- static (updates, value) => updates.OnPosition(value)),
- VectorUpdated: vector => _inboundEntityEvents.Run(
- _liveEntityNetworkUpdates!, vector,
- static (updates, value) => updates.OnVector(value)),
- StateUpdated: state => _inboundEntityEvents.Run(
- _liveEntityNetworkUpdates!, state,
- static (updates, value) => updates.OnState(value)),
- ParentUpdated: parent => _inboundEntityEvents.Run(
- _liveEntityHydration!, parent,
- static (hydration, value) => hydration.OnParent(value)),
- TeleportStarted: teleport => _inboundEntityEvents.Run(
- _localPlayerTeleportSink,
- teleport,
- static (sink, value) => sink.OnTeleportStarted(value)),
- AppearanceUpdated: appearance => _inboundEntityEvents.Run(
- _liveEntityHydration!, appearance,
- static (hydration, value) => hydration.OnAppearance(value)),
- PlayPhysicsScript: script => _inboundEntityEvents.Run(
- this, script, static (window, value) => window.OnPlayScriptReceived(value)),
- PlayPhysicsScriptType: message => _inboundEntityEvents.Run(
- this, message, static (window, value) => window._entityEffects?.HandleTyped(value)));
-
private AcDream.App.Net.LiveInventorySessionBindings
CreateLiveInventorySessionBindings() => new(
Objects,
@@ -3276,47 +3263,6 @@ public sealed class GameWindow : IDisposable
doll.MeshRefs = reposed;
}
- private void PublishLocalPhysicsTimestamps(
- uint guid,
- AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
- {
- AcDream.Core.Net.WorldSession? session = LiveSession;
- if (guid != _playerServerGuid || session is null) return;
- session.PublishAcceptedLocalPhysicsTimestamps(
- timestamps.Instance,
- timestamps.ServerControlledMove,
- timestamps.Teleport,
- timestamps.ForcePosition);
- }
-
- // #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
- // body's cell-relative CellPosition. This is the ONE place the streaming center
- // (_liveCenter) is allowed to touch the physics frame — at the placement seam,
- // converting the render-frame world position into the wire's (cell, local). After
- // seeding, physics carries (cell, local) forward without ever reading _liveCenter.
- private System.Numerics.Vector3 CellLocalForSeed(System.Numerics.Vector3 worldPos, uint cellId)
- {
- int lbX = (int)((cellId >> 24) & 0xFFu);
- int lbY = (int)((cellId >> 16) & 0xFFu);
- var origin = new System.Numerics.Vector3(
- (lbX - _liveCenterX) * 192f,
- (lbY - _liveCenterY) * 192f,
- 0f);
- return worldPos - origin;
- }
-
- private bool LoginWorldReady()
- {
- return TryGetLoginWorldCell(out uint cell)
- && EvaluateWorldRevealReadiness(cell).IsReady;
- }
-
- private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
- uint destinationCell)
- {
- return _worldReveal?.Evaluate(destinationCell) ?? default;
- }
-
private bool TryGetLoginWorldCell(out uint cell)
{
cell = 0;
@@ -3329,20 +3275,6 @@ public sealed class GameWindow : IDisposable
return cell != 0;
}
- ///
- /// Server-sent direct PhysicsScript (F754). EntityEffectController owns
- /// GUID translation and pre-materialization queueing.
- ///
- ///
- /// F754 remains a direct PhysicsScript DID. It is never resolved through a
- /// typed PhysicsScriptTable.
- ///
- ///
- private void OnPlayScriptReceived(AcDream.Core.Net.Messages.PlayPhysicsScript message)
- {
- _entityEffects?.HandleDirect(message);
- }
-
private void UpdateSkyPes(
float dayFraction,
AcDream.Core.World.DayGroupData? dayGroup,
@@ -3512,76 +3444,10 @@ public sealed class GameWindow : IDisposable
}
private void OnUpdate(double dt)
{
- using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
-
- // A resource callback can request deletion while the presentation
- // owner is inside a synchronous resume/suspend transition. The first
- // unregister leaves a canonical teardown tombstone; drain it only now,
- // after that callback stack has unwound on the same update thread.
- try
- {
- _liveEntityHydration?.RetryPendingTeardowns();
- }
- catch (AggregateException error)
- {
- // The tombstone retains every unfinished owner step. Report this
- // attempt, but keep the frame advancing so the next update can
- // retry after transient renderer/plugin teardown failures.
- Console.Error.WriteLine($"[live-entity-teardown] {error}");
- }
-
- AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
+ using var _updStage = _frameProfiler.BeginStage(
+ AcDream.App.Diagnostics.FrameStage.Update);
+ _updateFrameOrchestrator.Tick(
new AcDream.App.Update.UpdateFrameInput(dt));
- float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
-
- // Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
- // Timer::cur_time at the packet/default-script call site. Publish this
- // update frame's clock before streaming and network dispatch, then
- // reuse the same timestamp for animation hooks and the later drain.
- _scriptRunner?.PublishTime(frameTiming.ScriptTime);
-
- // Streaming publishes before inbound CreateObject dispatch so newly
- // accepted live projections can enter a resident bucket this frame.
- _streamingFrame.Tick();
-
- // Input callbacks feed the current object tick. Packets already read
- // by Core.Net remain queued until the retail object/physics phase has
- // consumed that input and published its final pose.
- _gameplayInputFrame!.Tick(frameTiming);
-
- // Drain pending live-session traffic AFTER streaming so any incoming
- // CreateObject events find their landblock already loaded in
- // GpuWorldState. Non-blocking — returns immediately if no datagrams
- // are in the kernel buffer. Fires EntitySpawned events synchronously.
- // Step 2: routed through the controller; functionally identical.
- // Retail SmartBox::UseTime (0x00455410) advances CObjectMaint and
- // CPhysics before it drains the inbound event queue. Keep the complete
- // live-object phase on that side of the barrier too. In particular,
- // the recall action retires before ACE's Hidden SetState freezes its
- // PartArray at the teleport boundary.
- _liveFrameCoordinator.Tick(frameDelta);
- _liveEntityLiveness?.Tick(ClientTimerNow());
-
- // Retail teleport transit runs after streaming and inbound state so a
- // newly resident destination can materialize in this same frame. The
- // focused owner also converges an F751 that arrived before the local
- // player projection/controller became available.
- _localPlayerTeleport!.Tick(frameDelta);
- // Phase K.1a — tick the input dispatcher so Hold-type bindings
- // re-fire while their chord is held. K.1b adds the subscribers
- // that actually consume the events.
- // Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
- // on selection, then revert). No-op when nothing is flashing.
- // Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam.
-
- // Phase K.2 — auto-enter player mode at login. The guard
- // returns true on the firing tick (one-shot); subsequent ticks
- // are no-ops. Skipped offline (no active session → IsLiveInWorld
- // predicate stays false). Cancelled by manual fly-toggle in
- // OnInputAction (Ctrl+Tab) or DebugPanel.
- _playerModeAutoEntry?.TryEnter();
-
- _cameraFrame.Tick(frameTiming);
}
private void OnCameraModeChanged(bool _modeBool)
diff --git a/src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs b/src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
new file mode 100644
index 00000000..79e4fd32
--- /dev/null
+++ b/src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
@@ -0,0 +1,19 @@
+using AcDream.App.World;
+
+namespace AcDream.App.Rendering;
+
+/// Routes retail PartArray world-entry boundaries to live animation owners.
+internal sealed class LiveEntityPartArrayLifecycle
+{
+ private readonly LiveEntityAnimationRuntimeView _animations;
+
+ public LiveEntityPartArrayLifecycle(
+ LiveEntityAnimationRuntimeView animations) =>
+ _animations = animations ?? throw new ArgumentNullException(nameof(animations));
+
+ public void HandleEnterWorld(uint localEntityId)
+ {
+ if (_animations.TryGetValue(localEntityId, out LiveEntityAnimationState? animation))
+ animation.Sequencer?.Manager.HandleEnterWorld();
+ }
+}
diff --git a/src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs b/src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
new file mode 100644
index 00000000..92a5422f
--- /dev/null
+++ b/src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
@@ -0,0 +1,78 @@
+using AcDream.App.Input;
+using AcDream.App.World;
+using AcDream.Core.Vfx;
+
+namespace AcDream.App.Update;
+
+/// Bridges canonical live-entity teardown into the host frame.
+internal sealed class LiveEntityTeardownFramePhase : IUpdateFrameTeardownPhase
+{
+ private readonly LiveEntityRuntime _runtime;
+
+ public LiveEntityTeardownFramePhase(LiveEntityRuntime runtime) =>
+ _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ public void RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
+}
+
+internal sealed class ConsoleUpdateFrameFailureSink : IUpdateFrameFailureSink
+{
+ public void ReportTeardownFailure(AggregateException error) =>
+ Console.Error.WriteLine($"[live-entity-teardown] {error}");
+}
+
+/// Publishes the frame clock to the retail PhysicsScript owner.
+internal sealed class PhysicsScriptClockPublisher : IUpdateFrameScriptClockPublisher
+{
+ private readonly PhysicsScriptRunner _runner;
+
+ public PhysicsScriptClockPublisher(PhysicsScriptRunner runner) =>
+ _runner = runner ?? throw new ArgumentNullException(nameof(runner));
+
+ public void PublishTime(double scriptTime) => _runner.PublishTime(scriptTime);
+}
+
+internal interface IClientMonotonicTimeSource
+{
+ double Now { get; }
+}
+
+internal sealed class StopwatchClientMonotonicTimeSource
+ : IClientMonotonicTimeSource
+{
+ public double Now =>
+ System.Diagnostics.Stopwatch.GetTimestamp()
+ / (double)System.Diagnostics.Stopwatch.Frequency;
+}
+
+///
+/// Supplies the absolute client timer to liveness independently of the
+/// normalized simulation delta.
+///
+internal sealed class LiveEntityLivenessFramePhase
+ : ILiveEntityLivenessFramePhase
+{
+ private readonly LiveEntityLivenessController _liveness;
+ private readonly IClientMonotonicTimeSource _clock;
+
+ public LiveEntityLivenessFramePhase(
+ LiveEntityLivenessController liveness,
+ IClientMonotonicTimeSource clock)
+ {
+ _liveness = liveness ?? throw new ArgumentNullException(nameof(liveness));
+ _clock = clock ?? throw new ArgumentNullException(nameof(clock));
+ }
+
+ public void Tick() => _liveness.Tick(_clock.Now);
+}
+
+internal sealed class PlayerModeAutoEntryFramePhase
+ : IPlayerModeAutoEntryFramePhase
+{
+ private readonly PlayerModeAutoEntry _autoEntry;
+
+ public PlayerModeAutoEntryFramePhase(PlayerModeAutoEntry autoEntry) =>
+ _autoEntry = autoEntry ?? throw new ArgumentNullException(nameof(autoEntry));
+
+ public void TryEnter() => _autoEntry.TryEnter();
+}
diff --git a/src/AcDream.App/World/LiveEntityDeletionController.cs b/src/AcDream.App/World/LiveEntityDeletionController.cs
new file mode 100644
index 00000000..b0436883
--- /dev/null
+++ b/src/AcDream.App/World/LiveEntityDeletionController.cs
@@ -0,0 +1,69 @@
+using AcDream.App.Input;
+using AcDream.Core.Items;
+using AcDream.Core.Net;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.App.World;
+
+internal interface ILiveEntityPruneSink
+{
+ bool Prune(LiveEntityPruneCandidate candidate);
+}
+
+///
+/// Owns authoritative and expiry-driven live-object deletion through one
+/// generation-safe runtime transaction.
+///
+internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
+{
+ private readonly LiveEntityRuntime _runtime;
+ private readonly ClientObjectTable _objects;
+ private readonly ILiveEntityTeardownCoordinator _teardown;
+ private readonly ILocalPlayerIdentitySource _identity;
+ private readonly Action? _diagnostic;
+
+ public LiveEntityDeletionController(
+ LiveEntityRuntime runtime,
+ ClientObjectTable objects,
+ ILiveEntityTeardownCoordinator teardown,
+ ILocalPlayerIdentitySource identity,
+ Action? diagnostic = null)
+ {
+ _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
+ _objects = objects ?? throw new ArgumentNullException(nameof(objects));
+ _teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
+ _diagnostic = diagnostic;
+ }
+
+ ///
+ /// Retail SmartBox::HandleDeleteObject @ 0x00451EA0 rejects the
+ /// player, requires the exact Instance timestamp, then performs one
+ /// symmetric logical teardown.
+ ///
+ public bool Delete(DeleteObject.Parsed delete)
+ {
+ if (delete.Guid == _identity.ServerGuid)
+ return false;
+
+ if (!_runtime.TryGetRecord(delete.Guid, out _))
+ _teardown.ForgetUnknownOwner(delete.Guid);
+
+ bool removed = _runtime.UnregisterLiveEntity(
+ delete,
+ isLocalPlayer: false,
+ beforeTeardown: () =>
+ ObjectTableWiring.ApplyEntityDelete(_objects, delete));
+ if (removed)
+ {
+ _diagnostic?.Invoke(
+ $"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
+ }
+ return removed;
+ }
+
+ public bool Prune(LiveEntityPruneCandidate candidate) =>
+ Delete(new DeleteObject.Parsed(
+ candidate.ServerGuid,
+ candidate.Generation));
+}
diff --git a/src/AcDream.App/World/LiveEntityHydrationController.cs b/src/AcDream.App/World/LiveEntityHydrationController.cs
index 1c20b5b7..c12cc06e 100644
--- a/src/AcDream.App/World/LiveEntityHydrationController.cs
+++ b/src/AcDream.App/World/LiveEntityHydrationController.cs
@@ -1,4 +1,5 @@
using AcDream.App.Rendering;
+using AcDream.App.Input;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
@@ -146,9 +147,9 @@ internal sealed class LiveEntityHydrationController
private readonly ILiveEntityReadyPublisher _ready;
private readonly ILiveEntityWorldOriginCoordinator _origin;
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
- private readonly ILiveEntityTeardownCoordinator _teardown;
- private readonly Action _publishTimestamps;
- private readonly Func _playerGuid;
+ private readonly IAcceptedLocalPhysicsTimestampPublisher _timestamps;
+ private readonly ILocalPlayerIdentitySource _identity;
+ private readonly LiveEntityDeletionController _deletion;
private readonly Action? _diagnostic;
public LiveEntityHydrationController(
@@ -160,9 +161,9 @@ internal sealed class LiveEntityHydrationController
ILiveEntityReadyPublisher ready,
ILiveEntityWorldOriginCoordinator origin,
ILiveEntityNetworkUpdateSink networkUpdates,
- ILiveEntityTeardownCoordinator teardown,
- Action publishTimestamps,
- Func playerGuid,
+ IAcceptedLocalPhysicsTimestampPublisher timestamps,
+ ILocalPlayerIdentitySource identity,
+ LiveEntityDeletionController deletion,
Action? diagnostic = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
@@ -173,10 +174,9 @@ internal sealed class LiveEntityHydrationController
_ready = ready ?? throw new ArgumentNullException(nameof(ready));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
- _teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
- _publishTimestamps = publishTimestamps
- ?? throw new ArgumentNullException(nameof(publishTimestamps));
- _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
+ _timestamps = timestamps ?? throw new ArgumentNullException(nameof(timestamps));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
+ _deletion = deletion ?? throw new ArgumentNullException(nameof(deletion));
_diagnostic = diagnostic;
}
@@ -207,7 +207,7 @@ internal sealed class LiveEntityHydrationController
try
{
- _publishTimestamps(spawn.Guid, result.Timestamps);
+ _timestamps.Publish(spawn.Guid, result.Timestamps);
if (_runtime.IsCurrentCreateIntegration(
record,
createIntegrationVersion)
@@ -353,23 +353,7 @@ internal sealed class LiveEntityHydrationController
// SmartBox::HandleDeleteObject rejects the player before resolving an
// object or touching any queued owner state. A pre-Create player
// Delete must therefore be side-effect-free too.
- if (delete.Guid == _playerGuid())
- return false;
-
- if (!_runtime.TryGetRecord(delete.Guid, out _))
- _teardown.ForgetUnknownOwner(delete.Guid);
-
- bool removed = _runtime.UnregisterLiveEntity(
- delete,
- isLocalPlayer: false,
- beforeTeardown: () =>
- ObjectTableWiring.ApplyEntityDelete(_objects, delete));
- if (removed)
- {
- _diagnostic?.Invoke(
- $"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
- }
- return removed;
+ return _deletion.Delete(delete);
}
///
@@ -377,9 +361,7 @@ internal sealed class LiveEntityHydrationController
/// generation gate and teardown transaction as an authoritative F747.
///
public bool OnPrune(LiveEntityPruneCandidate candidate) =>
- OnDelete(new DeleteObject.Parsed(
- candidate.ServerGuid,
- candidate.Generation));
+ _deletion.Prune(candidate);
public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
@@ -415,7 +397,7 @@ internal sealed class LiveEntityHydrationController
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
LiveEntityRecord[] records = _runtime.Records
- .Where(record => (record.ServerGuid != _playerGuid()
+ .Where(record => (record.ServerGuid != _identity.ServerGuid
|| !record.InitialHydrationCompleted
|| record.CreateProjectionSynchronizationPending
|| record.AppearanceProjectionSynchronizationPending)
diff --git a/src/AcDream.App/World/LiveEntityHydrationPorts.cs b/src/AcDream.App/World/LiveEntityHydrationPorts.cs
index b10dfcb6..ea0969c4 100644
--- a/src/AcDream.App/World/LiveEntityHydrationPorts.cs
+++ b/src/AcDream.App/World/LiveEntityHydrationPorts.cs
@@ -1,3 +1,4 @@
+using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
@@ -113,7 +114,17 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
private readonly StreamingController _streaming;
private readonly GpuWorldState _worldState;
private readonly WorldRevealCoordinator _worldReveal;
- private readonly Func _playerGuid;
+ private sealed class DelegateIdentitySource : ILocalPlayerIdentitySource
+ {
+ private readonly Func _read;
+
+ public DelegateIdentitySource(Func read) =>
+ _read = read ?? throw new ArgumentNullException(nameof(read));
+
+ public uint ServerGuid => _read();
+ }
+
+ private readonly ILocalPlayerIdentitySource _identity;
private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
private readonly Action? _diagnostic;
@@ -122,7 +133,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
StreamingController streaming,
GpuWorldState worldState,
WorldRevealCoordinator worldReveal,
- Func playerGuid,
+ ILocalPlayerIdentitySource identity,
ISealedDungeonCellClassifier sealedDungeonCells,
Action? diagnostic = null)
{
@@ -130,18 +141,37 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
- _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_sealedDungeonCells = sealedDungeonCells
?? throw new ArgumentNullException(nameof(sealedDungeonCells));
_diagnostic = diagnostic;
}
+ internal LiveEntityWorldOriginCoordinator(
+ LiveWorldOriginState origin,
+ StreamingController streaming,
+ GpuWorldState worldState,
+ WorldRevealCoordinator worldReveal,
+ Func playerGuid,
+ ISealedDungeonCellClassifier sealedDungeonCells,
+ Action? diagnostic = null)
+ : this(
+ origin,
+ streaming,
+ worldState,
+ worldReveal,
+ new DelegateIdentitySource(playerGuid),
+ sealedDungeonCells,
+ diagnostic)
+ {
+ }
+
public bool IsKnown => _origin.IsKnown;
public LiveEntityOriginInitialization TryInitialize(
WorldSession.EntitySpawn spawn)
{
- if (spawn.Guid != _playerGuid()
+ if (spawn.Guid != _identity.ServerGuid
|| spawn.Position is not { } position)
{
return new(_origin.IsKnown, Array.Empty());
diff --git a/src/AcDream.App/World/LiveEntityLivenessController.cs b/src/AcDream.App/World/LiveEntityLivenessController.cs
index c6ebfd3c..1645ce29 100644
--- a/src/AcDream.App/World/LiveEntityLivenessController.cs
+++ b/src/AcDream.App/World/LiveEntityLivenessController.cs
@@ -1,4 +1,5 @@
using AcDream.Core.Net.Messages;
+using AcDream.App.Input;
namespace AcDream.App.World;
@@ -86,19 +87,19 @@ internal sealed class LiveEntityLivenessController
private const double MaintenanceIntervalSeconds = 1.0;
private readonly LiveEntityRuntime _runtime;
- private readonly Func _playerGuid;
- private readonly Action _prune;
+ private readonly ILocalPlayerIdentitySource _identity;
+ private readonly ILiveEntityPruneSink _prune;
private readonly LiveEntityLivenessTracker _tracker = new();
private readonly List _samples = new();
private double _nextMaintenanceAt;
public LiveEntityLivenessController(
LiveEntityRuntime runtime,
- Func playerGuid,
- Action prune)
+ ILocalPlayerIdentitySource identity,
+ ILiveEntityPruneSink prune)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
- _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_prune = prune ?? throw new ArgumentNullException(nameof(prune));
}
@@ -108,7 +109,7 @@ internal sealed class LiveEntityLivenessController
return;
_nextMaintenanceAt = now + MaintenanceIntervalSeconds;
- uint playerGuid = _playerGuid();
+ uint playerGuid = _identity.ServerGuid;
if (playerGuid == 0
|| !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player)
|| player.Snapshot.Position is not { } playerPosition)
@@ -144,7 +145,7 @@ internal sealed class LiveEntityLivenessController
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
&& current.Generation == candidate.Generation)
{
- _prune(candidate);
+ _prune.Prune(candidate);
}
}
}
diff --git a/src/AcDream.App/World/LiveEntityRuntimeTeardownController.cs b/src/AcDream.App/World/LiveEntityRuntimeTeardownController.cs
index 48bd8cb3..cd79d305 100644
--- a/src/AcDream.App/World/LiveEntityRuntimeTeardownController.cs
+++ b/src/AcDream.App/World/LiveEntityRuntimeTeardownController.cs
@@ -1,4 +1,5 @@
using AcDream.App.Interaction;
+using AcDream.App.Input;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
@@ -38,7 +39,7 @@ internal sealed class LiveEntityRuntimeTeardownController
private readonly ShadowObjectRegistry? _shadows;
private readonly LiveEntityLightController? _lights;
private readonly EntityClassificationCache? _classification;
- private readonly Func? _playerGuid;
+ private readonly ILocalPlayerIdentitySource? _identity;
private readonly Func _createPlan;
private readonly Action _forgetUnknownOwner;
@@ -57,7 +58,7 @@ internal sealed class LiveEntityRuntimeTeardownController
ShadowObjectRegistry shadows,
LiveEntityLightController lights,
EntityClassificationCache classification,
- Func playerGuid)
+ ILocalPlayerIdentitySource identity)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
@@ -77,7 +78,7 @@ internal sealed class LiveEntityRuntimeTeardownController
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
_classification = classification
?? throw new ArgumentNullException(nameof(classification));
- _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
_createPlan = CreatePlan;
_forgetUnknownOwner = effects.ForgetUnknownOwner;
}
@@ -158,7 +159,7 @@ internal sealed class LiveEntityRuntimeTeardownController
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
record,
- _playerGuid!()));
+ _identity!.ServerGuid));
cleanups.Add(() => _children!.OnLogicalTeardown(record));
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
cleanups.Add(() => _lights!.Forget(existingEntity.Id));
diff --git a/src/AcDream.App/World/LiveWorldOriginState.cs b/src/AcDream.App/World/LiveWorldOriginState.cs
index 370e411f..551bb45c 100644
--- a/src/AcDream.App/World/LiveWorldOriginState.cs
+++ b/src/AcDream.App/World/LiveWorldOriginState.cs
@@ -1,3 +1,5 @@
+using System.Numerics;
+
namespace AcDream.App.World;
///
@@ -40,6 +42,23 @@ internal sealed class LiveWorldOriginState
CenterY = centerY;
}
+ public (int X, int Y) GetCenter() => (CenterX, CenterY);
+
+ ///
+ /// Converts the streamed render frame back into retail's landblock-local
+ /// physics frame at the one placement seed boundary.
+ ///
+ public Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
+ {
+ int landblockX = (int)((cellId >> 24) & 0xFFu);
+ int landblockY = (int)((cellId >> 16) & 0xFFu);
+ var origin = new Vector3(
+ (landblockX - CenterX) * 192f,
+ (landblockY - CenterY) * 192f,
+ 0f);
+ return worldPosition - origin;
+ }
+
///
/// Ends the accepted-origin lifetime while retaining the last coordinates
/// as the next placeholder. Consumers must gate them on
diff --git a/src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs b/src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
new file mode 100644
index 00000000..965995ce
--- /dev/null
+++ b/src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
@@ -0,0 +1,43 @@
+using AcDream.App.Input;
+using AcDream.App.Net;
+
+namespace AcDream.App.World;
+
+internal interface IAcceptedLocalPhysicsTimestampPublisher
+{
+ void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps);
+}
+
+///
+/// Publishes accepted local-player physics timestamps to the current exact
+/// session without retaining the window host.
+///
+internal sealed class LiveSessionLocalPhysicsTimestampPublisher
+ : IAcceptedLocalPhysicsTimestampPublisher
+{
+ private readonly ILocalPlayerIdentitySource _identity;
+ private readonly ILiveWorldSessionSource _session;
+
+ public LiveSessionLocalPhysicsTimestampPublisher(
+ ILocalPlayerIdentitySource identity,
+ ILiveWorldSessionSource session)
+ {
+ _identity = identity ?? throw new ArgumentNullException(nameof(identity));
+ _session = session ?? throw new ArgumentNullException(nameof(session));
+ }
+
+ public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
+ {
+ if (serverGuid != _identity.ServerGuid
+ || _session.CurrentSession is not { } session)
+ {
+ return;
+ }
+
+ session.PublishAcceptedLocalPhysicsTimestamps(
+ timestamps.Instance,
+ timestamps.ServerControlledMove,
+ timestamps.Teleport,
+ timestamps.ForcePosition);
+ }
+}
diff --git a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs
index ba308732..ea55f29f 100644
--- a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs
+++ b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs
@@ -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? PublishTimestampsAction { get; set; }
+ public Action? 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 _operations;
+
+ public RecordingTimestampPublisher(List operations) =>
+ _operations = operations;
+
+ public Action? 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; }
diff --git a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
index 3303e781..b2298e78 100644
--- a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
+++ b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
@@ -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);
+ }
}
diff --git a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
index 12c1f907..b9e2aded 100644
--- a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
+++ b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
@@ -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();
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));
- 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 calls,
RecordingTeardown? teardown = null,