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 <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 19:06:09 +02:00
parent ecb9f79444
commit ce41efb9e5
29 changed files with 613 additions and 778 deletions

View file

@ -12,6 +12,7 @@ using AcDream.Core.Physics;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.Spells; using AcDream.Core.Spells;
using AcDream.Core.Vfx; using AcDream.Core.Vfx;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using DatReaderWriter; using DatReaderWriter;
using Silk.NET.Input; using Silk.NET.Input;
@ -49,7 +50,7 @@ internal sealed record ContentEffectsAudioDependencies(
ResidencyBudgetOptions ResidencyBudgets, ResidencyBudgetOptions ResidencyBudgets,
PhysicsDataCache PhysicsDataCache, PhysicsDataCache PhysicsDataCache,
bool DumpMotionEnabled, bool DumpMotionEnabled,
RuntimeCharacterState Character, GameRuntime Runtime,
AnimationHookRouter HookRouter, AnimationHookRouter HookRouter,
EntityEffectPoseRegistry EffectPoses, EntityEffectPoseRegistry EffectPoses,
DeferredEntityEffectAdvanceSource EntityEffectAdvance, DeferredEntityEffectAdvanceSource EntityEffectAdvance,
@ -57,7 +58,10 @@ internal sealed record ContentEffectsAudioDependencies(
TranslucencyFadeManager TranslucencyFades, TranslucencyFadeManager TranslucencyFades,
bool NoAudio, bool NoAudio,
Action<string> Log, Action<string> Log,
Action<string> Error); Action<string> Error)
{
public RuntimeCharacterState Character => Runtime.CharacterOwner;
}
internal interface IGameWindowContentEffectsAudioPublication internal interface IGameWindowContentEffectsAudioPublication
{ {

View file

@ -7,6 +7,7 @@ using AcDream.App.Runtime;
using AcDream.App.Settings; using AcDream.App.Settings;
using AcDream.App.Update; using AcDream.App.Update;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Lighting; using AcDream.Core.Lighting;
@ -21,6 +22,7 @@ namespace AcDream.App.Composition;
internal sealed record FrameRootDependencies( internal sealed record FrameRootDependencies(
RuntimeOptions Options, RuntimeOptions Options,
GameRuntime Runtime,
GL Gl, GL Gl,
IWindow Window, IWindow Window,
IInputContext Input, IInputContext Input,
@ -33,7 +35,6 @@ internal sealed record FrameRootDependencies(
LocalPlayerModeState PlayerMode, LocalPlayerModeState PlayerMode,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
ChaseCameraInputState ChaseCameraInput, ChaseCameraInputState ChaseCameraInput,
RuntimeLocalPlayerMovementState PlayerController,
LiveWorldOriginState WorldOrigin, LiveWorldOriginState WorldOrigin,
ParticleVisibilityController ParticleVisibility, ParticleVisibilityController ParticleVisibility,
EntityEffectPoseRegistry EffectPoses, EntityEffectPoseRegistry EffectPoses,
@ -48,12 +49,18 @@ internal sealed record FrameRootDependencies(
DebugVmRenderFactsPublisher DebugVmRenderFacts, DebugVmRenderFactsPublisher DebugVmRenderFacts,
IInputCaptureSource InputCapture, IInputCaptureSource InputCapture,
DispatcherCameraInputSource CameraInput, DispatcherCameraInputSource CameraInput,
SelectionState Selection,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations, LiveEntityAnimationRuntimeView<LiveEntityAnimationState> Animations,
UpdateFrameClock UpdateClock, UpdateFrameClock UpdateClock,
CombatState Combat,
GameFrameGraphSlot FrameGraphs, GameFrameGraphSlot FrameGraphs,
Action<string> Log); Action<string> Log)
{
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
public SelectionState Selection => Runtime.ActionOwner.Selection;
public CombatState Combat => Runtime.ActionOwner.Combat;
}
internal sealed record FrameRootResult( internal sealed record FrameRootResult(
UpdateFrameOrchestrator Update, UpdateFrameOrchestrator Update,

View file

@ -44,19 +44,15 @@ internal sealed record InteractionRetainedUiDependencies(
RetainedUiInputCaptureSlot RetainedInputCapture, RetainedUiInputCaptureSlot RetainedInputCapture,
InputDispatcher? InputDispatcher, InputDispatcher? InputDispatcher,
RuntimeSettingsController Settings, RuntimeSettingsController Settings,
RuntimeActionState Actions, GameRuntime Runtime,
IRuntimeCombatAttackOperations CombatAttackOperations, IRuntimeCombatAttackOperations CombatAttackOperations,
RuntimeCombatTargetOperationsSlot CombatTargetOperations, RuntimeCombatTargetOperationsSlot CombatTargetOperations,
RuntimeSpellCastOperationsSlot SpellCastOperations, RuntimeSpellCastOperationsSlot SpellCastOperations,
RuntimeInventoryState Inventory,
MagicCatalog MagicCatalog, MagicCatalog MagicCatalog,
RuntimeCharacterState Character,
RuntimeCommunicationState Communication,
StackSplitQuantityState StackSplitQuantity, StackSplitQuantityState StackSplitQuantity,
BufferedUiRegistry? UiRegistry, BufferedUiRegistry? UiRegistry,
LiveCombatModeCommandSlot CombatModeCommands, LiveCombatModeCommandSlot CombatModeCommands,
ILocalPlayerIdentitySource PlayerIdentity, ILocalPlayerIdentitySource PlayerIdentity,
IRuntimeLocalPlayerControllerSource PlayerController,
ILocalPlayerModeSource PlayerMode, ILocalPlayerModeSource PlayerMode,
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource> Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
SelectionCameraFactory, SelectionCameraFactory,
@ -64,7 +60,20 @@ internal sealed record InteractionRetainedUiDependencies(
VitalsVM? ExistingVitals, VitalsVM? ExistingVitals,
Action<string>? Toast, Action<string>? Toast,
Func<double> ClientTime, Func<double> ClientTime,
Action<string> Log); Action<string> 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( internal sealed record RetainedUiComposition(
UiHost Host, UiHost Host,

View file

@ -25,6 +25,7 @@ using AcDream.Core.Rendering;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Core.Vfx; using AcDream.Core.Vfx;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.World; using AcDream.Runtime.World;
using DatReaderWriter; using DatReaderWriter;
@ -45,8 +46,7 @@ internal sealed record LivePresentationDependencies(
PhysicsDataCache PhysicsDataCache, PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState, WorldGameState WorldGameState,
WorldEvents WorldEvents, WorldEvents WorldEvents,
SelectionState Selection, GameRuntime Runtime,
RuntimeEntityObjectLifetime EntityObjects,
LiveEntityRuntimeSlot RuntimeSlot, LiveEntityRuntimeSlot RuntimeSlot,
DeferredLiveEntityMotionRuntimeBindings MotionBindings, DeferredLiveEntityMotionRuntimeBindings MotionBindings,
DeferredEntityEffectAdvanceSource EffectAdvance, DeferredEntityEffectAdvanceSource EffectAdvance,
@ -61,7 +61,6 @@ internal sealed record LivePresentationDependencies(
CellVisibility CellVisibility, CellVisibility CellVisibility,
LiveWorldOriginState WorldOrigin, LiveWorldOriginState WorldOrigin,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
RuntimeLocalPlayerMovementState PlayerController,
PointerPositionState PointerPosition, PointerPositionState PointerPosition,
PlayerApproachCompletionState PlayerApproachCompletions, PlayerApproachCompletionState PlayerApproachCompletions,
GameRenderResourceLifetime RenderResourceLifetime, GameRenderResourceLifetime RenderResourceLifetime,
@ -73,7 +72,18 @@ internal sealed record LivePresentationDependencies(
DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics, DeferredRenderFrameDiagnosticsSource? DevFrameDiagnostics,
DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics, DeferredRenderFrameDiagnosticsSource UiFrameDiagnostics,
Action<string> Log, Action<string> Log,
Action<string>? Toast); Action<string>? 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( internal sealed record LivePresentationResult(
DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle, DeferredLiveEntityRuntimeComponentLifecycle ComponentLifecycle,
@ -336,7 +346,7 @@ internal sealed class LivePresentationCompositionPhase
staticAnimationScheduler.Unregister, staticAnimationScheduler.Unregister,
(entity, info) => staticAnimationScheduler.Rebind(entity, info)); (entity, info) => staticAnimationScheduler.Rebind(entity, info));
var worldTransit = new RuntimeWorldTransitState(d.Log); RuntimeWorldTransitState worldTransit = d.WorldTransit;
var worldAvailability = var worldAvailability =
new WorldGenerationAvailabilityState(worldTransit); new WorldGenerationAvailabilityState(worldTransit);
var worldState = new GpuWorldState( var worldState = new GpuWorldState(

View file

@ -26,6 +26,7 @@ using AcDream.Core.Player;
using AcDream.Core.Social; using AcDream.Core.Social;
using AcDream.Core.Spells; using AcDream.Core.Spells;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
@ -35,6 +36,7 @@ namespace AcDream.App.Composition;
internal sealed record SessionPlayerDependencies( internal sealed record SessionPlayerDependencies(
RuntimeOptions Options, RuntimeOptions Options,
GameRuntime Runtime,
IWindow Window, IWindow Window,
object DatLock, object DatLock,
RuntimeSettingsController Settings, RuntimeSettingsController Settings,
@ -49,8 +51,6 @@ internal sealed record SessionPlayerDependencies(
PhysicsDataCache PhysicsDataCache, PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState, WorldGameState WorldGameState,
WorldEvents WorldEvents, WorldEvents WorldEvents,
RuntimeActionState Actions,
RuntimeEntityObjectLifetime EntityObjects,
EntityClassificationCache ClassificationCache, EntityClassificationCache ClassificationCache,
LiveEntityRuntimeSlot RuntimeSlot, LiveEntityRuntimeSlot RuntimeSlot,
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities, LiveEntityAnimationRuntimeView<LiveEntityAnimationState> AnimatedEntities,
@ -60,7 +60,6 @@ internal sealed record SessionPlayerDependencies(
RetailInboundEventDispatcher InboundEntityEvents, RetailInboundEventDispatcher InboundEntityEvents,
DeferredLiveEntityMotionRuntimeBindings MotionBindings, DeferredLiveEntityMotionRuntimeBindings MotionBindings,
LocalPlayerIdentityState PlayerIdentity, LocalPlayerIdentityState PlayerIdentity,
RuntimeLocalPlayerMovementState PlayerController,
LocalPlayerPhysicsHostSlot PlayerHost, LocalPlayerPhysicsHostSlot PlayerHost,
LocalPlayerModeState PlayerMode, LocalPlayerModeState PlayerMode,
ChaseCameraInputState ChaseCameraInput, ChaseCameraInputState ChaseCameraInput,
@ -71,7 +70,6 @@ internal sealed record SessionPlayerDependencies(
LocalPlayerShadowState PlayerShadow, LocalPlayerShadowState PlayerShadow,
ViewportAspectState ViewportAspect, ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions, PlayerApproachCompletionState PlayerApproachCompletions,
RuntimeInventoryState Inventory,
PointerPositionState PointerPosition, PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput, DispatcherMovementInputSource MovementInput,
IInputCaptureSource InputCapture, IInputCaptureSource InputCapture,
@ -82,10 +80,24 @@ internal sealed record SessionPlayerDependencies(
MovementTruthDiagnosticController MovementDiagnostics, MovementTruthDiagnosticController MovementDiagnostics,
CombatAttackOperationsSlot CombatAttackOperations, CombatAttackOperationsSlot CombatAttackOperations,
CombatFeedbackSlot CombatFeedback, CombatFeedbackSlot CombatFeedback,
RuntimeCommunicationState Communication,
RuntimeCharacterState Character,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback, TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log); Action<string> 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( internal sealed record SessionPlayerResult(
LandblockStreamer Streamer, LandblockStreamer Streamer,
@ -430,11 +442,7 @@ internal sealed class SessionPlayerCompositionPhase
sealedDungeonCells, sealedDungeonCells,
d.Log); d.Log);
var liveSessionLease = scope.Acquire( LiveSessionController liveSession = d.Runtime.Session;
"live-session controller",
static () => new LiveSessionController(),
static value => value.Dispose());
LiveSessionController liveSession = liveSessionLease.Resource;
var liveSessionCommands = new LiveSessionCommandSurface(); var liveSessionCommands = new LiveSessionCommandSurface();
var liveSessionSource = new LiveSessionAppSource( var liveSessionSource = new LiveSessionAppSource(
liveSession, liveSession,
@ -869,19 +877,9 @@ internal sealed class SessionPlayerCompositionPhase
"live combat-mode commands", "live combat-mode commands",
d.CombatModeCommands.BindOwned(combatCommand)); d.CombatModeCommands.BindOwned(combatCommand));
var gameRuntime = new CurrentGameRuntimeAdapter( var gameRuntime = new CurrentGameRuntimeAdapter(
liveSession, d.Runtime,
sessionHost, sessionHost,
liveSessionCommands, liveSessionCommands,
d.PlayerIdentity,
d.EntityObjects,
d.Inventory,
d.Character,
d.Communication,
d.Actions,
d.PlayerController,
d.WorldEnvironment.Runtime,
live.WorldTransit,
d.UpdateClock,
live.SelectionInteractions); live.SelectionInteractions);
bindings.Adopt("current game runtime adapter", gameRuntime); bindings.Adopt("current game runtime adapter", gameRuntime);
bindings.Adopt( bindings.Adopt(
@ -987,7 +985,6 @@ internal sealed class SessionPlayerCompositionPhase
_publication.PublishSessionPlayer(result); _publication.PublishSessionPlayer(result);
streamerLease.Transfer(); streamerLease.Transfer();
liveSessionLease.Transfer();
teleportLease.Transfer(); teleportLease.Transfer();
gameplayActionsLease?.Transfer(); gameplayActionsLease?.Transfer();
componentLifecycleLease.Transfer(); componentLifecycleLease.Transfer();

View file

@ -6,6 +6,7 @@ using AcDream.App.Settings;
using AcDream.Core.Chat; using AcDream.Core.Chat;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Player; using AcDream.Core.Player;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Input; using AcDream.UI.Abstractions.Input;
using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Chat;
@ -35,16 +36,22 @@ internal sealed record SettingsDevToolsDependencies(
RuntimeSettingsController Settings, RuntimeSettingsController Settings,
IRuntimeSettingsStartupTarget StartupTarget, IRuntimeSettingsStartupTarget StartupTarget,
HostQuiescenceGate HostQuiescence, HostQuiescenceGate HostQuiescence,
RuntimeCommunicationState Communication, GameRuntime Runtime,
CombatState Combat,
LocalPlayerState LocalPlayer,
SettingsDevToolsOptionalDependencies? DevTools, SettingsDevToolsOptionalDependencies? DevTools,
RuntimeDiagnosticCommandSlot DiagnosticCommands, RuntimeDiagnosticCommandSlot DiagnosticCommands,
CombatFeedbackSlot CombatFeedback, CombatFeedbackSlot CombatFeedback,
KeyBindings KeyBindings, KeyBindings KeyBindings,
FrameProfiler FrameProfiler, FrameProfiler FrameProfiler,
FramebufferResizeController FramebufferResize, FramebufferResizeController FramebufferResize,
Action<string> Log); Action<string> Log)
{
public RuntimeCommunicationState Communication =>
Runtime.CommunicationOwner;
public CombatState Combat => Runtime.ActionOwner.Combat;
public LocalPlayerState LocalPlayer => Runtime.CharacterOwner.LocalPlayer;
}
internal interface IGameWindowSettingsDevToolsPublication internal interface IGameWindowSettingsDevToolsPublication
{ {

View file

@ -1,5 +1,6 @@
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Input; namespace AcDream.App.Input;
@ -13,7 +14,23 @@ internal interface ILocalPlayerIdentitySource
/// </summary> /// </summary>
internal sealed class LocalPlayerIdentityState : 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 internal interface ILocalPlayerPhysicsHostSource

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.Settings; using AcDream.App.Settings;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Content; using AcDream.Content;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
@ -163,7 +164,7 @@ public sealed class GameWindow :
private readonly AcDream.App.Input.MovementTruthDiagnosticController private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics; _movementTruthDiagnostics;
private readonly LocalPlayerOutboundController _localPlayerOutbound; 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; private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
// Step 7 projectile presentation. The controller owns no identity map; // Step 7 projectile presentation. The controller owns no identity map;
// each runtime component is stored on the canonical LiveEntityRecord. // 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 // J4/J5.1: Runtime owns communication, selection, combat, and target-mode
// state. App, UI, plugins, and live-session routing borrow exact children. // state. App, UI, plugins, and live-session routing borrow exact children.
private readonly RuntimeCommunicationState _runtimeCommunication = new(); private readonly GameRuntime _runtime;
private readonly RuntimeActionState _runtimeActions; private readonly IDisposable _runtimeHostLease;
private RuntimeCommunicationState _runtimeCommunication =>
_runtime.CommunicationOwner;
private RuntimeActionState _runtimeActions => _runtime.ActionOwner;
public AcDream.Core.Selection.SelectionState Selection => public AcDream.Core.Selection.SelectionState Selection =>
_runtimeActions.Selection; _runtimeActions.Selection;
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat; public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
@ -338,9 +342,12 @@ public sealed class GameWindow :
_runtimeCommunication.Squelch; _runtimeCommunication.Squelch;
public AcDream.Core.Combat.CombatState Combat => public AcDream.Core.Combat.CombatState Combat =>
_runtimeActions.Combat; _runtimeActions.Combat;
private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new(); private RuntimeEntityObjectLifetime _runtimeEntityObjects =>
private readonly RuntimeInventoryState _runtimeInventory; _runtime.EntityObjects;
private readonly RuntimeCharacterState _runtimeCharacter; private RuntimeInventoryState _runtimeInventory =>
_runtime.InventoryOwner;
private RuntimeCharacterState _runtimeCharacter =>
_runtime.CharacterOwner;
public AcDream.Core.Items.ItemManaState ItemMana => public AcDream.Core.Items.ItemManaState ItemMana =>
_runtimeInventory.ItemMana; _runtimeInventory.ItemMana;
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog // 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.SceneLightingUboBinding? _sceneLightingUbo;
private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer; private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer;
// Phase B.2: player movement mode. // Phase B.2: player movement mode.
private readonly RuntimeLocalPlayerMovementState _playerControllerSlot = new(); private RuntimeLocalPlayerMovementState _playerControllerSlot =>
_runtime.MovementOwner;
private PlayerMovementController? _playerController private PlayerMovementController? _playerController
=> _playerControllerSlot.Controller; => _playerControllerSlot.Controller;
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new(); 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 readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new();
private bool _playerMode private bool _playerMode
=> _localPlayerMode.IsPlayerMode; => _localPlayerMode.IsPlayerMode;
private readonly AcDream.App.Input.LocalPlayerIdentityState _localPlayerIdentity = new(); private readonly AcDream.App.Input.LocalPlayerIdentityState
_localPlayerIdentity;
private uint _playerServerGuid private uint _playerServerGuid
{ {
get => _localPlayerIdentity.ServerGuid; get => _localPlayerIdentity.ServerGuid;
@ -510,7 +519,6 @@ public sealed class GameWindow :
// Runtime owns the canonical session generation and transport lifetime. // Runtime owns the canonical session generation and transport lifetime.
// The window retains only the composition handles needed for startup and // The window retains only the composition handles needed for startup and
// shutdown; graphical feature callbacks borrow state through App adapters. // shutdown; graphical feature callbacks borrow state through App adapters.
private LiveSessionController? _liveSessionController;
private LiveSessionHost? _liveSessionHost; private LiveSessionHost? _liveSessionHost;
private AcDream.Core.Net.WorldSession? LiveSession => private AcDream.Core.Net.WorldSession? LiveSession =>
_liveSessionHost?.CurrentSession; _liveSessionHost?.CurrentSession;
@ -565,22 +573,24 @@ public sealed class GameWindow :
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null) AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
{ {
_options = options ?? throw new System.ArgumentNullException(nameof(options)); _options = options ?? throw new System.ArgumentNullException(nameof(options));
_worldEnvironment = new AcDream.App.World.WorldEnvironmentController( _runtime = new GameRuntime(new GameRuntimeDependencies(
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,
_combatAttackOperations, _combatAttackOperations,
_combatTargetOperations, _combatTargetOperations,
_combatModeOperations, _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 = var alphaScratchBudgets =
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create( AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
_options.ResidencyBudgets.AlphaScratchBytes); _options.ResidencyBudgets.AlphaScratchBytes);
@ -1039,7 +1049,6 @@ public sealed class GameWindow :
|| _streamingOriginRecenter is not null || _streamingOriginRecenter is not null
|| _worldReveal is not null || _worldReveal is not null
|| _spawnClaimHydration is not null || _spawnClaimHydration is not null
|| _liveSessionController is not null
|| _liveEntityHydration is not null || _liveEntityHydration is not null
|| _liveEntityNetworkUpdates is not null || _liveEntityNetworkUpdates is not null
|| _liveEntityLiveness is not null || _liveEntityLiveness is not null
@ -1063,7 +1072,6 @@ public sealed class GameWindow :
_streamingOriginRecenter = result.StreamingOriginRecenter; _streamingOriginRecenter = result.StreamingOriginRecenter;
_worldReveal = result.WorldReveal; _worldReveal = result.WorldReveal;
_spawnClaimHydration = result.SpawnClaimHydration; _spawnClaimHydration = result.SpawnClaimHydration;
_liveSessionController = result.LiveSession;
_liveEntityHydration = result.Hydration; _liveEntityHydration = result.Hydration;
_liveEntityNetworkUpdates = result.NetworkUpdates; _liveEntityNetworkUpdates = result.NetworkUpdates;
_liveEntityLiveness = result.Liveness; _liveEntityLiveness = result.Liveness;
@ -1163,7 +1171,7 @@ public sealed class GameWindow :
_options.ResidencyBudgets, _options.ResidencyBudgets,
_physicsDataCache, _physicsDataCache,
_animationDiagnostics.DumpMotionEnabled, _animationDiagnostics.DumpMotionEnabled,
_runtimeCharacter, _runtime,
_hookRouter, _hookRouter,
_effectPoses, _effectPoses,
_entityEffectAdvance, _entityEffectAdvance,
@ -1221,9 +1229,7 @@ public sealed class GameWindow :
hostInputCamera.CameraController, hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine), contentEffectsAudio.Audio?.Engine),
_hostQuiescence, _hostQuiescence,
_runtimeCommunication, _runtime,
Combat,
LocalPlayer,
optionalDevTools, optionalDevTools,
_runtimeDiagnosticCommands, _runtimeDiagnosticCommands,
_combatFeedback, _combatFeedback,
@ -1270,19 +1276,15 @@ public sealed class GameWindow :
_retainedInputCapture, _retainedInputCapture,
hostInputCamera.InputDispatcher, hostInputCamera.InputDispatcher,
_runtimeSettings, _runtimeSettings,
_runtimeActions, _runtime,
_combatAttackOperations, _combatAttackOperations,
_combatTargetOperations, _combatTargetOperations,
_spellCastOperations, _spellCastOperations,
_runtimeInventory,
contentEffectsAudio.MagicCatalog, contentEffectsAudio.MagicCatalog,
_runtimeCharacter,
_runtimeCommunication,
_stackSplitQuantity, _stackSplitQuantity,
_uiRegistry, _uiRegistry,
_liveCombatModeCommands, _liveCombatModeCommands,
_localPlayerIdentity, _localPlayerIdentity,
_playerControllerSlot,
_localPlayerMode, _localPlayerMode,
viewPlane => new SelectionCameraSource( viewPlane => new SelectionCameraSource(
hostInputCamera.CameraController, hostInputCamera.CameraController,
@ -1323,8 +1325,7 @@ public sealed class GameWindow :
_physicsDataCache, _physicsDataCache,
_worldGameState, _worldGameState,
_worldEvents, _worldEvents,
Selection, _runtime,
_runtimeEntityObjects,
_liveEntityRuntimeSlot, _liveEntityRuntimeSlot,
_liveEntityMotionBindings, _liveEntityMotionBindings,
_entityEffectAdvance, _entityEffectAdvance,
@ -1339,7 +1340,6 @@ public sealed class GameWindow :
_cellVisibility, _cellVisibility,
_liveWorldOrigin, _liveWorldOrigin,
_localPlayerIdentity, _localPlayerIdentity,
_playerControllerSlot,
_pointerPosition, _pointerPosition,
_playerApproachCompletions, _playerApproachCompletions,
_renderResourceLifetime, _renderResourceLifetime,
@ -1369,6 +1369,7 @@ public sealed class GameWindow :
new SessionPlayerCompositionPhase( new SessionPlayerCompositionPhase(
new SessionPlayerDependencies( new SessionPlayerDependencies(
_options, _options,
_runtime,
_window!, _window!,
_datLock, _datLock,
_runtimeSettings, _runtimeSettings,
@ -1383,8 +1384,6 @@ public sealed class GameWindow :
_physicsDataCache, _physicsDataCache,
_worldGameState, _worldGameState,
_worldEvents, _worldEvents,
_runtimeActions,
_runtimeEntityObjects,
_classificationCache, _classificationCache,
_liveEntityRuntimeSlot, _liveEntityRuntimeSlot,
_animatedEntities, _animatedEntities,
@ -1394,7 +1393,6 @@ public sealed class GameWindow :
_inboundEntityEvents, _inboundEntityEvents,
_liveEntityMotionBindings, _liveEntityMotionBindings,
_localPlayerIdentity, _localPlayerIdentity,
_playerControllerSlot,
_playerHostSlot, _playerHostSlot,
_localPlayerMode, _localPlayerMode,
_chaseCameraInput, _chaseCameraInput,
@ -1405,7 +1403,6 @@ public sealed class GameWindow :
_localPlayerShadow, _localPlayerShadow,
_viewportAspect, _viewportAspect,
_playerApproachCompletions, _playerApproachCompletions,
_runtimeInventory,
_pointerPosition, _pointerPosition,
_movementInput, _movementInput,
_inputCapture, _inputCapture,
@ -1416,8 +1413,6 @@ public sealed class GameWindow :
_movementTruthDiagnostics, _movementTruthDiagnostics,
_combatAttackOperations, _combatAttackOperations,
_combatFeedback, _combatFeedback,
_runtimeCommunication,
_runtimeCharacter,
_portalTunnelFallback, _portalTunnelFallback,
Console.WriteLine), Console.WriteLine),
this).Compose( this).Compose(
@ -1437,6 +1432,7 @@ public sealed class GameWindow :
sessionPlayer) => new FrameRootCompositionPhase( sessionPlayer) => new FrameRootCompositionPhase(
new FrameRootDependencies( new FrameRootDependencies(
_options, _options,
_runtime,
platformResult.Graphics, platformResult.Graphics,
_window!, _window!,
platformResult.Input, platformResult.Input,
@ -1449,7 +1445,6 @@ public sealed class GameWindow :
_localPlayerMode, _localPlayerMode,
_localPlayerIdentity, _localPlayerIdentity,
_chaseCameraInput, _chaseCameraInput,
_playerControllerSlot,
_liveWorldOrigin, _liveWorldOrigin,
_particleVisibility, _particleVisibility,
_effectPoses, _effectPoses,
@ -1464,10 +1459,8 @@ public sealed class GameWindow :
_debugVmRenderFacts, _debugVmRenderFacts,
_inputCapture, _inputCapture,
_cameraInput, _cameraInput,
Selection,
_animatedEntities, _animatedEntities,
_updateFrameClock, _updateFrameClock,
Combat,
_frameGraphs, _frameGraphs,
Console.WriteLine), Console.WriteLine),
this).Compose( this).Compose(
@ -1571,7 +1564,7 @@ public sealed class GameWindow :
_retailUiLease, _retailUiLease,
_uiHost, _uiHost,
_devToolsComposition, _devToolsComposition,
_liveSessionController, _runtime,
_runtimeSettings, _runtimeSettings,
_movementInput, _movementInput,
_cameraInput, _cameraInput,
@ -1590,12 +1583,8 @@ public sealed class GameWindow :
_streamer, _streamer,
_equippedChildRenderer, _equippedChildRenderer,
_liveEntities, _liveEntities,
_runtimeEntityObjects, _runtime,
_runtimeInventory, _runtimeHostLease,
_runtimeCharacter,
_runtimeCommunication,
_runtimeActions,
_playerControllerSlot,
_renderSceneShadow, _renderSceneShadow,
_livePresentationBindings, _livePresentationBindings,
_entityEffectAdvance, _entityEffectAdvance,

View file

@ -17,6 +17,7 @@ using AcDream.App.World;
using AcDream.Content; using AcDream.Content;
using AcDream.Core.Audio; using AcDream.Core.Audio;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities; using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
@ -62,7 +63,7 @@ internal sealed record IngressShutdownRoots(
// Keeps failed physical UI bindings alive through native-window release. // Keeps failed physical UI bindings alive through native-window release.
UiHost? RetainedUiHost, UiHost? RetainedUiHost,
DevToolsCompositionOwner? DevTools, DevToolsCompositionOwner? DevTools,
LiveSessionController? LiveSession, GameRuntime Runtime,
RuntimeSettingsController Settings, RuntimeSettingsController Settings,
DispatcherMovementInputSource MovementInput, DispatcherMovementInputSource MovementInput,
DispatcherCameraInputSource CameraInput, DispatcherCameraInputSource CameraInput,
@ -83,12 +84,8 @@ internal sealed record LiveShutdownRoots(
LandblockStreamer? Streamer, LandblockStreamer? Streamer,
EquippedChildRenderController? EquippedChildren, EquippedChildRenderController? EquippedChildren,
LiveEntityRuntime? LiveEntities, LiveEntityRuntime? LiveEntities,
RuntimeEntityObjectLifetime EntityObjects, GameRuntime Runtime,
RuntimeInventoryState Inventory, IDisposable RuntimeHostLease,
RuntimeCharacterState Character,
RuntimeCommunicationState Communication,
RuntimeActionState Actions,
RuntimeLocalPlayerMovementState Movement,
RenderSceneShadowRuntime? RenderSceneShadow, RenderSceneShadowRuntime? RenderSceneShadow,
LivePresentationRuntimeBindings? PresentationBindings, LivePresentationRuntimeBindings? PresentationBindings,
DeferredEntityEffectAdvanceSource EffectAdvance, DeferredEntityEffectAdvanceSource EffectAdvance,
@ -352,7 +349,7 @@ internal static class GameWindowShutdownManifest
Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()), Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()),
Hard("retained UI input", ingress.RetailUi.QuiesceInput), Hard("retained UI input", ingress.RetailUi.QuiesceInput),
Hard("developer tools input", () => ingress.DevTools?.DeactivateInput()), 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", new ResourceShutdownStage("physical ingress cleanup",
[ [
@ -391,24 +388,6 @@ internal static class GameWindowShutdownManifest
"shadow render scene", "shadow render scene",
() => live.RenderSceneShadow?.Dispose()), () => 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", new ResourceShutdownStage("effect dispatch edges",
[ [
Hard("live-presentation bindings", () => live.PresentationBindings?.Dispose()), Hard("live-presentation bindings", () => live.PresentationBindings?.Dispose()),
@ -456,9 +435,10 @@ internal static class GameWindowShutdownManifest
Hard("sky", () => render.Sky?.Dispose()), Hard("sky", () => render.Sky?.Dispose()),
Hard("particles", () => render.Particles?.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", new ResourceShutdownStage("shared texture owners",
[ [
@ -515,15 +495,14 @@ internal static class GameWindowShutdownManifest
private static ResourceShutdownOperation Soft(string name, Action action) => private static ResourceShutdownOperation Soft(string name, Action action) =>
new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue); new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue);
private static void DisposeLiveSession(LiveSessionController? controller) private static void DisposeGameRuntime(GameRuntime runtime)
{ {
if (controller is null) runtime.Dispose();
return; GameRuntimeOwnershipSnapshot ownership = runtime.CaptureOwnership();
controller.Dispose(); if (!ownership.IsConverged)
if (!controller.IsDisposalComplete)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"Live-session disposal was deferred by a reentrant lifecycle callback."); "The canonical game runtime did not converge after disposal.");
} }
} }

View file

@ -1,10 +1,6 @@
using AcDream.App.Input;
using AcDream.App.Interaction; using AcDream.App.Interaction;
using AcDream.App.Net; using AcDream.App.Net;
using AcDream.App.World;
using AcDream.Runtime; using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session; using AcDream.Runtime.Session;
using AcDream.Runtime.World; using AcDream.Runtime.World;
using AcDream.UI.Abstractions; using AcDream.UI.Abstractions;
@ -12,9 +8,9 @@ using AcDream.UI.Abstractions;
namespace AcDream.App.Runtime; namespace AcDream.App.Runtime;
/// <summary> /// <summary>
/// App composition root for Slice J's borrowed runtime boundary. Focused /// Synchronous graphical command adapter over one canonical
/// adapters project the existing canonical owners; this root owns only those /// <see cref="GameRuntime"/>. It owns only its host lease and observer
/// adapters and never owns or copies gameplay state. /// subscriptions; every view and mutable owner is borrowed directly.
/// </summary> /// </summary>
internal sealed class CurrentGameRuntimeAdapter internal sealed class CurrentGameRuntimeAdapter
: IGameRuntimeView, : IGameRuntimeView,
@ -22,72 +18,80 @@ internal sealed class CurrentGameRuntimeAdapter
IRuntimeEventSource, IRuntimeEventSource,
IDisposable IDisposable
{ {
private readonly CurrentGameRuntimeViewAdapter _view; private readonly GameRuntime _runtime;
private readonly CurrentGameRuntimeEventAdapter _events;
private readonly CurrentGameRuntimeCommandAdapter _commands; private readonly CurrentGameRuntimeCommandAdapter _commands;
private readonly IDisposable _hostLease;
private readonly object _subscriptionGate = new();
private readonly HashSet<AdapterSubscription> _subscriptions = [];
private bool _disposed; private bool _disposed;
public CurrentGameRuntimeAdapter( public CurrentGameRuntimeAdapter(
LiveSessionController session, GameRuntime runtime,
LiveSessionHost sessionHost, LiveSessionHost sessionHost,
ICommandBus commands, ICommandBus commands,
LocalPlayerIdentityState playerIdentity,
RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory,
RuntimeCharacterState character,
RuntimeCommunicationState communication,
RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement,
RuntimeWorldEnvironmentState environment,
RuntimeWorldTransitState worldTransit,
IGameRuntimeClock clock,
SelectionInteractionController selection) SelectionInteractionController selection)
{ {
_view = new CurrentGameRuntimeViewAdapter( _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
session, ArgumentNullException.ThrowIfNull(sessionHost);
playerIdentity, ArgumentNullException.ThrowIfNull(commands);
entityObjects, ArgumentNullException.ThrowIfNull(selection);
inventory,
character, _hostLease = runtime.AcquireHostLease(
communication, "graphical game-runtime command adapter");
actions, try
movement, {
environment, _commands = new CurrentGameRuntimeCommandAdapter(
worldTransit, runtime.Session,
clock); sessionHost,
entityObjects.BindEventContext( commands,
() => _view.Generation, this,
() => clock.FrameNumber); runtime.InventoryOwner,
_events = new CurrentGameRuntimeEventAdapter( runtime.CharacterOwner,
_view, runtime.ActionOwner,
entityObjects, runtime.MovementOwner,
communication); selection,
_commands = new CurrentGameRuntimeCommandAdapter( runtime.EventSink);
session, }
sessionHost, catch
commands, {
_view, _hostLease.Dispose();
inventory, throw;
character, }
actions,
movement,
selection,
_events);
} }
public RuntimeGenerationToken Generation => _view.Generation; private bool IsActive =>
public RuntimeLifecycleSnapshot Lifecycle => _view.Lifecycle; !_disposed
public IGameRuntimeClock Clock => _view.Clock; && !_runtime.Session.IsDisposalComplete;
public IRuntimeEntityView Entities => _view.Entities;
public IRuntimeInventoryView Inventory => _view.Inventory; public RuntimeGenerationToken Generation => _runtime.Generation;
public IRuntimeInventoryStateView InventoryState => _view.InventoryState;
public IRuntimeCharacterView Character => _view.Character; public RuntimeLifecycleSnapshot Lifecycle
public IRuntimeSocialView Social => _view.Social; {
public IRuntimeChatView Chat => _view.Chat; get
public IRuntimeActionView Actions => _view.Actions; {
public IRuntimeMovementView Movement => _view.Movement; RuntimeLifecycleSnapshot current = _runtime.Lifecycle;
public IRuntimeWorldEnvironmentView Environment => _view.Environment; return IsActive
public IRuntimePortalView Portal => _view.Portal; ? 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 IRuntimeSessionCommands Session => _commands;
public IRuntimeSelectionCommands Selection => _commands; public IRuntimeSelectionCommands Selection => _commands;
@ -109,18 +113,64 @@ internal sealed class CurrentGameRuntimeAdapter
public IRuntimeSocialCommands SocialCommands => _commands; public IRuntimeSocialCommands SocialCommands => _commands;
IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands; IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands;
public RuntimeStateCheckpoint CaptureCheckpoint() => public RuntimeStateCheckpoint CaptureCheckpoint()
_view.CaptureCheckpoint(); {
RuntimeStateCheckpoint checkpoint = _runtime.CaptureCheckpoint();
return checkpoint with { Lifecycle = Lifecycle.State };
}
public IDisposable Subscribe(IRuntimeEventObserver observer) => public IDisposable Subscribe(IRuntimeEventObserver observer)
_events.Subscribe(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() public void Dispose()
{ {
if (_disposed) lock (_subscriptionGate)
return; {
_disposed = true; if (_disposed)
_view.Deactivate(); return;
_events.Dispose(); _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);
}
} }
} }

View file

@ -30,25 +30,25 @@ internal sealed class CurrentGameRuntimeCommandAdapter
private readonly LiveSessionController _session; private readonly LiveSessionController _session;
private readonly LiveSessionHost _sessionHost; private readonly LiveSessionHost _sessionHost;
private readonly ICommandBus _commands; private readonly ICommandBus _commands;
private readonly CurrentGameRuntimeViewAdapter _view; private readonly IGameRuntimeView _view;
private readonly RuntimeInventoryState _inventory; private readonly RuntimeInventoryState _inventory;
private readonly RuntimeCharacterState _character; private readonly RuntimeCharacterState _character;
private readonly RuntimeActionState _actions; private readonly RuntimeActionState _actions;
private readonly RuntimeLocalPlayerMovementState _movement; private readonly RuntimeLocalPlayerMovementState _movement;
private readonly SelectionInteractionController _selection; private readonly SelectionInteractionController _selection;
private readonly ICurrentGameRuntimeEventSink _events; private readonly IGameRuntimeEventSink _events;
public CurrentGameRuntimeCommandAdapter( public CurrentGameRuntimeCommandAdapter(
LiveSessionController session, LiveSessionController session,
LiveSessionHost sessionHost, LiveSessionHost sessionHost,
ICommandBus commands, ICommandBus commands,
CurrentGameRuntimeViewAdapter view, IGameRuntimeView view,
RuntimeInventoryState inventory, RuntimeInventoryState inventory,
RuntimeCharacterState character, RuntimeCharacterState character,
RuntimeActionState actions, RuntimeActionState actions,
RuntimeLocalPlayerMovementState movement, RuntimeLocalPlayerMovementState movement,
SelectionInteractionController selection, SelectionInteractionController selection,
ICurrentGameRuntimeEventSink events) IGameRuntimeEventSink events)
{ {
_session = session ?? throw new ArgumentNullException(nameof(session)); _session = session ?? throw new ArgumentNullException(nameof(session));
_sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost)); _sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost));
@ -80,7 +80,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
ToCommandStatus(result.Status), ToCommandStatus(result.Status),
result.CharacterId, result.CharacterId,
result.CharacterName); result.CharacterName);
_events.EmitLifecycle(previous); _events.EmitLifecycle(previous, _view.Lifecycle.State);
return result; return result;
} }
@ -100,7 +100,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
ToCommandStatus(result.Status), ToCommandStatus(result.Status),
result.CharacterId, result.CharacterId,
result.CharacterName); result.CharacterName);
_events.EmitLifecycle(previous); _events.EmitLifecycle(previous, _view.Lifecycle.State);
return result; return result;
} }
@ -125,7 +125,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
operation: 2, operation: 2,
acknowledgement.Status, acknowledgement.Status,
text: acknowledgement.Error?.GetType().Name ?? string.Empty); text: acknowledgement.Error?.GetType().Name ?? string.Empty);
_events.EmitLifecycle(previous); _events.EmitLifecycle(previous, _view.Lifecycle.State);
return acknowledgement; return acknowledgement;
} }
@ -674,7 +674,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
RuntimeGenerationToken expectedGeneration, RuntimeGenerationToken expectedGeneration,
bool requireWorld) bool requireWorld)
{ {
if (!_view.IsActive) if (_view.Lifecycle.State == RuntimeLifecycleState.Disposed)
return RuntimeCommandStatus.Inactive; return RuntimeCommandStatus.Inactive;
if (expectedGeneration != _view.Generation) if (expectedGeneration != _view.Generation)
return RuntimeCommandStatus.StaleGeneration; return RuntimeCommandStatus.StaleGeneration;

View file

@ -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);
}
/// <summary>
/// Lazily projects existing owner notifications into one ordered diagnostic
/// stream. With no subscribers it attaches no callbacks and adds no per-event
/// production work.
/// </summary>
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);
}
}

View file

@ -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;
/// <summary>
/// Read-only borrowed projections over the current App owners. Enumeration is
/// synchronous and allocation-free; no mutable collection is copied or owned.
/// </summary>
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;
}

View file

@ -31,7 +31,17 @@ internal sealed class UpdateFrameClock
: IPhysicsScriptTimeSource, : IPhysicsScriptTimeSource,
IGameRuntimeClock 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 ulong FrameNumber => _runtime.FrameNumber;
public double SimulationTimeSeconds => _runtime.SimulationTimeSeconds; public double SimulationTimeSeconds => _runtime.SimulationTimeSeconds;

View file

@ -109,6 +109,8 @@ public sealed class ContentEffectsAudioCompositionTests
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose( Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
fixture.Platform, fixture.Platform,
fixture.Host)); fixture.Host));
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.NotNull(fixture.Publication.Audio); Assert.NotNull(fixture.Publication.Audio);
Assert.False(fixture.Publication.Audio!.Engine.IsDisposalComplete); Assert.False(fixture.Publication.Audio!.Engine.IsDisposalComplete);
@ -127,6 +129,8 @@ public sealed class ContentEffectsAudioCompositionTests
Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose( Assert.Throws<InvalidOperationException>(() => fixture.Phase().Compose(
fixture.Platform, fixture.Platform,
fixture.Host)); fixture.Host));
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal( Assert.Equal(
Enum.GetValues<ContentEffectsAudioCompositionPoint>() Enum.GetValues<ContentEffectsAudioCompositionPoint>()
@ -258,7 +262,7 @@ public sealed class ContentEffectsAudioCompositionTests
ResidencyBudgetOptions.Default, ResidencyBudgetOptions.Default,
new PhysicsDataCache(), new PhysicsDataCache(),
false, false,
new RuntimeCharacterState(), GameRuntimeTestFactory.Create(),
Router, Router,
Poses, Poses,
new DeferredEntityEffectAdvanceSource(), new DeferredEntityEffectAdvanceSource(),
@ -293,7 +297,7 @@ public sealed class ContentEffectsAudioCompositionTests
public void Dispose() public void Dispose()
{ {
Publication.Dispose(); Publication.Dispose();
Dependencies.Character.Dispose(); Dependencies.Runtime.Dispose();
} }
} }

View file

@ -10,6 +10,7 @@ using AcDream.App.World;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Core.Spells; using AcDream.Core.Spells;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay; using AcDream.Runtime.Gameplay;
namespace AcDream.App.Tests.Composition; namespace AcDream.App.Tests.Composition;
@ -33,7 +34,7 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact] [Fact]
public void EnabledUiPublishesOneExactResultAfterFrozenConstructionOrder() public void EnabledUiPublishesOneExactResultAfterFrozenConstructionOrder()
{ {
var fixture = new Fixture(retailUi: true); using var fixture = new Fixture(retailUi: true);
InteractionRetainedUiResult result = fixture.Compose(); InteractionRetainedUiResult result = fixture.Compose();
@ -56,7 +57,7 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact] [Fact]
public void DisabledUiAcquiresNoRetainedFrontendResource() public void DisabledUiAcquiresNoRetainedFrontendResource()
{ {
var fixture = new Fixture(retailUi: false); using var fixture = new Fixture(retailUi: false);
InteractionRetainedUiResult result = fixture.Compose(); InteractionRetainedUiResult result = fixture.Compose();
@ -81,11 +82,13 @@ public sealed class InteractionRetainedUiCompositionTests
int pointValue) int pointValue)
{ {
var point = (InteractionRetainedUiCompositionPoint)pointValue; var point = (InteractionRetainedUiCompositionPoint)pointValue;
var fixture = new Fixture(retailUi: true, failurePoint: point); using var fixture = new Fixture(retailUi: true, failurePoint: point);
Assert.Throws<InvalidOperationException>(fixture.Compose); Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.Equal(point, fixture.Points[^1]); Assert.Equal(point, fixture.Points[^1]);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
if (point == InteractionRetainedUiCompositionPoint.ResultPublished) if (point == InteractionRetainedUiCompositionPoint.ResultPublished)
{ {
Assert.Empty(fixture.Factory.Releases); Assert.Empty(fixture.Factory.Releases);
@ -129,9 +132,11 @@ public sealed class InteractionRetainedUiCompositionTests
[Fact] [Fact]
public void PublicationFailureRollsBackCompleteUnpublishedPrefix() public void PublicationFailureRollsBackCompleteUnpublishedPrefix()
{ {
var fixture = new Fixture(retailUi: true, publicationFailure: true); using var fixture = new Fixture(retailUi: true, publicationFailure: true);
Assert.Throws<InvalidOperationException>(fixture.Compose); Assert.Throws<InvalidOperationException>(fixture.Compose);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal( Assert.Equal(
[ [
@ -161,7 +166,7 @@ public sealed class InteractionRetainedUiCompositionTests
Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source); Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source);
} }
private sealed class Fixture private sealed class Fixture : IDisposable
{ {
private readonly InteractionRetainedUiCompositionPoint? _failurePoint; private readonly InteractionRetainedUiCompositionPoint? _failurePoint;
@ -175,9 +180,7 @@ public sealed class InteractionRetainedUiCompositionTests
Publication = new Publication(publicationFailure); Publication = new Publication(publicationFailure);
RuntimeOptions options = RuntimeOptions.Parse("dat", static _ => null) RuntimeOptions options = RuntimeOptions.Parse("dat", static _ => null)
with { RetailUi = retailUi }; with { RetailUi = retailUi };
var actions = new RuntimeActionState( GameRuntime runtime = GameRuntimeTestFactory.Create(
new InventoryTransactionState(new ClientObjectTable()),
new Spellbook(),
new NoopCombatOperations(), new NoopCombatOperations(),
new NoopCombatTargetOperations(), new NoopCombatTargetOperations(),
new NoopCombatModeOperations(), new NoopCombatModeOperations(),
@ -196,19 +199,15 @@ public sealed class InteractionRetainedUiCompositionTests
RetainedInputCapture: null!, RetainedInputCapture: null!,
InputDispatcher: null, InputDispatcher: null,
Settings: null!, Settings: null!,
Actions: actions, Runtime: runtime,
CombatAttackOperations: new NoopCombatOperations(), CombatAttackOperations: new NoopCombatOperations(),
CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(), CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(),
SpellCastOperations: new RuntimeSpellCastOperationsSlot(), SpellCastOperations: new RuntimeSpellCastOperationsSlot(),
Inventory: null!,
MagicCatalog: null!, MagicCatalog: null!,
Character: null!,
Communication: null!,
StackSplitQuantity: null!, StackSplitQuantity: null!,
UiRegistry: null, UiRegistry: null,
CombatModeCommands: null!, CombatModeCommands: null!,
PlayerIdentity: null!, PlayerIdentity: null!,
PlayerController: null!,
PlayerMode: null!, PlayerMode: null!,
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(), SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(), FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
@ -235,6 +234,8 @@ public sealed class InteractionRetainedUiCompositionTests
if (_failurePoint == point) if (_failurePoint == point)
throw new InvalidOperationException($"fault at {point}"); throw new InvalidOperationException($"fault at {point}");
}).Compose(); }).Compose();
public void Dispose() => Dependencies.Runtime.Dispose();
} }
private sealed class FakeFactory : IInteractionRetainedUiCompositionFactory private sealed class FakeFactory : IInteractionRetainedUiCompositionFactory

View file

@ -88,7 +88,10 @@ public sealed class SessionPlayerCompositionTests
window); window);
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window); Assert.DoesNotContain("IsSpawnClaimUnhydratable", window);
Assert.Contains("LandblockStreamer.CreateForRequests(", phase); 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 LocalPlayerTeleportController(", phase);
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase); Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase);
} }
@ -109,7 +112,7 @@ public sealed class SessionPlayerCompositionTests
"streamerLease.Resource.Start();", "streamerLease.Resource.Start();",
"new StreamingController(", "new StreamingController(",
"new WorldRevealCoordinator(", "new WorldRevealCoordinator(",
"new LiveSessionController()", "LiveSessionController liveSession = d.Runtime.Session;",
"new LiveEntityHydrationController(", "new LiveEntityHydrationController(",
"new LiveEntityNetworkUpdateController(", "new LiveEntityNetworkUpdateController(",
"new GameplayInputFrameController(", "new GameplayInputFrameController(",

View file

@ -33,6 +33,8 @@ public sealed class SettingsDevToolsCompositionTests
SettingsDevToolsResult result = fixture.Compose(); SettingsDevToolsResult result = fixture.Compose();
Assert.Null(result.DevTools); Assert.Null(result.DevTools);
Assert.False(
fixture.Dependencies.Runtime.CaptureOwnership().IsDisposeRequested);
Assert.Equal(1, fixture.Startup.DisplayCalls); Assert.Equal(1, fixture.Startup.DisplayCalls);
Assert.Equal(1, fixture.Startup.AudioCalls); Assert.Equal(1, fixture.Startup.AudioCalls);
Assert.Equal(0, fixture.Factory.Calls); Assert.Equal(0, fixture.Factory.Calls);
@ -229,9 +231,7 @@ public sealed class SettingsDevToolsCompositionTests
Settings, Settings,
Startup, Startup,
new HostQuiescenceGate(), new HostQuiescenceGate(),
new RuntimeCommunicationState(), GameRuntimeTestFactory.Create(),
new CombatState(),
new LocalPlayerState(new Spellbook()),
enabled enabled
? new SettingsDevToolsOptionalDependencies( ? new SettingsDevToolsOptionalDependencies(
new Facts(), new Facts(),
@ -273,6 +273,7 @@ public sealed class SettingsDevToolsCompositionTests
public void Dispose() public void Dispose()
{ {
Dependencies.Runtime.Dispose();
Publication.Owner?.Dispose(); Publication.Owner?.Dispose();
_dispatcher.Dispose(); _dispatcher.Dispose();
_profiler.Dispose(); _profiler.Dispose();

View file

@ -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<double>? 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<ClientObject> 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() { }
}
}

View file

@ -13,18 +13,22 @@ public sealed class GameWindowLiveSessionOwnershipTests
BindingFlags.Instance | BindingFlags.NonPublic; BindingFlags.Instance | BindingFlags.NonPublic;
[Fact] [Fact]
public void GameWindowRetainsControllerAndFocusedHostButNoMirroredSession() public void GameWindowRetainsCanonicalRuntimeAndFocusedHostButNoMirroredSession()
{ {
FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance); FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance);
Assert.Contains( Assert.Contains(
fields, fields,
field => field.Name == "_liveSessionController" field => field.Name == "_runtime"
&& field.FieldType == typeof(LiveSessionController)); && field.FieldType == typeof(GameRuntime));
Assert.Contains( Assert.Contains(
fields, fields,
field => field.Name == "_liveSessionHost" field => field.Name == "_liveSessionHost"
&& field.FieldType == typeof(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.Name == "_liveSession");
Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession)); Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(LiveSessionResetPlan)); Assert.DoesNotContain(fields, field => field.FieldType == typeof(LiveSessionResetPlan));
@ -54,6 +58,48 @@ public sealed class GameWindowLiveSessionOwnershipTests
|| field.FieldType == typeof(ulong)); || 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] [Theory]
[InlineData("TryStartLiveSession")] [InlineData("TryStartLiveSession")]
[InlineData("ClearInboundEntityState")] [InlineData("ClearInboundEntityState")]
@ -66,4 +112,32 @@ public sealed class GameWindowLiveSessionOwnershipTests
{ {
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateInstance)); 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.");
}
} }

