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,