From ce41efb9e5938f79a7580476b949d172f52a1e69 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Jul 2026 19:06:09 +0200 Subject: [PATCH] refactor(runtime): cut graphical host over to canonical root Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger. Co-authored-by: OpenAI Codex --- .../ContentEffectsAudioComposition.cs | 8 +- .../Composition/FrameRootComposition.cs | 15 +- .../InteractionRetainedUiComposition.cs | 21 +- .../LivePresentationComposition.cs | 20 +- .../Composition/SessionPlayerComposition.cs | 45 ++- .../SettingsDevToolsComposition.cs | 15 +- .../Input/LocalPlayerRuntimeState.cs | 19 +- src/AcDream.App/Rendering/GameWindow.cs | 93 +++--- .../Rendering/GameWindowLifetime.cs | 47 +-- .../Runtime/CurrentGameRuntimeAdapter.cs | 194 +++++++----- .../CurrentGameRuntimeCommandAdapter.cs | 16 +- .../Runtime/CurrentGameRuntimeEventAdapter.cs | 275 ------------------ .../Runtime/CurrentGameRuntimeViewAdapter.cs | 134 --------- .../Update/UpdateFrameOrchestrator.cs | 12 +- .../ContentEffectsAudioCompositionTests.cs | 8 +- .../InteractionRetainedUiCompositionTests.cs | 27 +- .../SessionPlayerCompositionTests.cs | 7 +- .../SettingsDevToolsCompositionTests.cs | 7 +- .../GameRuntimeTestFactory.cs | 62 ++++ .../GameWindowLiveSessionOwnershipTests.cs | 80 ++++- .../GameWindowSlice8BoundaryTests.cs | 7 +- .../Runtime/CurrentGameRuntimeAdapterTests.cs | 83 +++--- .../Runtime/RuntimeActionOwnershipTests.cs | 24 +- .../Runtime/RuntimeCharacterOwnershipTests.cs | 16 +- .../Runtime/RuntimeInventoryOwnershipTests.cs | 21 +- .../Runtime/RuntimeMovementOwnershipTests.cs | 24 +- .../RuntimeWorldTransitOwnershipTests.cs | 25 +- .../Streaming/LandblockBuildOriginTests.cs | 25 +- .../World/RuntimeEntityOwnershipTests.cs | 61 ++-- 29 files changed, 613 insertions(+), 778 deletions(-) delete mode 100644 src/AcDream.App/Runtime/CurrentGameRuntimeEventAdapter.cs delete mode 100644 src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs create mode 100644 tests/AcDream.App.Tests/GameRuntimeTestFactory.cs diff --git a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs index 6b348c16..bfeb2859 100644 --- a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs +++ b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs @@ -12,6 +12,7 @@ using AcDream.Core.Physics; using AcDream.Core.Rendering; using AcDream.Core.Spells; using AcDream.Core.Vfx; +using AcDream.Runtime; using AcDream.Runtime.Gameplay; using DatReaderWriter; using Silk.NET.Input; @@ -49,7 +50,7 @@ internal sealed record ContentEffectsAudioDependencies( ResidencyBudgetOptions ResidencyBudgets, PhysicsDataCache PhysicsDataCache, bool DumpMotionEnabled, - RuntimeCharacterState Character, + GameRuntime Runtime, AnimationHookRouter HookRouter, EntityEffectPoseRegistry EffectPoses, DeferredEntityEffectAdvanceSource EntityEffectAdvance, @@ -57,7 +58,10 @@ internal sealed record ContentEffectsAudioDependencies( TranslucencyFadeManager TranslucencyFades, bool NoAudio, Action Log, - Action Error); + Action Error) +{ + public RuntimeCharacterState Character => Runtime.CharacterOwner; +} internal interface IGameWindowContentEffectsAudioPublication { diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index ac69b292..0319914f 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -7,6 +7,7 @@ using AcDream.App.Runtime; using AcDream.App.Settings; using AcDream.App.Update; using AcDream.App.World; +using AcDream.Runtime; using AcDream.Runtime.Session; using AcDream.Core.Combat; using AcDream.Core.Lighting; @@ -21,6 +22,7 @@ namespace AcDream.App.Composition; internal sealed record FrameRootDependencies( RuntimeOptions Options, + GameRuntime Runtime, GL Gl, IWindow Window, IInputContext Input, @@ -33,7 +35,6 @@ internal sealed record FrameRootDependencies( LocalPlayerModeState PlayerMode, LocalPlayerIdentityState PlayerIdentity, ChaseCameraInputState ChaseCameraInput, - RuntimeLocalPlayerMovementState PlayerController, LiveWorldOriginState WorldOrigin, ParticleVisibilityController ParticleVisibility, EntityEffectPoseRegistry EffectPoses, @@ -48,12 +49,18 @@ internal sealed record FrameRootDependencies( DebugVmRenderFactsPublisher DebugVmRenderFacts, IInputCaptureSource InputCapture, DispatcherCameraInputSource CameraInput, - SelectionState Selection, LiveEntityAnimationRuntimeView Animations, UpdateFrameClock UpdateClock, - CombatState Combat, GameFrameGraphSlot FrameGraphs, - Action Log); + Action Log) +{ + public RuntimeLocalPlayerMovementState PlayerController => + Runtime.MovementOwner; + + public SelectionState Selection => Runtime.ActionOwner.Selection; + + public CombatState Combat => Runtime.ActionOwner.Combat; +} internal sealed record FrameRootResult( UpdateFrameOrchestrator Update, diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index e30f2ef9..7f189414 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -44,19 +44,15 @@ internal sealed record InteractionRetainedUiDependencies( RetainedUiInputCaptureSlot RetainedInputCapture, InputDispatcher? InputDispatcher, RuntimeSettingsController Settings, - RuntimeActionState Actions, + GameRuntime Runtime, IRuntimeCombatAttackOperations CombatAttackOperations, RuntimeCombatTargetOperationsSlot CombatTargetOperations, RuntimeSpellCastOperationsSlot SpellCastOperations, - RuntimeInventoryState Inventory, MagicCatalog MagicCatalog, - RuntimeCharacterState Character, - RuntimeCommunicationState Communication, StackSplitQuantityState StackSplitQuantity, BufferedUiRegistry? UiRegistry, LiveCombatModeCommandSlot CombatModeCommands, ILocalPlayerIdentitySource PlayerIdentity, - IRuntimeLocalPlayerControllerSource PlayerController, ILocalPlayerModeSource PlayerMode, Func SelectionCameraFactory, @@ -64,7 +60,20 @@ internal sealed record InteractionRetainedUiDependencies( VitalsVM? ExistingVitals, Action? Toast, Func ClientTime, - Action Log); + Action Log) +{ + public RuntimeActionState Actions => Runtime.ActionOwner; + + public RuntimeInventoryState Inventory => Runtime.InventoryOwner; + + public RuntimeCharacterState Character => Runtime.CharacterOwner; + + public RuntimeCommunicationState Communication => + Runtime.CommunicationOwner; + + public IRuntimeLocalPlayerControllerSource PlayerController => + Runtime.MovementOwner; +} internal sealed record RetainedUiComposition( UiHost Host, diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index 2b75382d..0e3cfe73 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -25,6 +25,7 @@ using AcDream.Core.Rendering; using AcDream.Core.Selection; using AcDream.Core.Vfx; using AcDream.Core.World; +using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.World; using DatReaderWriter; @@ -45,8 +46,7 @@ internal sealed record LivePresentationDependencies( PhysicsDataCache PhysicsDataCache, WorldGameState WorldGameState, WorldEvents WorldEvents, - SelectionState Selection, - RuntimeEntityObjectLifetime EntityObjects, + GameRuntime Runtime, LiveEntityRuntimeSlot RuntimeSlot, DeferredLiveEntityMotionRuntimeBindings MotionBindings, DeferredEntityEffectAdvanceSource EffectAdvance, @@ -61,7 +61,6 @@ internal sealed record LivePresentationDependencies( CellVisibility CellVisibility, LiveWorldOriginState WorldOrigin, LocalPlayerIdentityState PlayerIdentity, - RuntimeLocalPlayerMovementState PlayerController, PointerPositionState PointerPosition, PlayerApproachCompletionState PlayerApproachCompletions, GameRenderResourceLifetime RenderResourceLifetime, @@ -73,7 +72,18 @@ internal sealed record LivePresentationDependencies( DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics, DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics, Action Log, - Action? Toast); + Action? Toast) +{ + public SelectionState Selection => Runtime.ActionOwner.Selection; + + public RuntimeEntityObjectLifetime EntityObjects => + Runtime.EntityObjects; + + public RuntimeWorldTransitState WorldTransit => Runtime.TransitOwner; + + public RuntimeLocalPlayerMovementState PlayerController => + Runtime.MovementOwner; +} internal sealed record LivePresentationResult( DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle, @@ -336,7 +346,7 @@ internal sealed class LivePresentationCompositionPhase staticAnimationScheduler.Unregister, (entity, info) => staticAnimationScheduler.Rebind(entity, info)); - var worldTransit = new RuntimeWorldTransitState(d.Log); + RuntimeWorldTransitState worldTransit = d.WorldTransit; var worldAvailability = new WorldGenerationAvailabilityState(worldTransit); var worldState = new GpuWorldState( diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index 13ba9d5e..1d44f1d9 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -26,6 +26,7 @@ using AcDream.Core.Player; using AcDream.Core.Social; using AcDream.Core.Spells; using AcDream.Core.World; +using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; @@ -35,6 +36,7 @@ namespace AcDream.App.Composition; internal sealed record SessionPlayerDependencies( RuntimeOptions Options, + GameRuntime Runtime, IWindow Window, object DatLock, RuntimeSettingsController Settings, @@ -49,8 +51,6 @@ internal sealed record SessionPlayerDependencies( PhysicsDataCache PhysicsDataCache, WorldGameState WorldGameState, WorldEvents WorldEvents, - RuntimeActionState Actions, - RuntimeEntityObjectLifetime EntityObjects, EntityClassificationCache ClassificationCache, LiveEntityRuntimeSlot RuntimeSlot, LiveEntityAnimationRuntimeView AnimatedEntities, @@ -60,7 +60,6 @@ internal sealed record SessionPlayerDependencies( RetailInboundEventDispatcher InboundEntityEvents, DeferredLiveEntityMotionRuntimeBindings MotionBindings, LocalPlayerIdentityState PlayerIdentity, - RuntimeLocalPlayerMovementState PlayerController, LocalPlayerPhysicsHostSlot PlayerHost, LocalPlayerModeState PlayerMode, ChaseCameraInputState ChaseCameraInput, @@ -71,7 +70,6 @@ internal sealed record SessionPlayerDependencies( LocalPlayerShadowState PlayerShadow, ViewportAspectState ViewportAspect, PlayerApproachCompletionState PlayerApproachCompletions, - RuntimeInventoryState Inventory, PointerPositionState PointerPosition, DispatcherMovementInputSource MovementInput, IInputCaptureSource InputCapture, @@ -82,10 +80,24 @@ internal sealed record SessionPlayerDependencies( MovementTruthDiagnosticController MovementDiagnostics, CombatAttackOperationsSlot CombatAttackOperations, CombatFeedbackSlot CombatFeedback, - RuntimeCommunicationState Communication, - RuntimeCharacterState Character, TransferableResourceSlot PortalTunnelFallback, - Action Log); + Action Log) +{ + public RuntimeActionState Actions => Runtime.ActionOwner; + + public RuntimeEntityObjectLifetime EntityObjects => + Runtime.EntityObjects; + + public RuntimeLocalPlayerMovementState PlayerController => + Runtime.MovementOwner; + + public RuntimeInventoryState Inventory => Runtime.InventoryOwner; + + public RuntimeCommunicationState Communication => + Runtime.CommunicationOwner; + + public RuntimeCharacterState Character => Runtime.CharacterOwner; +} internal sealed record SessionPlayerResult( LandblockStreamer Streamer, @@ -430,11 +442,7 @@ internal sealed class SessionPlayerCompositionPhase sealedDungeonCells, d.Log); - var liveSessionLease = scope.Acquire( - "live-session controller", - static () => new LiveSessionController(), - static value => value.Dispose()); - LiveSessionController liveSession = liveSessionLease.Resource; + LiveSessionController liveSession = d.Runtime.Session; var liveSessionCommands = new LiveSessionCommandSurface(); var liveSessionSource = new LiveSessionAppSource( liveSession, @@ -869,19 +877,9 @@ internal sealed class SessionPlayerCompositionPhase "live combat-mode commands", d.CombatModeCommands.BindOwned(combatCommand)); var gameRuntime = new CurrentGameRuntimeAdapter( - liveSession, + d.Runtime, sessionHost, liveSessionCommands, - d.PlayerIdentity, - d.EntityObjects, - d.Inventory, - d.Character, - d.Communication, - d.Actions, - d.PlayerController, - d.WorldEnvironment.Runtime, - live.WorldTransit, - d.UpdateClock, live.SelectionInteractions); bindings.Adopt("current game runtime adapter", gameRuntime); bindings.Adopt( @@ -987,7 +985,6 @@ internal sealed class SessionPlayerCompositionPhase _publication.PublishSessionPlayer(result); streamerLease.Transfer(); - liveSessionLease.Transfer(); teleportLease.Transfer(); gameplayActionsLease?.Transfer(); componentLifecycleLease.Transfer(); diff --git a/src/AcDream.App/Composition/SettingsDevToolsComposition.cs b/src/AcDream.App/Composition/SettingsDevToolsComposition.cs index 13a5ff97..67d3c760 100644 --- a/src/AcDream.App/Composition/SettingsDevToolsComposition.cs +++ b/src/AcDream.App/Composition/SettingsDevToolsComposition.cs @@ -6,6 +6,7 @@ using AcDream.App.Settings; using AcDream.Core.Chat; using AcDream.Core.Combat; using AcDream.Core.Player; +using AcDream.Runtime; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions.Input; using AcDream.UI.Abstractions.Panels.Chat; @@ -35,16 +36,22 @@ internal sealed record SettingsDevToolsDependencies( RuntimeSettingsController Settings, IRuntimeSettingsStartupTarget StartupTarget, HostQuiescenceGate HostQuiescence, - RuntimeCommunicationState Communication, - CombatState Combat, - LocalPlayerState LocalPlayer, + GameRuntime Runtime, SettingsDevToolsOptionalDependencies? DevTools, RuntimeDiagnosticCommandSlot DiagnosticCommands, CombatFeedbackSlot CombatFeedback, KeyBindings KeyBindings, FrameProfiler FrameProfiler, FramebufferResizeController FramebufferResize, - Action Log); + Action Log) +{ + public RuntimeCommunicationState Communication => + Runtime.CommunicationOwner; + + public CombatState Combat => Runtime.ActionOwner.Combat; + + public LocalPlayerState LocalPlayer => Runtime.CharacterOwner.LocalPlayer; +} internal interface IGameWindowSettingsDevToolsPublication { diff --git a/src/AcDream.App/Input/LocalPlayerRuntimeState.cs b/src/AcDream.App/Input/LocalPlayerRuntimeState.cs index 39613afa..4a1c5924 100644 --- a/src/AcDream.App/Input/LocalPlayerRuntimeState.cs +++ b/src/AcDream.App/Input/LocalPlayerRuntimeState.cs @@ -1,5 +1,6 @@ using AcDream.Core.Physics; using AcDream.App.Physics; +using AcDream.Runtime.Gameplay; namespace AcDream.App.Input; @@ -13,7 +14,23 @@ internal interface ILocalPlayerIdentitySource /// internal sealed class LocalPlayerIdentityState : ILocalPlayerIdentitySource { - public uint ServerGuid { get; set; } + private readonly RuntimeLocalPlayerIdentityState _runtime; + + public LocalPlayerIdentityState() + : this(new RuntimeLocalPlayerIdentityState()) + { + } + + public LocalPlayerIdentityState(RuntimeLocalPlayerIdentityState runtime) + { + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + } + + public uint ServerGuid + { + get => _runtime.ServerGuid; + set => _runtime.ServerGuid = value; + } } internal interface ILocalPlayerPhysicsHostSource diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8cc3a206..f6ee8a55 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -6,6 +6,7 @@ using AcDream.App.Rendering.Wb; using AcDream.App.Settings; using AcDream.App.World; using AcDream.Content; +using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; @@ -163,7 +164,7 @@ public sealed class GameWindow : private readonly AcDream.App.Input.MovementTruthDiagnosticController _movementTruthDiagnostics; private readonly LocalPlayerOutboundController _localPlayerOutbound; - private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new(); + private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock; private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController; // Step 7 projectile presentation. The controller owns no identity map; // each runtime component is stored on the canonical LiveEntityRecord. @@ -325,8 +326,11 @@ public sealed class GameWindow : // J4/J5.1: Runtime owns communication, selection, combat, and target-mode // state. App, UI, plugins, and live-session routing borrow exact children. - private readonly RuntimeCommunicationState _runtimeCommunication = new(); - private readonly RuntimeActionState _runtimeActions; + private readonly GameRuntime _runtime; + private readonly IDisposable _runtimeHostLease; + private RuntimeCommunicationState _runtimeCommunication => + _runtime.CommunicationOwner; + private RuntimeActionState _runtimeActions => _runtime.ActionOwner; public AcDream.Core.Selection.SelectionState Selection => _runtimeActions.Selection; public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat; @@ -338,9 +342,12 @@ public sealed class GameWindow : _runtimeCommunication.Squelch; public AcDream.Core.Combat.CombatState Combat => _runtimeActions.Combat; - private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new(); - private readonly RuntimeInventoryState _runtimeInventory; - private readonly RuntimeCharacterState _runtimeCharacter; + private RuntimeEntityObjectLifetime _runtimeEntityObjects => + _runtime.EntityObjects; + private RuntimeInventoryState _runtimeInventory => + _runtime.InventoryOwner; + private RuntimeCharacterState _runtimeCharacter => + _runtime.CharacterOwner; public AcDream.Core.Items.ItemManaState ItemMana => _runtimeInventory.ItemMana; // Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog @@ -431,7 +438,8 @@ public sealed class GameWindow : private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo; private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer; // Phase B.2: player movement mode. - private readonly RuntimeLocalPlayerMovementState _playerControllerSlot = new(); + private RuntimeLocalPlayerMovementState _playerControllerSlot => + _runtime.MovementOwner; private PlayerMovementController? _playerController => _playerControllerSlot.Controller; private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new(); @@ -442,7 +450,8 @@ public sealed class GameWindow : private readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new(); private bool _playerMode => _localPlayerMode.IsPlayerMode; - private readonly AcDream.App.Input.LocalPlayerIdentityState _localPlayerIdentity = new(); + private readonly AcDream.App.Input.LocalPlayerIdentityState + _localPlayerIdentity; private uint _playerServerGuid { get => _localPlayerIdentity.ServerGuid; @@ -510,7 +519,6 @@ public sealed class GameWindow : // Runtime owns the canonical session generation and transport lifetime. // The window retains only the composition handles needed for startup and // shutdown; graphical feature callbacks borrow state through App adapters. - private LiveSessionController? _liveSessionController; private LiveSessionHost? _liveSessionHost; private AcDream.Core.Net.WorldSession? LiveSession => _liveSessionHost?.CurrentSession; @@ -565,22 +573,24 @@ public sealed class GameWindow : AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null) { _options = options ?? throw new System.ArgumentNullException(nameof(options)); - _worldEnvironment = new AcDream.App.World.WorldEnvironmentController( - new AcDream.Runtime.World.RuntimeWorldEnvironmentState( - log: Console.WriteLine, - timeSyncDiagnostic: - options.DumpSky ? Console.WriteLine : null), - options.ForcedDayGroupIndex, - Console.WriteLine); - _runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects); - _runtimeCharacter = new RuntimeCharacterState(); - _runtimeActions = new RuntimeActionState( - _runtimeInventory.Transactions, - _runtimeCharacter.Spellbook, + _runtime = new GameRuntime(new GameRuntimeDependencies( _combatAttackOperations, _combatTargetOperations, _combatModeOperations, - _spellCastOperations); + _spellCastOperations, + Log: Console.WriteLine, + TimeSyncDiagnostic: + options.DumpSky ? Console.WriteLine : null)); + _runtimeHostLease = _runtime.AcquireHostLease( + "graphical GameWindow"); + _localPlayerIdentity = new AcDream.App.Input.LocalPlayerIdentityState( + _runtime.PlayerIdentity); + _updateFrameClock = new AcDream.App.Update.UpdateFrameClock( + _runtime.Clock); + _worldEnvironment = new AcDream.App.World.WorldEnvironmentController( + _runtime.EnvironmentOwner, + options.ForcedDayGroupIndex, + Console.WriteLine); var alphaScratchBudgets = AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create( _options.ResidencyBudgets.AlphaScratchBytes); @@ -1039,7 +1049,6 @@ public sealed class GameWindow : || _streamingOriginRecenter is not null || _worldReveal is not null || _spawnClaimHydration is not null - || _liveSessionController is not null || _liveEntityHydration is not null || _liveEntityNetworkUpdates is not null || _liveEntityLiveness is not null @@ -1063,7 +1072,6 @@ public sealed class GameWindow : _streamingOriginRecenter = result.StreamingOriginRecenter; _worldReveal = result.WorldReveal; _spawnClaimHydration = result.SpawnClaimHydration; - _liveSessionController = result.LiveSession; _liveEntityHydration = result.Hydration; _liveEntityNetworkUpdates = result.NetworkUpdates; _liveEntityLiveness = result.Liveness; @@ -1163,7 +1171,7 @@ public sealed class GameWindow : _options.ResidencyBudgets, _physicsDataCache, _animationDiagnostics.DumpMotionEnabled, - _runtimeCharacter, + _runtime, _hookRouter, _effectPoses, _entityEffectAdvance, @@ -1221,9 +1229,7 @@ public sealed class GameWindow : hostInputCamera.CameraController, contentEffectsAudio.Audio?.Engine), _hostQuiescence, - _runtimeCommunication, - Combat, - LocalPlayer, + _runtime, optionalDevTools, _runtimeDiagnosticCommands, _combatFeedback, @@ -1270,19 +1276,15 @@ public sealed class GameWindow : _retainedInputCapture, hostInputCamera.InputDispatcher, _runtimeSettings, - _runtimeActions, + _runtime, _combatAttackOperations, _combatTargetOperations, _spellCastOperations, - _runtimeInventory, contentEffectsAudio.MagicCatalog, - _runtimeCharacter, - _runtimeCommunication, _stackSplitQuantity, _uiRegistry, _liveCombatModeCommands, _localPlayerIdentity, - _playerControllerSlot, _localPlayerMode, viewPlane => new SelectionCameraSource( hostInputCamera.CameraController, @@ -1323,8 +1325,7 @@ public sealed class GameWindow : _physicsDataCache, _worldGameState, _worldEvents, - Selection, - _runtimeEntityObjects, + _runtime, _liveEntityRuntimeSlot, _liveEntityMotionBindings, _entityEffectAdvance, @@ -1339,7 +1340,6 @@ public sealed class GameWindow : _cellVisibility, _liveWorldOrigin, _localPlayerIdentity, - _playerControllerSlot, _pointerPosition, _playerApproachCompletions, _renderResourceLifetime, @@ -1369,6 +1369,7 @@ public sealed class GameWindow : new SessionPlayerCompositionPhase( new SessionPlayerDependencies( _options, + _runtime, _window!, _datLock, _runtimeSettings, @@ -1383,8 +1384,6 @@ public sealed class GameWindow : _physicsDataCache, _worldGameState, _worldEvents, - _runtimeActions, - _runtimeEntityObjects, _classificationCache, _liveEntityRuntimeSlot, _animatedEntities, @@ -1394,7 +1393,6 @@ public sealed class GameWindow : _inboundEntityEvents, _liveEntityMotionBindings, _localPlayerIdentity, - _playerControllerSlot, _playerHostSlot, _localPlayerMode, _chaseCameraInput, @@ -1405,7 +1403,6 @@ public sealed class GameWindow : _localPlayerShadow, _viewportAspect, _playerApproachCompletions, - _runtimeInventory, _pointerPosition, _movementInput, _inputCapture, @@ -1416,8 +1413,6 @@ public sealed class GameWindow : _movementTruthDiagnostics, _combatAttackOperations, _combatFeedback, - _runtimeCommunication, - _runtimeCharacter, _portalTunnelFallback, Console.WriteLine), this).Compose( @@ -1437,6 +1432,7 @@ public sealed class GameWindow : sessionPlayer) => new FrameRootCompositionPhase( new FrameRootDependencies( _options, + _runtime, platformResult.Graphics, _window!, platformResult.Input, @@ -1449,7 +1445,6 @@ public sealed class GameWindow : _localPlayerMode, _localPlayerIdentity, _chaseCameraInput, - _playerControllerSlot, _liveWorldOrigin, _particleVisibility, _effectPoses, @@ -1464,10 +1459,8 @@ public sealed class GameWindow : _debugVmRenderFacts, _inputCapture, _cameraInput, - Selection, _animatedEntities, _updateFrameClock, - Combat, _frameGraphs, Console.WriteLine), this).Compose( @@ -1571,7 +1564,7 @@ public sealed class GameWindow : _retailUiLease, _uiHost, _devToolsComposition, - _liveSessionController, + _runtime, _runtimeSettings, _movementInput, _cameraInput, @@ -1590,12 +1583,8 @@ public sealed class GameWindow : _streamer, _equippedChildRenderer, _liveEntities, - _runtimeEntityObjects, - _runtimeInventory, - _runtimeCharacter, - _runtimeCommunication, - _runtimeActions, - _playerControllerSlot, + _runtime, + _runtimeHostLease, _renderSceneShadow, _livePresentationBindings, _entityEffectAdvance, diff --git a/src/AcDream.App/Rendering/GameWindowLifetime.cs b/src/AcDream.App/Rendering/GameWindowLifetime.cs index 3a9f53c6..650d77be 100644 --- a/src/AcDream.App/Rendering/GameWindowLifetime.cs +++ b/src/AcDream.App/Rendering/GameWindowLifetime.cs @@ -17,6 +17,7 @@ using AcDream.App.World; using AcDream.Content; using AcDream.Core.Audio; using AcDream.Core.Physics; +using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; @@ -62,7 +63,7 @@ internal sealed record IngressShutdownRoots( // Keeps failed physical UI bindings alive through native-window release. UiHost? RetainedUiHost, DevToolsCompositionOwner? DevTools, - LiveSessionController? LiveSession, + GameRuntime Runtime, RuntimeSettingsController Settings, DispatcherMovementInputSource MovementInput, DispatcherCameraInputSource CameraInput, @@ -83,12 +84,8 @@ internal sealed record LiveShutdownRoots( LandblockStreamer? Streamer, EquippedChildRenderController? EquippedChildren, LiveEntityRuntime? LiveEntities, - RuntimeEntityObjectLifetime EntityObjects, - RuntimeInventoryState Inventory, - RuntimeCharacterState Character, - RuntimeCommunicationState Communication, - RuntimeActionState Actions, - RuntimeLocalPlayerMovementState Movement, + GameRuntime Runtime, + IDisposable RuntimeHostLease, RenderSceneShadowRuntime? RenderSceneShadow, LivePresentationRuntimeBindings? PresentationBindings, DeferredEntityEffectAdvanceSource EffectAdvance, @@ -352,7 +349,7 @@ internal static class GameWindowShutdownManifest Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()), Hard("retained UI input", ingress.RetailUi.QuiesceInput), Hard("developer tools input", () => ingress.DevTools?.DeactivateInput()), - Hard("live session", () => DisposeLiveSession(ingress.LiveSession)), + Hard("game runtime session", ingress.Runtime.StopSession), ]), new ResourceShutdownStage("physical ingress cleanup", [ @@ -391,24 +388,6 @@ internal static class GameWindowShutdownManifest "shadow render scene", () => live.RenderSceneShadow?.Dispose()), ]), - new ResourceShutdownStage("runtime entity/object stream", - [ - Hard( - "runtime local movement state", - live.Movement.Dispose), - Hard( - "runtime action state", - live.Actions.Dispose), - Hard( - "runtime character state", - live.Character.Dispose), - Hard( - "runtime inventory state", - live.Inventory.Dispose), - Hard( - "runtime entity/object lifetime", - live.EntityObjects.Dispose), - ]), new ResourceShutdownStage("effect dispatch edges", [ Hard("live-presentation bindings", () => live.PresentationBindings?.Dispose()), @@ -456,9 +435,10 @@ internal static class GameWindowShutdownManifest Hard("sky", () => render.Sky?.Dispose()), Hard("particles", () => render.Particles?.Dispose()), ]), - new ResourceShutdownStage("runtime communication state", + new ResourceShutdownStage("game runtime root", [ - Hard("communication owner", live.Communication.Dispose), + Hard("graphical runtime host lease", live.RuntimeHostLease.Dispose), + Hard("game runtime", () => DisposeGameRuntime(live.Runtime)), ]), new ResourceShutdownStage("shared texture owners", [ @@ -515,15 +495,14 @@ internal static class GameWindowShutdownManifest private static ResourceShutdownOperation Soft(string name, Action action) => new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue); - private static void DisposeLiveSession(LiveSessionController? controller) + private static void DisposeGameRuntime(GameRuntime runtime) { - if (controller is null) - return; - controller.Dispose(); - if (!controller.IsDisposalComplete) + runtime.Dispose(); + GameRuntimeOwnershipSnapshot ownership = runtime.CaptureOwnership(); + if (!ownership.IsConverged) { throw new InvalidOperationException( - "Live-session disposal was deferred by a reentrant lifecycle callback."); + "The canonical game runtime did not converge after disposal."); } } diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs index b6c0f4a3..372d5c6b 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeAdapter.cs @@ -1,10 +1,6 @@ -using AcDream.App.Input; using AcDream.App.Interaction; using AcDream.App.Net; -using AcDream.App.World; using AcDream.Runtime; -using AcDream.Runtime.Entities; -using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; using AcDream.Runtime.World; using AcDream.UI.Abstractions; @@ -12,9 +8,9 @@ using AcDream.UI.Abstractions; namespace AcDream.App.Runtime; /// -/// App composition root for Slice J's borrowed runtime boundary. Focused -/// adapters project the existing canonical owners; this root owns only those -/// adapters and never owns or copies gameplay state. +/// Synchronous graphical command adapter over one canonical +/// . It owns only its host lease and observer +/// subscriptions; every view and mutable owner is borrowed directly. /// internal sealed class CurrentGameRuntimeAdapter : IGameRuntimeView, @@ -22,72 +18,80 @@ internal sealed class CurrentGameRuntimeAdapter IRuntimeEventSource, IDisposable { - private readonly CurrentGameRuntimeViewAdapter _view; - private readonly CurrentGameRuntimeEventAdapter _events; + private readonly GameRuntime _runtime; private readonly CurrentGameRuntimeCommandAdapter _commands; + private readonly IDisposable _hostLease; + private readonly object _subscriptionGate = new(); + private readonly HashSet _subscriptions = []; private bool _disposed; public CurrentGameRuntimeAdapter( - LiveSessionController session, + GameRuntime runtime, LiveSessionHost sessionHost, ICommandBus commands, - LocalPlayerIdentityState playerIdentity, - RuntimeEntityObjectLifetime entityObjects, - RuntimeInventoryState inventory, - RuntimeCharacterState character, - RuntimeCommunicationState communication, - RuntimeActionState actions, - RuntimeLocalPlayerMovementState movement, - RuntimeWorldEnvironmentState environment, - RuntimeWorldTransitState worldTransit, - IGameRuntimeClock clock, SelectionInteractionController selection) { - _view = new CurrentGameRuntimeViewAdapter( - session, - playerIdentity, - entityObjects, - inventory, - character, - communication, - actions, - movement, - environment, - worldTransit, - clock); - entityObjects.BindEventContext( - () => _view.Generation, - () => clock.FrameNumber); - _events = new CurrentGameRuntimeEventAdapter( - _view, - entityObjects, - communication); - _commands = new CurrentGameRuntimeCommandAdapter( - session, - sessionHost, - commands, - _view, - inventory, - character, - actions, - movement, - selection, - _events); + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + ArgumentNullException.ThrowIfNull(sessionHost); + ArgumentNullException.ThrowIfNull(commands); + ArgumentNullException.ThrowIfNull(selection); + + _hostLease = runtime.AcquireHostLease( + "graphical game-runtime command adapter"); + try + { + _commands = new CurrentGameRuntimeCommandAdapter( + runtime.Session, + sessionHost, + commands, + this, + runtime.InventoryOwner, + runtime.CharacterOwner, + runtime.ActionOwner, + runtime.MovementOwner, + selection, + runtime.EventSink); + } + catch + { + _hostLease.Dispose(); + throw; + } } - public RuntimeGenerationToken Generation => _view.Generation; - public RuntimeLifecycleSnapshot Lifecycle => _view.Lifecycle; - public IGameRuntimeClock Clock => _view.Clock; - public IRuntimeEntityView Entities => _view.Entities; - public IRuntimeInventoryView Inventory => _view.Inventory; - public IRuntimeInventoryStateView InventoryState => _view.InventoryState; - public IRuntimeCharacterView Character => _view.Character; - public IRuntimeSocialView Social => _view.Social; - public IRuntimeChatView Chat => _view.Chat; - public IRuntimeActionView Actions => _view.Actions; - public IRuntimeMovementView Movement => _view.Movement; - public IRuntimeWorldEnvironmentView Environment => _view.Environment; - public IRuntimePortalView Portal => _view.Portal; + private bool IsActive => + !_disposed + && !_runtime.Session.IsDisposalComplete; + + public RuntimeGenerationToken Generation => _runtime.Generation; + + public RuntimeLifecycleSnapshot Lifecycle + { + get + { + RuntimeLifecycleSnapshot current = _runtime.Lifecycle; + return IsActive + ? current + : current with + { + State = RuntimeLifecycleState.Disposed, + HasTransport = false, + }; + } + } + + public IGameRuntimeClock Clock => _runtime.Clock; + public IRuntimeEntityView Entities => _runtime.Entities; + public IRuntimeInventoryView Inventory => _runtime.Inventory; + public IRuntimeInventoryStateView InventoryState => + _runtime.InventoryState; + public IRuntimeCharacterView Character => _runtime.Character; + public IRuntimeSocialView Social => _runtime.Social; + public IRuntimeChatView Chat => _runtime.Chat; + public IRuntimeActionView Actions => _runtime.Actions; + public IRuntimeMovementView Movement => _runtime.Movement; + public IRuntimeWorldEnvironmentView Environment => _runtime.Environment; + public IRuntimePortalView Portal => _runtime.Portal; public IRuntimeSessionCommands Session => _commands; public IRuntimeSelectionCommands Selection => _commands; @@ -109,18 +113,64 @@ internal sealed class CurrentGameRuntimeAdapter public IRuntimeSocialCommands SocialCommands => _commands; IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands; - public RuntimeStateCheckpoint CaptureCheckpoint() => - _view.CaptureCheckpoint(); + public RuntimeStateCheckpoint CaptureCheckpoint() + { + RuntimeStateCheckpoint checkpoint = _runtime.CaptureCheckpoint(); + return checkpoint with { Lifecycle = Lifecycle.State }; + } - public IDisposable Subscribe(IRuntimeEventObserver observer) => - _events.Subscribe(observer); + public IDisposable Subscribe(IRuntimeEventObserver observer) + { + ArgumentNullException.ThrowIfNull(observer); + lock (_subscriptionGate) + { + ObjectDisposedException.ThrowIf(_disposed, this); + IDisposable runtimeSubscription = _runtime.Subscribe(observer); + var subscription = new AdapterSubscription( + this, + runtimeSubscription); + _subscriptions.Add(subscription); + return subscription; + } + } public void Dispose() { - if (_disposed) - return; - _disposed = true; - _view.Deactivate(); - _events.Dispose(); + lock (_subscriptionGate) + { + if (_disposed) + return; + _disposed = true; + while (_subscriptions.Count != 0) + _subscriptions.First().DisposeCore(); + } + _hostLease.Dispose(); + } + + private void Remove(AdapterSubscription subscription) + { + lock (_subscriptionGate) + _subscriptions.Remove(subscription); + } + + private sealed class AdapterSubscription( + CurrentGameRuntimeAdapter owner, + IDisposable runtimeSubscription) : IDisposable + { + private CurrentGameRuntimeAdapter? _owner = owner; + private IDisposable? _runtimeSubscription = runtimeSubscription; + + public void Dispose() => DisposeCore(); + + public void DisposeCore() + { + IDisposable? runtime = Interlocked.Exchange( + ref _runtimeSubscription, + null); + if (runtime is null) + return; + runtime.Dispose(); + Interlocked.Exchange(ref _owner, null)?.Remove(this); + } } } diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs index 5dd07a36..0a101d2c 100644 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs +++ b/src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs @@ -30,25 +30,25 @@ internal sealed class CurrentGameRuntimeCommandAdapter private readonly LiveSessionController _session; private readonly LiveSessionHost _sessionHost; private readonly ICommandBus _commands; - private readonly CurrentGameRuntimeViewAdapter _view; + private readonly IGameRuntimeView _view; private readonly RuntimeInventoryState _inventory; private readonly RuntimeCharacterState _character; private readonly RuntimeActionState _actions; private readonly RuntimeLocalPlayerMovementState _movement; private readonly SelectionInteractionController _selection; - private readonly ICurrentGameRuntimeEventSink _events; + private readonly IGameRuntimeEventSink _events; public CurrentGameRuntimeCommandAdapter( LiveSessionController session, LiveSessionHost sessionHost, ICommandBus commands, - CurrentGameRuntimeViewAdapter view, + IGameRuntimeView view, RuntimeInventoryState inventory, RuntimeCharacterState character, RuntimeActionState actions, RuntimeLocalPlayerMovementState movement, SelectionInteractionController selection, - ICurrentGameRuntimeEventSink events) + IGameRuntimeEventSink events) { _session = session ?? throw new ArgumentNullException(nameof(session)); _sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost)); @@ -80,7 +80,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter ToCommandStatus(result.Status), result.CharacterId, result.CharacterName); - _events.EmitLifecycle(previous); + _events.EmitLifecycle(previous, _view.Lifecycle.State); return result; } @@ -100,7 +100,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter ToCommandStatus(result.Status), result.CharacterId, result.CharacterName); - _events.EmitLifecycle(previous); + _events.EmitLifecycle(previous, _view.Lifecycle.State); return result; } @@ -125,7 +125,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter operation: 2, acknowledgement.Status, text: acknowledgement.Error?.GetType().Name ?? string.Empty); - _events.EmitLifecycle(previous); + _events.EmitLifecycle(previous, _view.Lifecycle.State); return acknowledgement; } @@ -674,7 +674,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter RuntimeGenerationToken expectedGeneration, bool requireWorld) { - if (!_view.IsActive) + if (_view.Lifecycle.State == RuntimeLifecycleState.Disposed) return RuntimeCommandStatus.Inactive; if (expectedGeneration != _view.Generation) return RuntimeCommandStatus.StaleGeneration; diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeEventAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeEventAdapter.cs deleted file mode 100644 index b91d04c4..00000000 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeEventAdapter.cs +++ /dev/null @@ -1,275 +0,0 @@ -using AcDream.Runtime; -using AcDream.Runtime.Entities; -using AcDream.Runtime.Gameplay; - -namespace AcDream.App.Runtime; - -internal interface ICurrentGameRuntimeEventSink -{ - void EmitCommand( - RuntimeCommandDomain domain, - int operation, - RuntimeCommandStatus status, - uint primaryObjectId = 0u, - string? text = null); - - void EmitLifecycle(RuntimeLifecycleState previous); -} - -/// -/// Lazily projects existing owner notifications into one ordered diagnostic -/// stream. With no subscribers it attaches no callbacks and adds no per-event -/// production work. -/// -internal sealed class CurrentGameRuntimeEventAdapter - : IRuntimeEventSource, - ICurrentGameRuntimeEventSink, - IRuntimeEntityObjectObserver, - IRuntimeCommunicationObserver, - IDisposable -{ - private readonly CurrentGameRuntimeViewAdapter _view; - private readonly RuntimeEntityObjectLifetime _entityObjects; - private readonly RuntimeCommunicationState _communication; - private readonly object _observerGate = new(); - private IRuntimeEventObserver[] _observers = []; - private IDisposable? _entityObjectSubscription; - private IDisposable? _communicationSubscription; - private bool _ownerEventsAttached; - private bool _disposed; - - internal long DispatchFailureCount { get; private set; } - internal Exception? LastDispatchFailure { get; private set; } - - public CurrentGameRuntimeEventAdapter( - CurrentGameRuntimeViewAdapter view, - RuntimeEntityObjectLifetime entityObjects, - RuntimeCommunicationState communication) - { - _view = view ?? throw new ArgumentNullException(nameof(view)); - _entityObjects = entityObjects - ?? throw new ArgumentNullException(nameof(entityObjects)); - _communication = communication - ?? throw new ArgumentNullException(nameof(communication)); - } - - public IDisposable Subscribe(IRuntimeEventObserver observer) - { - ArgumentNullException.ThrowIfNull(observer); - lock (_observerGate) - { - ObjectDisposedException.ThrowIf(_disposed, this); - IRuntimeEventObserver[] current = _observers; - if (Array.IndexOf(current, observer) >= 0) - { - throw new InvalidOperationException( - "The runtime observer is already subscribed."); - } - - var replacement = new IRuntimeEventObserver[current.Length + 1]; - Array.Copy(current, replacement, current.Length); - replacement[^1] = observer; - Volatile.Write(ref _observers, replacement); - if (!_ownerEventsAttached) - AttachOwnerEvents(); - } - - return new ObserverSubscription(this, observer); - } - - public void Dispose() - { - lock (_observerGate) - { - if (_disposed) - return; - _disposed = true; - Volatile.Write(ref _observers, []); - DetachOwnerEvents(); - } - } - - public void EmitCommand( - RuntimeCommandDomain domain, - int operation, - RuntimeCommandStatus status, - uint primaryObjectId = 0u, - string? text = null) - { - if (!TryObservers(out IRuntimeEventObserver[] observers)) - return; - var delta = new RuntimeCommandDelta( - NextStamp(), - domain, - operation, - status, - primaryObjectId, - text); - foreach (IRuntimeEventObserver observer in observers) - { - try - { - observer.OnCommand(in delta); - } - catch (Exception error) - { - RecordDispatchFailure(error); - } - } - } - - public void EmitLifecycle(RuntimeLifecycleState previous) - { - RuntimeLifecycleState current = _view.Lifecycle.State; - if (current == previous - || !TryObservers(out IRuntimeEventObserver[] observers)) - { - return; - } - - var delta = new RuntimeLifecycleDelta( - NextStamp(), - previous, - current); - foreach (IRuntimeEventObserver observer in observers) - { - try - { - observer.OnLifecycle(in delta); - } - catch (Exception error) - { - RecordDispatchFailure(error); - } - } - } - - private void Unsubscribe(IRuntimeEventObserver observer) - { - lock (_observerGate) - { - IRuntimeEventObserver[] current = _observers; - int index = Array.IndexOf(current, observer); - if (index < 0) - return; - if (current.Length == 1) - { - Volatile.Write(ref _observers, []); - DetachOwnerEvents(); - return; - } - - var replacement = new IRuntimeEventObserver[current.Length - 1]; - if (index != 0) - Array.Copy(current, 0, replacement, 0, index); - if (index != current.Length - 1) - { - Array.Copy( - current, - index + 1, - replacement, - index, - current.Length - index - 1); - } - Volatile.Write(ref _observers, replacement); - } - } - - private void AttachOwnerEvents() - { - _entityObjectSubscription = _entityObjects.Events.Subscribe(this); - _communicationSubscription = _communication.Events.Subscribe(this); - _ownerEventsAttached = true; - } - - private void DetachOwnerEvents() - { - if (!_ownerEventsAttached) - return; - _communicationSubscription?.Dispose(); - _communicationSubscription = null; - _entityObjectSubscription?.Dispose(); - _entityObjectSubscription = null; - _ownerEventsAttached = false; - } - - public void OnEntity(in RuntimeEntityDelta delta) - { - if (!TryObservers(out IRuntimeEventObserver[] observers)) - return; - foreach (IRuntimeEventObserver observer in observers) - { - try - { - observer.OnEntity(in delta); - } - catch (Exception error) - { - RecordDispatchFailure(error); - } - } - } - - public void OnInventory(in RuntimeInventoryDelta delta) - { - if (!TryObservers(out IRuntimeEventObserver[] observers)) - return; - foreach (IRuntimeEventObserver observer in observers) - { - try - { - observer.OnInventory(in delta); - } - catch (Exception error) - { - RecordDispatchFailure(error); - } - } - } - - public void OnChat(in RuntimeCommunicationEvent committed) - { - if (!TryObservers(out IRuntimeEventObserver[] observers)) - return; - var delta = new RuntimeChatDelta( - NextStamp(), - committed.Entry); - foreach (IRuntimeEventObserver observer in observers) - { - try - { - observer.OnChat(in delta); - } - catch (Exception error) - { - RecordDispatchFailure(error); - } - } - } - - private RuntimeEventStamp NextStamp() => - _entityObjects.Events.NextStamp(); - - private bool TryObservers(out IRuntimeEventObserver[] observers) - { - observers = Volatile.Read(ref _observers); - return observers.Length != 0; - } - - private void RecordDispatchFailure(Exception error) - { - DispatchFailureCount++; - LastDispatchFailure = error; - } - - private sealed class ObserverSubscription( - CurrentGameRuntimeEventAdapter owner, - IRuntimeEventObserver observer) - : IDisposable - { - private CurrentGameRuntimeEventAdapter? _owner = owner; - - public void Dispose() => - Interlocked.Exchange(ref _owner, null)?.Unsubscribe(observer); - } -} diff --git a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs b/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs deleted file mode 100644 index bfbcdddb..00000000 --- a/src/AcDream.App/Runtime/CurrentGameRuntimeViewAdapter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using AcDream.App.Input; -using AcDream.App.Net; -using AcDream.Core.Chat; -using AcDream.Core.Items; -using AcDream.Runtime; -using AcDream.Runtime.Entities; -using AcDream.Runtime.Gameplay; -using AcDream.Runtime.Session; -using AcDream.Runtime.World; - -namespace AcDream.App.Runtime; - -/// -/// Read-only borrowed projections over the current App owners. Enumeration is -/// synchronous and allocation-free; no mutable collection is copied or owned. -/// -internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView -{ - private readonly LiveSessionController _session; - private readonly LocalPlayerIdentityState _playerIdentity; - private readonly IGameRuntimeClock _clock; - private readonly IRuntimeEntityView _entityView; - private readonly IRuntimeInventoryView _inventoryView; - private readonly IRuntimeInventoryStateView _inventoryStateView; - private readonly IRuntimeCharacterView _characterView; - private readonly IRuntimeSocialView _socialView; - private readonly IRuntimeChatView _chatView; - private readonly IRuntimeActionView _actionView; - private readonly IRuntimeMovementView _movementView; - private readonly IRuntimeWorldEnvironmentView _environmentView; - private readonly IRuntimePortalView _portalView; - private bool _active = true; - - public CurrentGameRuntimeViewAdapter( - LiveSessionController session, - LocalPlayerIdentityState playerIdentity, - RuntimeEntityObjectLifetime entityObjects, - RuntimeInventoryState inventory, - RuntimeCharacterState character, - RuntimeCommunicationState communication, - RuntimeActionState actions, - RuntimeLocalPlayerMovementState movement, - RuntimeWorldEnvironmentState environment, - RuntimeWorldTransitState worldTransit, - IGameRuntimeClock clock) - { - _session = session ?? throw new ArgumentNullException(nameof(session)); - _playerIdentity = playerIdentity - ?? throw new ArgumentNullException(nameof(playerIdentity)); - ArgumentNullException.ThrowIfNull(entityObjects); - ArgumentNullException.ThrowIfNull(communication); - _clock = clock ?? throw new ArgumentNullException(nameof(clock)); - - _entityView = entityObjects.EntityView; - _inventoryView = entityObjects.InventoryView; - _inventoryStateView = ( - inventory ?? throw new ArgumentNullException(nameof(inventory))).View; - _characterView = ( - character ?? throw new ArgumentNullException(nameof(character))).View; - _chatView = communication.View; - _socialView = communication.SocialView; - _actionView = ( - actions ?? throw new ArgumentNullException(nameof(actions))).View; - _movementView = ( - movement ?? throw new ArgumentNullException(nameof(movement))).View; - _environmentView = environment - ?? throw new ArgumentNullException(nameof(environment)); - _portalView = worldTransit - ?? throw new ArgumentNullException(nameof(worldTransit)); - } - - internal bool IsActive => _active && !_session.IsDisposalComplete; - - public RuntimeGenerationToken Generation => _session.Generation; - - public RuntimeLifecycleSnapshot Lifecycle - { - get - { - RuntimeLifecycleState state; - if (!IsActive) - state = RuntimeLifecycleState.Disposed; - else if (_session.IsInWorld) - state = RuntimeLifecycleState.InWorld; - else if (_session.CurrentSession is not null) - state = RuntimeLifecycleState.Starting; - else if (_session.SessionGeneration == 0UL) - state = RuntimeLifecycleState.Constructed; - else - state = RuntimeLifecycleState.Stopped; - - return new RuntimeLifecycleSnapshot( - Generation, - state, - _playerIdentity.ServerGuid, - _session.CurrentSession is not null); - } - } - - public IGameRuntimeClock Clock => _clock; - public IRuntimeEntityView Entities => _entityView; - public IRuntimeInventoryView Inventory => _inventoryView; - public IRuntimeInventoryStateView InventoryState => _inventoryStateView; - public IRuntimeCharacterView Character => _characterView; - public IRuntimeSocialView Social => _socialView; - public IRuntimeChatView Chat => _chatView; - public IRuntimeActionView Actions => _actionView; - public IRuntimeMovementView Movement => _movementView; - public IRuntimeWorldEnvironmentView Environment => _environmentView; - public IRuntimePortalView Portal => _portalView; - - public RuntimeStateCheckpoint CaptureCheckpoint() => - new( - Generation, - Lifecycle.State, - _clock.FrameNumber, - _entityView.Count, - _entityView.MaterializedCount, - _inventoryView.ObjectCount, - _inventoryView.ContainerCount, - _inventoryStateView.Snapshot, - _characterView.Snapshot, - _socialView.Snapshot, - _chatView.Revision, - _chatView.Count, - _actionView.Snapshot, - _movementView.Snapshot, - _environmentView.Snapshot, - _environmentView.Ownership, - _portalView.Snapshot, - _portalView.Ownership); - - internal void Deactivate() => _active = false; -} diff --git a/src/AcDream.App/Update/UpdateFrameOrchestrator.cs b/src/AcDream.App/Update/UpdateFrameOrchestrator.cs index 5bf0cdd2..b53a9427 100644 --- a/src/AcDream.App/Update/UpdateFrameOrchestrator.cs +++ b/src/AcDream.App/Update/UpdateFrameOrchestrator.cs @@ -31,7 +31,17 @@ internal sealed class UpdateFrameClock : IPhysicsScriptTimeSource, IGameRuntimeClock { - private readonly GameRuntimeClock _runtime = new(); + private readonly GameRuntimeClock _runtime; + + public UpdateFrameClock() + : this(new GameRuntimeClock()) + { + } + + public UpdateFrameClock(GameRuntimeClock runtime) + { + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + } public ulong FrameNumber => _runtime.FrameNumber; public double SimulationTimeSeconds => _runtime.SimulationTimeSeconds; diff --git a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs index aaff8615..6573b198 100644 --- a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs @@ -109,6 +109,8 @@ public sealed class ContentEffectsAudioCompositionTests Assert.Throws(() => fixture.Phase().Compose( fixture.Platform, fixture.Host)); + Assert.False( + fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); Assert.NotNull(fixture.Publication.Audio); Assert.False(fixture.Publication.Audio!.Engine.IsDisposalComplete); @@ -127,6 +129,8 @@ public sealed class ContentEffectsAudioCompositionTests Assert.Throws(() => fixture.Phase().Compose( fixture.Platform, fixture.Host)); + Assert.False( + fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); Assert.Equal( Enum.GetValues() @@ -258,7 +262,7 @@ public sealed class ContentEffectsAudioCompositionTests ResidencyBudgetOptions.Default, new PhysicsDataCache(), false, - new RuntimeCharacterState(), + GameRuntimeTestFactory.Create(), Router, Poses, new DeferredEntityEffectAdvanceSource(), @@ -293,7 +297,7 @@ public sealed class ContentEffectsAudioCompositionTests public void Dispose() { Publication.Dispose(); - Dependencies.Character.Dispose(); + Dependencies.Runtime.Dispose(); } } diff --git a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs index 2172b563..8d3f4fcc 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs @@ -10,6 +10,7 @@ using AcDream.App.World; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Spells; +using AcDream.Runtime; using AcDream.Runtime.Gameplay; namespace AcDream.App.Tests.Composition; @@ -33,7 +34,7 @@ public sealed class InteractionRetainedUiCompositionTests [Fact] public void EnabledUiPublishesOneExactResultAfterFrozenConstructionOrder() { - var fixture = new Fixture(retailUi: true); + using var fixture = new Fixture(retailUi: true); InteractionRetainedUiResult result = fixture.Compose(); @@ -56,7 +57,7 @@ public sealed class InteractionRetainedUiCompositionTests [Fact] public void DisabledUiAcquiresNoRetainedFrontendResource() { - var fixture = new Fixture(retailUi: false); + using var fixture = new Fixture(retailUi: false); InteractionRetainedUiResult result = fixture.Compose(); @@ -81,11 +82,13 @@ public sealed class InteractionRetainedUiCompositionTests int pointValue) { var point = (InteractionRetainedUiCompositionPoint)pointValue; - var fixture = new Fixture(retailUi: true, failurePoint: point); + using var fixture = new Fixture(retailUi: true, failurePoint: point); Assert.Throws(fixture.Compose); Assert.Equal(point, fixture.Points[^1]); + Assert.False( + fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); if (point == InteractionRetainedUiCompositionPoint.ResultPublished) { Assert.Empty(fixture.Factory.Releases); @@ -129,9 +132,11 @@ public sealed class InteractionRetainedUiCompositionTests [Fact] public void PublicationFailureRollsBackCompleteUnpublishedPrefix() { - var fixture = new Fixture(retailUi: true, publicationFailure: true); + using var fixture = new Fixture(retailUi: true, publicationFailure: true); Assert.Throws(fixture.Compose); + Assert.False( + fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); Assert.Equal( [ @@ -161,7 +166,7 @@ public sealed class InteractionRetainedUiCompositionTests Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source); } - private sealed class Fixture + private sealed class Fixture : IDisposable { private readonly InteractionRetainedUiCompositionPoint? _failurePoint; @@ -175,9 +180,7 @@ public sealed class InteractionRetainedUiCompositionTests Publication = new Publication(publicationFailure); RuntimeOptions options = RuntimeOptions.Parse("dat", static _ => null) with { RetailUi = retailUi }; - var actions = new RuntimeActionState( - new InventoryTransactionState(new ClientObjectTable()), - new Spellbook(), + GameRuntime runtime = GameRuntimeTestFactory.Create( new NoopCombatOperations(), new NoopCombatTargetOperations(), new NoopCombatModeOperations(), @@ -196,19 +199,15 @@ public sealed class InteractionRetainedUiCompositionTests RetainedInputCapture: null!, InputDispatcher: null, Settings: null!, - Actions: actions, + Runtime: runtime, CombatAttackOperations: new NoopCombatOperations(), CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(), SpellCastOperations: new RuntimeSpellCastOperationsSlot(), - Inventory: null!, MagicCatalog: null!, - Character: null!, - Communication: null!, StackSplitQuantity: null!, UiRegistry: null, CombatModeCommands: null!, PlayerIdentity: null!, - PlayerController: null!, PlayerMode: null!, SelectionCameraFactory: static _ => Stub(), FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(), @@ -235,6 +234,8 @@ public sealed class InteractionRetainedUiCompositionTests if (_failurePoint == point) throw new InvalidOperationException($"fault at {point}"); }).Compose(); + + public void Dispose() => Dependencies.Runtime.Dispose(); } private sealed class FakeFactory : IInteractionRetainedUiCompositionFactory diff --git a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs index d02fc386..e858eff3 100644 --- a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs @@ -88,7 +88,10 @@ public sealed class SessionPlayerCompositionTests window); Assert.DoesNotContain("IsSpawnClaimUnhydratable", window); Assert.Contains("LandblockStreamer.CreateForRequests(", phase); - Assert.Contains("new LiveSessionController()", phase); + Assert.Contains( + "LiveSessionController liveSession = d.Runtime.Session;", + phase); + Assert.DoesNotContain("new LiveSessionController()", phase); Assert.Contains("new LocalPlayerTeleportController(", phase); Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase); } @@ -109,7 +112,7 @@ public sealed class SessionPlayerCompositionTests "streamerLease.Resource.Start();", "new StreamingController(", "new WorldRevealCoordinator(", - "new LiveSessionController()", + "LiveSessionController liveSession = d.Runtime.Session;", "new LiveEntityHydrationController(", "new LiveEntityNetworkUpdateController(", "new GameplayInputFrameController(", diff --git a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs index f9b3436a..0ef2b5be 100644 --- a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs @@ -33,6 +33,8 @@ public sealed class SettingsDevToolsCompositionTests SettingsDevToolsResult result = fixture.Compose(); Assert.Null(result.DevTools); + Assert.False( + fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested); Assert.Equal(1, fixture.Startup.DisplayCalls); Assert.Equal(1, fixture.Startup.AudioCalls); Assert.Equal(0, fixture.Factory.Calls); @@ -229,9 +231,7 @@ public sealed class SettingsDevToolsCompositionTests Settings, Startup, new HostQuiescenceGate(), - new RuntimeCommunicationState(), - new CombatState(), - new LocalPlayerState(new Spellbook()), + GameRuntimeTestFactory.Create(), enabled ? new SettingsDevToolsOptionalDependencies( new Facts(), @@ -273,6 +273,7 @@ public sealed class SettingsDevToolsCompositionTests public void Dispose() { + Dependencies.Runtime.Dispose(); Publication.Owner?.Dispose(); _dispatcher.Dispose(); _profiler.Dispose(); diff --git a/tests/AcDream.App.Tests/GameRuntimeTestFactory.cs b/tests/AcDream.App.Tests/GameRuntimeTestFactory.cs new file mode 100644 index 00000000..d4cb02d3 --- /dev/null +++ b/tests/AcDream.App.Tests/GameRuntimeTestFactory.cs @@ -0,0 +1,62 @@ +using AcDream.Core.Combat; +using AcDream.Core.Items; +using AcDream.Core.Spells; +using AcDream.Runtime; +using AcDream.Runtime.Gameplay; +using AcDream.Runtime.Session; + +namespace AcDream.App.Tests; + +internal static class GameRuntimeTestFactory +{ + public static GameRuntime Create( + IRuntimeCombatAttackOperations? combatAttack = null, + IRuntimeCombatTargetOperations? combatTarget = null, + IRuntimeCombatModeOperations? combatMode = null, + IRuntimeSpellCastOperations? spellCast = null, + ILiveSessionOperations? session = null, + Func? combatTime = null) + { + var fallback = new NoopOperations(); + return new GameRuntime(new GameRuntimeDependencies( + combatAttack ?? fallback, + combatTarget ?? fallback, + combatMode ?? fallback, + spellCast ?? fallback, + SessionOperations: session, + CombatTime: combatTime)); + } + + private sealed class NoopOperations : + IRuntimeCombatAttackOperations, + IRuntimeCombatTargetOperations, + IRuntimeCombatModeOperations, + IRuntimeSpellCastOperations + { + public bool CanStartAttack() => false; + public void PrepareAttackRequest() { } + public bool SendAttack(AttackHeight height, float power) => false; + public void SendCancelAttack() { } + public bool IsDualWield => false; + public bool PlayerReadyForAttack => false; + public bool AutoRepeatAttack => false; + public bool AutoTarget => false; + public uint? SelectClosestTarget() => null; + public bool IsInWorld => false; + public IReadOnlyList GetOrderedEquipment() => []; + public void NotifyExplicitCombatModeRequest() { } + public void SendChangeCombatMode(CombatMode mode) { } + public uint LocalPlayerId => 0u; + public bool CanSend => false; + public bool HasRequiredComponents(uint spellId) => false; + public bool IsTargetCompatible( + uint targetId, + SpellMetadata spell, + bool showMessage) => false; + public void StopCompletely() { } + public void SendUntargeted(uint spellId) { } + public void SendTargeted(uint targetId, uint spellId) { } + public void DisplayMessage(string message) { } + public void IncrementBusy() { } + } +} diff --git a/tests/AcDream.App.Tests/Net/GameWindowLiveSessionOwnershipTests.cs b/tests/AcDream.App.Tests/Net/GameWindowLiveSessionOwnershipTests.cs index 508c3545..1b38412c 100644 --- a/tests/AcDream.App.Tests/Net/GameWindowLiveSessionOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Net/GameWindowLiveSessionOwnershipTests.cs @@ -13,18 +13,22 @@ public sealed class GameWindowLiveSessionOwnershipTests BindingFlags.Instance | BindingFlags.NonPublic; [Fact] - public void GameWindowRetainsControllerAndFocusedHostButNoMirroredSession() + public void GameWindowRetainsCanonicalRuntimeAndFocusedHostButNoMirroredSession() { FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance); Assert.Contains( fields, - field => field.Name == "_liveSessionController" - && field.FieldType == typeof(LiveSessionController)); + field => field.Name == "_runtime" + && field.FieldType == typeof(GameRuntime)); Assert.Contains( fields, field => field.Name == "_liveSessionHost" && field.FieldType == typeof(LiveSessionHost)); + Assert.DoesNotContain( + fields, + field => field.Name == "_liveSessionController" + || field.FieldType == typeof(LiveSessionController)); Assert.DoesNotContain(fields, field => field.Name == "_liveSession"); Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession)); Assert.DoesNotContain(fields, field => field.FieldType == typeof(LiveSessionResetPlan)); @@ -54,6 +58,48 @@ public sealed class GameWindowLiveSessionOwnershipTests || field.FieldType == typeof(ulong)); } + [Fact] + public void ProductionWindowConstructsOnlyTheCanonicalRuntimeRoot() + { + string root = FindRepositoryRoot(); + string source = File.ReadAllText(Path.Combine( + root, + "src", + "AcDream.App", + "Rendering", + "GameWindow.cs")); + + Assert.Equal( + 1, + CountOccurrences(source, "new GameRuntime(")); + Assert.Contains( + "private readonly GameRuntime _runtime;", + source, + StringComparison.Ordinal); + Assert.Contains( + "_runtimeHostLease = _runtime.AcquireHostLease(", + source, + StringComparison.Ordinal); + string[] forbidden = + [ + "new RuntimeEntityObjectLifetime(", + "new RuntimeInventoryState(", + "new RuntimeCharacterState(", + "new RuntimeCommunicationState(", + "new RuntimeActionState(", + "new RuntimeLocalPlayerMovementState(", + "new RuntimeWorldTransitState(", + "new LiveSessionController(", + "new GameRuntimeClock(", + ]; + Assert.All( + forbidden, + value => Assert.DoesNotContain( + value, + source, + StringComparison.Ordinal)); + } + [Theory] [InlineData("TryStartLiveSession")] [InlineData("ClearInboundEntityState")] @@ -66,4 +112,32 @@ public sealed class GameWindowLiveSessionOwnershipTests { Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateInstance)); } + + private static int CountOccurrences(string source, string value) + { + int count = 0; + int cursor = 0; + while ((cursor = source.IndexOf( + value, + cursor, + StringComparison.Ordinal)) >= 0) + { + count++; + cursor += value.Length; + } + return count; + } + + private static string FindRepositoryRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx"))) + return current.FullName; + current = current.Parent; + } + + throw new DirectoryNotFoundException("AcDream.slnx was not found."); + } } diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index 8efa0002..21a71fa7 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -258,7 +258,7 @@ public sealed class GameWindowSlice8BoundaryTests "Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)", "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", "Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())", - "Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))", + "Hard(\"game runtime session\", ingress.Runtime.StopSession)", "new ResourceShutdownStage(\"physical ingress cleanup\"", "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", @@ -469,12 +469,11 @@ public sealed class GameWindowSlice8BoundaryTests "new ResourceShutdownStage(\"frame borrowers\"", "new ResourceShutdownStage(\"session dependents\"", "new ResourceShutdownStage(\"live entities\"", - "new ResourceShutdownStage(\"runtime entity/object stream\"", "new ResourceShutdownStage(\"effect dispatch edges\"", "new ResourceShutdownStage(\"live entity dependents\"", "new ResourceShutdownStage(\"submitted GPU work\"", "new ResourceShutdownStage(\"render frontends\"", - "new ResourceShutdownStage(\"runtime communication state\"", + "new ResourceShutdownStage(\"game runtime root\"", "new ResourceShutdownStage(\"shared texture owners\"", "new ResourceShutdownStage(\"mesh adapter\"", "new ResourceShutdownStage(\"remaining render owners\"", @@ -496,7 +495,7 @@ public sealed class GameWindowSlice8BoundaryTests "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", "Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())", "Hard(\"camera pointer\", () => ingress.CameraPointer?.Deactivate())", - "Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))", + "Hard(\"game runtime session\", ingress.Runtime.StopSession)", "new ResourceShutdownStage(\"physical ingress cleanup\"", "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", diff --git a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs index 89754f7d..87025d8c 100644 --- a/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs +++ b/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs @@ -762,43 +762,40 @@ public sealed class CurrentGameRuntimeAdapterTests private readonly ItemInteractionController _items; private readonly LiveSessionController _session; private readonly IDisposable _combatModeBinding; + private readonly GameRuntime _gameRuntime; public Harness() { - Identity = new LocalPlayerIdentityState(); - EntityObjects = new RuntimeEntityObjectLifetime(); - InventoryState = new RuntimeInventoryState(EntityObjects); - Character = new RuntimeCharacterState(); - Entities = new LiveEntityRuntime( - new GpuWorldState(), - new NoopEntityResources(), - EntityObjects); - Objects = EntityObjects.Objects; - Communication = new RuntimeCommunicationState(); CombatAttackOperations = new CombatAttackOperationsSlot(); CombatModeOperations = new RuntimeCombatModeOperationsSlot(); SpellCastOperations = new RuntimeSpellCastOperationsSlot(); - Actions = new RuntimeActionState( - InventoryState.Transactions, - Character.Spellbook, + Options = LiveOptions(); + Commands = new RecordingCommandRouting(); + Transport = new TestTransport(); + _gameRuntime = GameRuntimeTestFactory.Create( CombatAttackOperations, new RuntimeCombatTargetOperationsSlot(), CombatModeOperations, SpellCastOperations, - now: static () => 0d); + new SessionOperations(Transport), + combatTime: static () => 0d); + _session = _gameRuntime.Session; + Identity = new LocalPlayerIdentityState( + _gameRuntime.PlayerIdentity); + Entities = new LiveEntityRuntime( + new GpuWorldState(), + new NoopEntityResources(), + EntityObjects); CombatMode = new RecordingCombatModeOperations(); _combatModeBinding = CombatModeOperations.BindOwned(CombatMode); - MovementState = new RuntimeLocalPlayerMovementState(); - Environment = new RuntimeWorldEnvironmentState(); MovementInput = new DispatcherMovementInputSource(MovementState); GameplayInput = new GameplayInputFrameController( dispatcher: null, MovementInput, mouseLook: null, new NoopCombatInput()); - Clock = new UpdateFrameClock(); - WorldTransit = new RuntimeWorldTransitState(); + Clock = new UpdateFrameClock(_gameRuntime.Clock); WorldReveal = new WorldRevealCoordinator( WorldTransit, static (_, _) => true, @@ -826,50 +823,42 @@ public sealed class CurrentGameRuntimeAdapterTests new SelectionTransport(() => _session?.IsInWorld == true), new NoopInteractionMovement()); - Options = LiveOptions(); - Commands = new RecordingCommandRouting(); - Transport = new TestTransport(); - _session = new LiveSessionController( - new SessionOperations(Transport)); Host = CreateHost(_session, Commands, Identity); Runtime = new CurrentGameRuntimeAdapter( - _session, + _gameRuntime, Host, Commands, - Identity, - EntityObjects, - InventoryState, - Character, - Communication, - Actions, - MovementState, - Environment, - WorldTransit, - Clock, selectionController); } public RuntimeOptions Options { get; } public LocalPlayerIdentityState Identity { get; } - public RuntimeEntityObjectLifetime EntityObjects { get; } - public RuntimeInventoryState InventoryState { get; } - public RuntimeCharacterState Character { get; } + public RuntimeEntityObjectLifetime EntityObjects => + _gameRuntime.EntityObjects; + public RuntimeInventoryState InventoryState => + _gameRuntime.InventoryOwner; + public RuntimeCharacterState Character => + _gameRuntime.CharacterOwner; public LiveEntityRuntime Entities { get; } - public ClientObjectTable Objects { get; } - public RuntimeCommunicationState Communication { get; } + public ClientObjectTable Objects => EntityObjects.Objects; + public RuntimeCommunicationState Communication => + _gameRuntime.CommunicationOwner; public ChatLog Chat => Communication.Chat; public CombatAttackOperationsSlot CombatAttackOperations { get; } public RuntimeCombatModeOperationsSlot CombatModeOperations { get; } public RuntimeSpellCastOperationsSlot SpellCastOperations { get; } - public RuntimeActionState Actions { get; } + public RuntimeActionState Actions => _gameRuntime.ActionOwner; public SelectionState Selection => Actions.Selection; - public RuntimeLocalPlayerMovementState MovementState { get; } - public RuntimeWorldEnvironmentState Environment { get; } + public RuntimeLocalPlayerMovementState MovementState => + _gameRuntime.MovementOwner; + public RuntimeWorldEnvironmentState Environment => + _gameRuntime.EnvironmentOwner; public DispatcherMovementInputSource MovementInput { get; } public GameplayInputFrameController GameplayInput { get; } public RecordingCombatModeOperations CombatMode { get; } public UpdateFrameClock Clock { get; } - public RuntimeWorldTransitState WorldTransit { get; } + public RuntimeWorldTransitState WorldTransit => + _gameRuntime.TransitOwner; public WorldRevealCoordinator WorldReveal { get; } public RecordingCommandRouting Commands { get; } public TestTransport Transport { get; } @@ -879,15 +868,11 @@ public sealed class CurrentGameRuntimeAdapterTests public void Dispose() { Runtime.Dispose(); - Character.Dispose(); - Communication.Dispose(); - _session.Dispose(); _items.Dispose(); _combatModeBinding.Dispose(); - Actions.Dispose(); - InventoryState.Dispose(); Entities.Clear(); - MovementState.Dispose(); + WorldReveal.ResetSession(); + _gameRuntime.Dispose(); } } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs index 4b5c0722..0458ecc6 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs @@ -15,11 +15,11 @@ public sealed class RuntimeActionOwnershipTests string program = ReadAppSource(root, "Program.cs"); Assert.Contains( - "private readonly RuntimeActionState _runtimeActions;", + "private readonly GameRuntime _runtime;", gameWindow, StringComparison.Ordinal); Assert.Contains( - "_runtimeActions = new RuntimeActionState(", + "_runtime = new GameRuntime(new GameRuntimeDependencies(", gameWindow, StringComparison.Ordinal); Assert.Contains( @@ -73,14 +73,9 @@ public sealed class RuntimeActionOwnershipTests Assert.Empty(Regex.Matches( production, @"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?RuntimeSpellCastState\s*\(")); - Assert.Equal( - 1, - Regex.Matches( - production, - @"\bnew\s+RuntimeActionState\s*\(").Count - + Regex.Matches( - production, - @"RuntimeActionState\s+\w+\s*=\s*new\s*\(\s*\)").Count); + Assert.Empty(Regex.Matches( + production, + @"\bnew\s+RuntimeActionState\s*\(")); } [Fact] @@ -138,14 +133,13 @@ public sealed class RuntimeActionOwnershipTests itemInteraction, StringComparison.Ordinal); Assert.Contains( - "\"runtime action state\"", + "new ResourceShutdownStage(\"game runtime root\"", shutdown, StringComparison.Ordinal); - AssertAppearsInOrder( + Assert.DoesNotContain( + "RuntimeActionState Actions", shutdown, - "\"runtime action state\"", - "\"runtime inventory state\"", - "\"runtime entity/object lifetime\""); + StringComparison.Ordinal); Assert.False(File.Exists(Path.Combine( root, "src", diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs index a8c299c8..77e2a1bb 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs @@ -8,7 +8,15 @@ public sealed class RuntimeCharacterOwnershipTests string source = ReadSource("Rendering", "GameWindow.cs"); Assert.Contains( - "_runtimeCharacter = new RuntimeCharacterState();", + "private readonly GameRuntime _runtime;", + source, + StringComparison.Ordinal); + Assert.Contains( + "_runtime = new GameRuntime(new GameRuntimeDependencies(", + source, + StringComparison.Ordinal); + Assert.DoesNotContain( + "new RuntimeCharacterState(", source, StringComparison.Ordinal); Assert.DoesNotContain( @@ -76,7 +84,11 @@ public sealed class RuntimeCharacterOwnershipTests inventory, StringComparison.Ordinal); Assert.Contains( - "\"runtime character state\"", + "new ResourceShutdownStage(\"game runtime root\"", + shutdown, + StringComparison.Ordinal); + Assert.DoesNotContain( + "RuntimeCharacterState Character", shutdown, StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs index 43fa0675..fc3a0357 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs @@ -8,7 +8,11 @@ public sealed class RuntimeInventoryOwnershipTests string source = ReadSource("Rendering", "GameWindow.cs"); Assert.Contains( - "_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);", + "private readonly GameRuntime _runtime;", + source, + StringComparison.Ordinal); + Assert.DoesNotContain( + "new RuntimeInventoryState(", source, StringComparison.Ordinal); Assert.DoesNotContain( @@ -77,11 +81,18 @@ public sealed class RuntimeInventoryOwnershipTests "_domain.Inventory.DesiredComponents", session, StringComparison.Ordinal); - AssertAppearsInOrder( + Assert.Contains( + "new ResourceShutdownStage(\"game runtime root\"", shutdown, - "\"runtime action state\"", - "\"runtime inventory state\"", - "\"runtime entity/object lifetime\""); + StringComparison.Ordinal); + Assert.Contains( + "Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))", + shutdown, + StringComparison.Ordinal); + Assert.DoesNotContain( + "RuntimeInventoryState Inventory", + shutdown, + StringComparison.Ordinal); } [Fact] diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs index 259c4a26..376cea1a 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs @@ -36,10 +36,14 @@ public sealed class RuntimeMovementOwnershipTests app, StringComparison.Ordinal); Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b")); - Assert.Single(Regex.Matches( + Assert.Empty(Regex.Matches( app, @"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(") .Cast()); + Assert.Single(Regex.Matches( + runtime, + @"new\s+RuntimeLocalPlayerMovementState\s*\(") + .Cast()); Assert.DoesNotContain( "ACDREAM_RUN_SKILL", runtime, @@ -59,10 +63,10 @@ public sealed class RuntimeMovementOwnershipTests root, "Input", "DispatcherMovementInputSource.cs"); - string view = ReadAppSource( + string adapter = ReadAppSource( root, "Runtime", - "CurrentGameRuntimeViewAdapter.cs"); + "CurrentGameRuntimeAdapter.cs"); string commands = ReadAppSource( root, "Runtime", @@ -73,7 +77,11 @@ public sealed class RuntimeMovementOwnershipTests "GameWindowLifetime.cs"); Assert.Contains( - "RuntimeLocalPlayerMovementState _playerControllerSlot = new();", + "RuntimeLocalPlayerMovementState _playerControllerSlot =>", + gameWindow, + StringComparison.Ordinal); + Assert.Contains( + "_runtime.MovementOwner;", gameWindow, StringComparison.Ordinal); Assert.Contains( @@ -90,16 +98,16 @@ public sealed class RuntimeMovementOwnershipTests StringComparison.Ordinal); Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal); Assert.Contains( - "movement ?? throw new ArgumentNullException(nameof(movement))).View", - view, + "private readonly GameRuntime _runtime;", + adapter, StringComparison.Ordinal); Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal); Assert.Contains( - "RuntimeLocalPlayerMovementState Movement", + "GameRuntime Runtime", lifetime, StringComparison.Ordinal); Assert.Contains( - "\"runtime local movement state\"", + "\"game runtime root\"", lifetime, StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs index b88b1b4c..2a0cf700 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeWorldTransitOwnershipTests.cs @@ -17,6 +17,11 @@ public sealed class RuntimeWorldTransitOwnershipTests "\n", Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories) .Select(File.ReadAllText)); + string runtimeRoot = Path.Combine(root, "src", "AcDream.Runtime"); + string runtime = string.Join( + "\n", + Directory.EnumerateFiles(runtimeRoot, "*.cs", SearchOption.AllDirectories) + .Select(File.ReadAllText)); Assert.False(File.Exists(Path.Combine( appRoot, @@ -38,10 +43,14 @@ public sealed class RuntimeWorldTransitOwnershipTests "record struct WorldRevealLifecycleSnapshot", app, StringComparison.Ordinal); - Assert.Single(Regex.Matches( + Assert.Empty(Regex.Matches( app, @"new\s+RuntimeWorldTransitState\s*\(") .Cast()); + Assert.Single(Regex.Matches( + runtime, + @"new\s+RuntimeWorldTransitState\s*\(") + .Cast()); } [Fact] @@ -56,19 +65,17 @@ public sealed class RuntimeWorldTransitOwnershipTests typeof(RuntimeWorldTransitState), availabilityFields[0].FieldType); - var viewFields = typeof(CurrentGameRuntimeViewAdapter) + var adapterFields = typeof(CurrentGameRuntimeAdapter) .GetFields( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); Assert.Contains( - viewFields, - field => field.FieldType == typeof(IRuntimePortalView)); + adapterFields, + field => field.FieldType == typeof(GameRuntime)); Assert.DoesNotContain( - typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes( - System.Reflection.BindingFlags.NonPublic), - type => type.Name.Contains( - "PortalView", - StringComparison.Ordinal)); + adapterFields, + field => field.FieldType == typeof(IRuntimePortalView) + || field.FieldType == typeof(RuntimeWorldTransitState)); var coordinatorFields = typeof(WorldRevealCoordinator) .GetFields( diff --git a/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs index 064ffe12..1de0a773 100644 --- a/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs @@ -363,7 +363,7 @@ public sealed class LandblockBuildOriginTests Assert.True(sessionStage >= 0); int sessionOperation = source.IndexOf( - "Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))", + "Hard(\"game runtime session\", ingress.Runtime.StopSession)", sessionStage, StringComparison.Ordinal); Assert.True(sessionOperation > sessionStage); @@ -380,25 +380,30 @@ public sealed class LandblockBuildOriginTests StringComparison.Ordinal); Assert.True(streamerDispose > dependentStage); - int helper = source.IndexOf( - "private static void DisposeLiveSession(LiveSessionController? controller)", + string runtime = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.Runtime", + "GameRuntime.cs")); + int helper = runtime.IndexOf( + "public void StopSession()", StringComparison.Ordinal); - Assert.True(helper > streamerDispose); - int sessionDispose = source.IndexOf( - "controller.Dispose()", + Assert.True(helper >= 0); + int sessionDispose = runtime.IndexOf( + "Session.Dispose();", helper, StringComparison.Ordinal); Assert.True(sessionDispose > helper); - int disposalCompletionBarrier = source.IndexOf( - "if (!controller.IsDisposalComplete)", + int disposalCompletionBarrier = runtime.IndexOf( + "if (!Session.IsDisposalComplete)", sessionDispose, StringComparison.Ordinal); Assert.True(disposalCompletionBarrier > sessionDispose); Assert.Contains( - "Live-session disposal was deferred by a reentrant lifecycle callback.", - source[disposalCompletionBarrier..], + "The Runtime session shutdown was deferred by a re-entrant callback.", + runtime[disposalCompletionBarrier..], StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs index 1c9217d7..38e9d976 100644 --- a/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimeEntityOwnershipTests.cs @@ -236,34 +236,18 @@ public sealed class RuntimeEntityOwnershipTests [Fact] public void CurrentRuntimeAdapters_DoNotRetainEntityOrInventoryMirrors() { - FieldInfo[] viewFields = typeof(CurrentGameRuntimeViewAdapter) + FieldInfo[] adapterFields = typeof(CurrentGameRuntimeAdapter) .GetFields(BindingFlags.Instance | BindingFlags.NonPublic); Assert.Contains( - viewFields, - field => field.FieldType == typeof(IRuntimeEntityView)); - Assert.Contains( - viewFields, - field => field.FieldType == typeof(IRuntimeInventoryView)); + adapterFields, + field => field.FieldType == typeof(GameRuntime)); Assert.DoesNotContain( - viewFields, - field => field.FieldType == typeof(LiveEntityRuntime) - || field.FieldType == typeof(ClientObjectTable)); - Assert.DoesNotContain( - typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes( - BindingFlags.NonPublic), - type => type.Name is "EntityView" or "InventoryView"); - - FieldInfo[] eventFields = typeof(CurrentGameRuntimeEventAdapter) - .GetFields(BindingFlags.Instance | BindingFlags.NonPublic); - Assert.DoesNotContain( - eventFields, + adapterFields, field => field.FieldType == typeof(LiveEntityRuntime) || field.FieldType == typeof(ClientObjectTable) - || field.FieldType == typeof(RuntimeEventSequencer)); - Assert.Contains( - eventFields, - field => field.FieldType - == typeof(RuntimeEntityObjectLifetime)); + || field.FieldType == typeof(IRuntimeEntityView) + || field.FieldType == typeof(IRuntimeInventoryView) + || field.FieldType == typeof(RuntimeEntityObjectLifetime)); string root = FindRepositoryRoot(); string liveSource = File.ReadAllText(Path.Combine( @@ -276,19 +260,34 @@ public sealed class RuntimeEntityOwnershipTests Assert.DoesNotContain("_directory.TryApply", liveSource); Assert.DoesNotContain("_directory.RemoveActive", liveSource); - string viewSource = File.ReadAllText(Path.Combine( + string appRuntimeRoot = Path.Combine( root, "src", "AcDream.App", - "Runtime", - "CurrentGameRuntimeViewAdapter.cs")); + "Runtime"); + Assert.False(File.Exists(Path.Combine( + appRuntimeRoot, + "CurrentGameRuntimeViewAdapter.cs"))); + Assert.False(File.Exists(Path.Combine( + appRuntimeRoot, + "CurrentGameRuntimeEventAdapter.cs"))); + + string rootSource = File.ReadAllText(Path.Combine( + root, + "src", + "AcDream.Runtime", + "GameRuntime.cs")); Assert.Contains( - "_entityView.MaterializedCount", - viewSource, + "public RuntimeEntityObjectLifetime EntityObjects", + rootSource, StringComparison.Ordinal); - Assert.DoesNotContain( - "_entities.MaterializedCount", - viewSource, + Assert.Contains( + "public IRuntimeEntityView Entities => EntityObjects.EntityView;", + rootSource, + StringComparison.Ordinal); + Assert.Contains( + "public IRuntimeInventoryView Inventory => EntityObjects.InventoryView;", + rootSource, StringComparison.Ordinal); }