View file

@ -258,7 +258,7 @@ public sealed class GameWindowSlice8BoundaryTests
"Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)", "Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)",
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.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\"", "new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",
@ -469,12 +469,11 @@ public sealed class GameWindowSlice8BoundaryTests
"new ResourceShutdownStage(\"frame borrowers\"", "new ResourceShutdownStage(\"frame borrowers\"",
"new ResourceShutdownStage(\"session dependents\"", "new ResourceShutdownStage(\"session dependents\"",
"new ResourceShutdownStage(\"live entities\"", "new ResourceShutdownStage(\"live entities\"",
"new ResourceShutdownStage(\"runtime entity/object stream\"",
"new ResourceShutdownStage(\"effect dispatch edges\"", "new ResourceShutdownStage(\"effect dispatch edges\"",
"new ResourceShutdownStage(\"live entity dependents\"", "new ResourceShutdownStage(\"live entity dependents\"",
"new ResourceShutdownStage(\"submitted GPU work\"", "new ResourceShutdownStage(\"submitted GPU work\"",
"new ResourceShutdownStage(\"render frontends\"", "new ResourceShutdownStage(\"render frontends\"",
"new ResourceShutdownStage(\"runtime communication state\"", "new ResourceShutdownStage(\"game runtime root\"",
"new ResourceShutdownStage(\"shared texture owners\"", "new ResourceShutdownStage(\"shared texture owners\"",
"new ResourceShutdownStage(\"mesh adapter\"", "new ResourceShutdownStage(\"mesh adapter\"",
"new ResourceShutdownStage(\"remaining render owners\"", "new ResourceShutdownStage(\"remaining render owners\"",
@ -496,7 +495,7 @@ public sealed class GameWindowSlice8BoundaryTests
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())", "Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())", "Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())",
"Hard(\"camera pointer\", () => ingress.CameraPointer?.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\"", "new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))", "Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))", "Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",

