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);
_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(
session,
runtime.Session,
sessionHost,
commands,
_view,
inventory,
character,
actions,
movement,
this,
runtime.InventoryOwner,
runtime.CharacterOwner,
runtime.ActionOwner,
runtime.MovementOwner,
selection,
_events);
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()
{
lock (_subscriptionGate)
{
if (_disposed)
return;
_disposed = true;
_view.Deactivate();
_events.Dispose();
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;

View file

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

View file

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

View file

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

View file

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

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;
[Fact]
public void GameWindowRetainsControllerAndFocusedHostButNoMirroredSession()
public void GameWindowRetainsCanonicalRuntimeAndFocusedHostButNoMirroredSession()
{
FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance);
Assert.Contains(
fields,
field => field.Name == "_liveSessionController"
&& field.FieldType == typeof(LiveSessionController));
field => field.Name == "_runtime"
&& field.FieldType == typeof(GameRuntime));
Assert.Contains(
fields,
field => field.Name == "_liveSessionHost"
&& field.FieldType == typeof(LiveSessionHost));
Assert.DoesNotContain(
fields,
field => field.Name == "_liveSessionController"
|| field.FieldType == typeof(LiveSessionController));
Assert.DoesNotContain(fields, field => field.Name == "_liveSession");
Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(LiveSessionResetPlan));
@ -54,6 +58,48 @@ public sealed class GameWindowLiveSessionOwnershipTests
|| field.FieldType == typeof(ulong));
}
[Fact]
public void ProductionWindowConstructsOnlyTheCanonicalRuntimeRoot()
{
string root = FindRepositoryRoot();
string source = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.Equal(
1,
CountOccurrences(source, "new GameRuntime("));
Assert.Contains(
"private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.Contains(
"_runtimeHostLease = _runtime.AcquireHostLease(",
source,
StringComparison.Ordinal);
string[] forbidden =
[
"new RuntimeEntityObjectLifetime(",
"new RuntimeInventoryState(",
"new RuntimeCharacterState(",
"new RuntimeCommunicationState(",
"new RuntimeActionState(",
"new RuntimeLocalPlayerMovementState(",
"new RuntimeWorldTransitState(",
"new LiveSessionController(",
"new GameRuntimeClock(",
];
Assert.All(
forbidden,
value => Assert.DoesNotContain(
value,
source,
StringComparison.Ordinal));
}
[Theory]
[InlineData("TryStartLiveSession")]
[InlineData("ClearInboundEntityState")]
@ -66,4 +112,32 @@ public sealed class GameWindowLiveSessionOwnershipTests
{
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateInstance));
}
private static int CountOccurrences(string source, string value)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(
value,
cursor,
StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
return current.FullName;
current = current.Parent;
}
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
}
}

View file

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

View file

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

View file

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

View file

@ -8,7 +8,15 @@ public sealed class RuntimeCharacterOwnershipTests
string source = ReadSource("Rendering", "GameWindow.cs");
Assert.Contains(
"_runtimeCharacter = new RuntimeCharacterState();",
"private readonly GameRuntime _runtime;",
source,
StringComparison.Ordinal);
Assert.Contains(
"_runtime = new GameRuntime(new GameRuntimeDependencies(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"new RuntimeCharacterState(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
@ -76,7 +84,11 @@ public sealed class RuntimeCharacterOwnershipTests
inventory,
StringComparison.Ordinal);
Assert.Contains(
"\"runtime character state\"",
"new ResourceShutdownStage(\"game runtime root\"",
shutdown,
StringComparison.Ordinal);
Assert.DoesNotContain(
"RuntimeCharacterState Character",
shutdown,
StringComparison.Ordinal);
}

View file

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

View file

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

View file

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

View file

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

View file

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