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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ using AcDream.App.Settings;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
using AcDream.Core.Player;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Input;
using AcDream.UI.Abstractions.Panels.Chat;
@ -35,16 +36,22 @@ internal sealed record SettingsDevToolsDependencies(
RuntimeSettingsController Settings,
IRuntimeSettingsStartupTarget StartupTarget,
HostQuiescenceGate HostQuiescence,
RuntimeCommunicationState Communication,
CombatState Combat,
LocalPlayerState LocalPlayer,
GameRuntime Runtime,
SettingsDevToolsOptionalDependencies? DevTools,
RuntimeDiagnosticCommandSlot DiagnosticCommands,
CombatFeedbackSlot CombatFeedback,
KeyBindings KeyBindings,
FrameProfiler FrameProfiler,
FramebufferResizeController FramebufferResize,
Action<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
{

View file

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

View file

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

View file

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

View file

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

View file

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

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,
IGameRuntimeClock
{
private readonly GameRuntimeClock _runtime = new();
private readonly GameRuntimeClock _runtime;
public UpdateFrameClock()
: this(new GameRuntimeClock())
{
}
public UpdateFrameClock(GameRuntimeClock runtime)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
}
public ulong FrameNumber => _runtime.FrameNumber;
public double SimulationTimeSeconds => _runtime.SimulationTimeSeconds;