View file

@ -762,43 +762,40 @@ public sealed class CurrentGameRuntimeAdapterTests
private readonly ItemInteractionController _items; private readonly ItemInteractionController _items;
private readonly LiveSessionController _session; private readonly LiveSessionController _session;
private readonly IDisposable _combatModeBinding; private readonly IDisposable _combatModeBinding;
private readonly GameRuntime _gameRuntime;
public Harness() 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(); CombatAttackOperations = new CombatAttackOperationsSlot();
CombatModeOperations = new RuntimeCombatModeOperationsSlot(); CombatModeOperations = new RuntimeCombatModeOperationsSlot();
SpellCastOperations = new RuntimeSpellCastOperationsSlot(); SpellCastOperations = new RuntimeSpellCastOperationsSlot();
Actions = new RuntimeActionState( Options = LiveOptions();
InventoryState.Transactions, Commands = new RecordingCommandRouting();
Character.Spellbook, Transport = new TestTransport();
_gameRuntime = GameRuntimeTestFactory.Create(
CombatAttackOperations, CombatAttackOperations,
new RuntimeCombatTargetOperationsSlot(), new RuntimeCombatTargetOperationsSlot(),
CombatModeOperations, CombatModeOperations,
SpellCastOperations, 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(); CombatMode = new RecordingCombatModeOperations();
_combatModeBinding = _combatModeBinding =
CombatModeOperations.BindOwned(CombatMode); CombatModeOperations.BindOwned(CombatMode);
MovementState = new RuntimeLocalPlayerMovementState();
Environment = new RuntimeWorldEnvironmentState();
MovementInput = new DispatcherMovementInputSource(MovementState); MovementInput = new DispatcherMovementInputSource(MovementState);
GameplayInput = new GameplayInputFrameController( GameplayInput = new GameplayInputFrameController(
dispatcher: null, dispatcher: null,
MovementInput, MovementInput,
mouseLook: null, mouseLook: null,
new NoopCombatInput()); new NoopCombatInput());
Clock = new UpdateFrameClock(); Clock = new UpdateFrameClock(_gameRuntime.Clock);
WorldTransit = new RuntimeWorldTransitState();
WorldReveal = new WorldRevealCoordinator( WorldReveal = new WorldRevealCoordinator(
WorldTransit, WorldTransit,
static (_, _) => true, static (_, _) => true,
@ -826,50 +823,42 @@ public sealed class CurrentGameRuntimeAdapterTests
new SelectionTransport(() => _session?.IsInWorld == true), new SelectionTransport(() => _session?.IsInWorld == true),
new NoopInteractionMovement()); new NoopInteractionMovement());
Options = LiveOptions();
Commands = new RecordingCommandRouting();
Transport = new TestTransport();
_session = new LiveSessionController(
new SessionOperations(Transport));
Host = CreateHost(_session, Commands, Identity); Host = CreateHost(_session, Commands, Identity);
Runtime = new CurrentGameRuntimeAdapter( Runtime = new CurrentGameRuntimeAdapter(
_session, _gameRuntime,
Host, Host,
Commands, Commands,
Identity,
EntityObjects,
InventoryState,
Character,
Communication,
Actions,
MovementState,
Environment,
WorldTransit,
Clock,
selectionController); selectionController);
} }
public RuntimeOptions Options { get; } public RuntimeOptions Options { get; }
public LocalPlayerIdentityState Identity { get; } public LocalPlayerIdentityState Identity { get; }
public RuntimeEntityObjectLifetime EntityObjects { get; } public RuntimeEntityObjectLifetime EntityObjects =>
public RuntimeInventoryState InventoryState { get; } _gameRuntime.EntityObjects;
public RuntimeCharacterState Character { get; } public RuntimeInventoryState InventoryState =>
_gameRuntime.InventoryOwner;
public RuntimeCharacterState Character =>
_gameRuntime.CharacterOwner;
public LiveEntityRuntime Entities { get; } public LiveEntityRuntime Entities { get; }
public ClientObjectTable Objects { get; } public ClientObjectTable Objects => EntityObjects.Objects;
public RuntimeCommunicationState Communication { get; } public RuntimeCommunicationState Communication =>
_gameRuntime.CommunicationOwner;
public ChatLog Chat => Communication.Chat; public ChatLog Chat => Communication.Chat;
public CombatAttackOperationsSlot CombatAttackOperations { get; } public CombatAttackOperationsSlot CombatAttackOperations { get; }
public RuntimeCombatModeOperationsSlot CombatModeOperations { get; } public RuntimeCombatModeOperationsSlot CombatModeOperations { get; }
public RuntimeSpellCastOperationsSlot SpellCastOperations { get; } public RuntimeSpellCastOperationsSlot SpellCastOperations { get; }
public RuntimeActionState Actions { get; } public RuntimeActionState Actions => _gameRuntime.ActionOwner;
public SelectionState Selection => Actions.Selection; public SelectionState Selection => Actions.Selection;
public RuntimeLocalPlayerMovementState MovementState { get; } public RuntimeLocalPlayerMovementState MovementState =>
public RuntimeWorldEnvironmentState Environment { get; } _gameRuntime.MovementOwner;
public RuntimeWorldEnvironmentState Environment =>
_gameRuntime.EnvironmentOwner;
public DispatcherMovementInputSource MovementInput { get; } public DispatcherMovementInputSource MovementInput { get; }
public GameplayInputFrameController GameplayInput { get; } public GameplayInputFrameController GameplayInput { get; }
public RecordingCombatModeOperations CombatMode { get; } public RecordingCombatModeOperations CombatMode { get; }
public UpdateFrameClock Clock { get; } public UpdateFrameClock Clock { get; }
public RuntimeWorldTransitState WorldTransit { get; } public RuntimeWorldTransitState WorldTransit =>
_gameRuntime.TransitOwner;
public WorldRevealCoordinator WorldReveal { get; } public WorldRevealCoordinator WorldReveal { get; }
public RecordingCommandRouting Commands { get; } public RecordingCommandRouting Commands { get; }
public TestTransport Transport { get; } public TestTransport Transport { get; }
@ -879,15 +868,11 @@ public sealed class CurrentGameRuntimeAdapterTests
public void Dispose() public void Dispose()
{ {
Runtime.Dispose(); Runtime.Dispose();
Character.Dispose();
Communication.Dispose();
_session.Dispose();
_items.Dispose(); _items.Dispose();
_combatModeBinding.Dispose(); _combatModeBinding.Dispose();
Actions.Dispose();
InventoryState.Dispose();
Entities.Clear(); Entities.Clear();
MovementState.Dispose(); WorldReveal.ResetSession();
_gameRuntime.Dispose();
} }
} }

View file

@ -15,11 +15,11 @@ public sealed class RuntimeActionOwnershipTests
string program = ReadAppSource(root, "Program.cs"); string program = ReadAppSource(root, "Program.cs");
Assert.Contains( Assert.Contains(
"private readonly RuntimeActionState _runtimeActions;", "private readonly GameRuntime _runtime;",
gameWindow, gameWindow,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"_runtimeActions = new RuntimeActionState(", "_runtime = new GameRuntime(new GameRuntimeDependencies(",
gameWindow, gameWindow,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
@ -73,14 +73,9 @@ public sealed class RuntimeActionOwnershipTests
Assert.Empty(Regex.Matches( Assert.Empty(Regex.Matches(
production, production,
@"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?RuntimeSpellCastState\s*\(")); @"\bnew\s+(?:AcDream\.Runtime\.Gameplay\.)?RuntimeSpellCastState\s*\("));
Assert.Equal( Assert.Empty(Regex.Matches(
1, production,
Regex.Matches( @"\bnew\s+RuntimeActionState\s*\("));
production,
@"\bnew\s+RuntimeActionState\s*\(").Count
+ Regex.Matches(
production,
@"RuntimeActionState\s+\w+\s*=\s*new\s*\(\s*\)").Count);
} }
[Fact] [Fact]
@ -138,14 +133,13 @@ public sealed class RuntimeActionOwnershipTests
itemInteraction, itemInteraction,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"\"runtime action state\"", "new ResourceShutdownStage(\"game runtime root\"",
shutdown, shutdown,
StringComparison.Ordinal); StringComparison.Ordinal);
AssertAppearsInOrder( Assert.DoesNotContain(
"RuntimeActionState Actions",
shutdown, shutdown,
"\"runtime action state\"", StringComparison.Ordinal);
"\"runtime inventory state\"",
"\"runtime entity/object lifetime\"");
Assert.False(File.Exists(Path.Combine( Assert.False(File.Exists(Path.Combine(
root, root,
"src", "src",

View file

@ -8,7 +8,15 @@ public sealed class RuntimeCharacterOwnershipTests
string source = ReadSource("Rendering", "GameWindow.cs"); string source = ReadSource("Rendering", "GameWindow.cs");
Assert.Contains( 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, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
@ -76,7 +84,11 @@ public sealed class RuntimeCharacterOwnershipTests
inventory, inventory,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"\"runtime character state\"", "new ResourceShutdownStage(\"game runtime root\"",
shutdown,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimeCharacterState Character",
shutdown, shutdown,
StringComparison.Ordinal); StringComparison.Ordinal);
} }

View file

@ -8,7 +8,11 @@ public sealed class RuntimeInventoryOwnershipTests
string source = ReadSource("Rendering", "GameWindow.cs"); string source = ReadSource("Rendering", "GameWindow.cs");
Assert.Contains( Assert.Contains(
"_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);", "private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new RuntimeInventoryState(",
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
@ -77,11 +81,18 @@ public sealed class RuntimeInventoryOwnershipTests
"_domain.Inventory.DesiredComponents", "_domain.Inventory.DesiredComponents",
session, session,
StringComparison.Ordinal); StringComparison.Ordinal);
AssertAppearsInOrder( Assert.Contains(
"new ResourceShutdownStage(\"game runtime root\"",
shutdown, shutdown,
"\"runtime action state\"", StringComparison.Ordinal);
"\"runtime inventory state\"", Assert.Contains(
"\"runtime entity/object lifetime\""); "Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))",
shutdown,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimeInventoryState Inventory",
shutdown,
StringComparison.Ordinal);
} }
[Fact] [Fact]

View file

@ -36,10 +36,14 @@ public sealed class RuntimeMovementOwnershipTests
app, app,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b")); Assert.Empty(Regex.Matches(app, @"\bbool\s+_autoRunActive\b"));
Assert.Single(Regex.Matches( Assert.Empty(Regex.Matches(
app, app,
@"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(") @"RuntimeLocalPlayerMovementState\s+\w+\s*=\s*new\s*\(")
.Cast<Match>()); .Cast<Match>());
Assert.Single(Regex.Matches(
runtime,
@"new\s+RuntimeLocalPlayerMovementState\s*\(")
.Cast<Match>());
Assert.DoesNotContain( Assert.DoesNotContain(
"ACDREAM_RUN_SKILL", "ACDREAM_RUN_SKILL",
runtime, runtime,
@ -59,10 +63,10 @@ public sealed class RuntimeMovementOwnershipTests
root, root,
"Input", "Input",
"DispatcherMovementInputSource.cs"); "DispatcherMovementInputSource.cs");
string view = ReadAppSource( string adapter = ReadAppSource(
root, root,
"Runtime", "Runtime",
"CurrentGameRuntimeViewAdapter.cs"); "CurrentGameRuntimeAdapter.cs");
string commands = ReadAppSource( string commands = ReadAppSource(
root, root,
"Runtime", "Runtime",
@ -73,7 +77,11 @@ public sealed class RuntimeMovementOwnershipTests
"GameWindowLifetime.cs"); "GameWindowLifetime.cs");
Assert.Contains( Assert.Contains(
"RuntimeLocalPlayerMovementState _playerControllerSlot = new();", "RuntimeLocalPlayerMovementState _playerControllerSlot =>",
gameWindow,
StringComparison.Ordinal);
Assert.Contains(
"_runtime.MovementOwner;",
gameWindow, gameWindow,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
@ -90,16 +98,16 @@ public sealed class RuntimeMovementOwnershipTests
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal); Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"movement ?? throw new ArgumentNullException(nameof(movement))).View", "private readonly GameRuntime _runtime;",
view, adapter,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal); Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"RuntimeLocalPlayerMovementState Movement", "GameRuntime Runtime",
lifetime, lifetime,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"\"runtime local movement state\"", "\"game runtime root\"",
lifetime, lifetime,
StringComparison.Ordinal); StringComparison.Ordinal);
} }

View file

@ -17,6 +17,11 @@ public sealed class RuntimeWorldTransitOwnershipTests
"\n", "\n",
Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories) Directory.EnumerateFiles(appRoot, "*.cs", SearchOption.AllDirectories)
.Select(File.ReadAllText)); .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( Assert.False(File.Exists(Path.Combine(
appRoot, appRoot,
@ -38,10 +43,14 @@ public sealed class RuntimeWorldTransitOwnershipTests
"record struct WorldRevealLifecycleSnapshot", "record struct WorldRevealLifecycleSnapshot",
app, app,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Single(Regex.Matches( Assert.Empty(Regex.Matches(
app, app,
@"new\s+RuntimeWorldTransitState\s*\(") @"new\s+RuntimeWorldTransitState\s*\(")
.Cast<Match>()); .Cast<Match>());
Assert.Single(Regex.Matches(
runtime,
@"new\s+RuntimeWorldTransitState\s*\(")
.Cast<Match>());
} }
[Fact] [Fact]
@ -56,19 +65,17 @@ public sealed class RuntimeWorldTransitOwnershipTests
typeof(RuntimeWorldTransitState), typeof(RuntimeWorldTransitState),
availabilityFields[0].FieldType); availabilityFields[0].FieldType);
var viewFields = typeof(CurrentGameRuntimeViewAdapter) var adapterFields = typeof(CurrentGameRuntimeAdapter)
.GetFields( .GetFields(
System.Reflection.BindingFlags.Instance System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic); | System.Reflection.BindingFlags.NonPublic);
Assert.Contains( Assert.Contains(
viewFields, adapterFields,
field => field.FieldType == typeof(IRuntimePortalView)); field => field.FieldType == typeof(GameRuntime));
Assert.DoesNotContain( Assert.DoesNotContain(
typeof(CurrentGameRuntimeViewAdapter).GetNestedTypes( adapterFields,
System.Reflection.BindingFlags.NonPublic), field => field.FieldType == typeof(IRuntimePortalView)
type => type.Name.Contains( || field.FieldType == typeof(RuntimeWorldTransitState));
"PortalView",
StringComparison.Ordinal));
var coordinatorFields = typeof(WorldRevealCoordinator) var coordinatorFields = typeof(WorldRevealCoordinator)
.GetFields( .GetFields(

View file

@ -363,7 +363,7 @@ public sealed class LandblockBuildOriginTests
Assert.True(sessionStage >= 0); Assert.True(sessionStage >= 0);
int sessionOperation = source.IndexOf( int sessionOperation = source.IndexOf(
"Hard(\"live session\", () => DisposeLiveSession(ingress.LiveSession))", "Hard(\"game runtime session\", ingress.Runtime.StopSession)",
sessionStage, sessionStage,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(sessionOperation > sessionStage); Assert.True(sessionOperation > sessionStage);
@ -380,25 +380,30 @@ public sealed class LandblockBuildOriginTests
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(streamerDispose > dependentStage); Assert.True(streamerDispose > dependentStage);
int helper = source.IndexOf( string runtime = File.ReadAllText(Path.Combine(
"private static void DisposeLiveSession(LiveSessionController? controller)", FindRepoRoot(),
"src",
"AcDream.Runtime",
"GameRuntime.cs"));
int helper = runtime.IndexOf(
"public void StopSession()",
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(helper > streamerDispose); Assert.True(helper >= 0);
int sessionDispose = source.IndexOf( int sessionDispose = runtime.IndexOf(
"controller.Dispose()", "Session.Dispose();",
helper, helper,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(sessionDispose > helper); Assert.True(sessionDispose > helper);
int disposalCompletionBarrier = source.IndexOf( int disposalCompletionBarrier = runtime.IndexOf(
"if (!controller.IsDisposalComplete)", "if (!Session.IsDisposalComplete)",
sessionDispose, sessionDispose,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.True(disposalCompletionBarrier > sessionDispose); Assert.True(disposalCompletionBarrier > sessionDispose);
Assert.Contains( Assert.Contains(
"Live-session disposal was deferred by a reentrant lifecycle callback.", "The Runtime session shutdown was deferred by a re-entrant callback.",
source[disposalCompletionBarrier..], runtime[disposalCompletionBarrier..],
StringComparison.Ordinal); StringComparison.Ordinal);
} }

View file

@ -236,34 +236,18 @@ public sealed class RuntimeEntityOwnershipTests
[Fact] [Fact]
public void CurrentRuntimeAdapters_DoNotRetainEntityOrInventoryMirrors() public void CurrentRuntimeAdapters_DoNotRetainEntityOrInventoryMirrors()
{ {
FieldInfo[] viewFields = typeof(CurrentGameRuntimeViewAdapter) FieldInfo[] adapterFields = typeof(CurrentGameRuntimeAdapter)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic); .GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Contains( Assert.Contains(
viewFields, adapterFields,
field => field.FieldType == typeof(IRuntimeEntityView)); field => field.FieldType == typeof(GameRuntime));
Assert.Contains(
viewFields,
field => field.FieldType == typeof(IRuntimeInventoryView));
Assert.DoesNotContain( Assert.DoesNotContain(
viewFields, adapterFields,
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,
field => field.FieldType == typeof(LiveEntityRuntime) field => field.FieldType == typeof(LiveEntityRuntime)
|| field.FieldType == typeof(ClientObjectTable) || field.FieldType == typeof(ClientObjectTable)
|| field.FieldType == typeof(RuntimeEventSequencer)); || field.FieldType == typeof(IRuntimeEntityView)
Assert.Contains( || field.FieldType == typeof(IRuntimeInventoryView)
eventFields, || field.FieldType == typeof(RuntimeEntityObjectLifetime));
field => field.FieldType
== typeof(RuntimeEntityObjectLifetime));
string root = FindRepositoryRoot(); string root = FindRepositoryRoot();
string liveSource = File.ReadAllText(Path.Combine( string liveSource = File.ReadAllText(Path.Combine(
@ -276,19 +260,34 @@ public sealed class RuntimeEntityOwnershipTests
Assert.DoesNotContain("_directory.TryApply", liveSource); Assert.DoesNotContain("_directory.TryApply", liveSource);
Assert.DoesNotContain("_directory.RemoveActive", liveSource); Assert.DoesNotContain("_directory.RemoveActive", liveSource);
string viewSource = File.ReadAllText(Path.Combine( string appRuntimeRoot = Path.Combine(
root, root,
"src", "src",
"AcDream.App", "AcDream.App",
"Runtime", "Runtime");
"CurrentGameRuntimeViewAdapter.cs")); 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( Assert.Contains(
"_entityView.MaterializedCount", "public RuntimeEntityObjectLifetime EntityObjects",
viewSource, rootSource,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain( Assert.Contains(
"_entities.MaterializedCount", "public IRuntimeEntityView Entities => EntityObjects.EntityView;",
viewSource, rootSource,
StringComparison.Ordinal);
Assert.Contains(
"public IRuntimeInventoryView Inventory => EntityObjects.InventoryView;",
rootSource,
StringComparison.Ordinal); StringComparison.Ordinal);
} }