refactor(app): compose session and player startup

Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 18:28:32 +02:00
parent 3573da12e1
commit 7771c07fb6
31 changed files with 1759 additions and 532 deletions

View file

@ -21,7 +21,8 @@ public sealed class GameWindow :
IGameWindowSettingsDevToolsPublication,
IGameWindowWorldRenderPublication,
IGameWindowInteractionRetainedUiPublication,
IGameWindowLivePresentationPublication
IGameWindowLivePresentationPublication,
IGameWindowSessionPlayerPublication
{
private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp()
@ -99,6 +100,7 @@ public sealed class GameWindow :
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
_renderFrameDiagnostics;
private LivePresentationRuntimeBindings? _livePresentationBindings;
private SessionPlayerRuntimeBindings? _sessionPlayerBindings;
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
@ -123,22 +125,14 @@ public sealed class GameWindow :
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
_streamingOriginRecenter;
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
private AcDream.App.Streaming.DatSpawnClaimHydrationClassifier?
_spawnClaimHydration;
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
_localPlayerTeleportSink = new();
private AcDream.App.Streaming.LocalPlayerTeleportController?
_localPlayerTeleport;
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
new(nearRadius: 4, farRadius: 12);
private int _nearRadius
{
get => _renderRange.NearRadius;
set => _renderRange.NearRadius = value;
}
private int _farRadius
{
get => _renderRange.FarRadius;
set => _renderRange.FarRadius = value;
}
// Phase B.3: physics engine — populated from the streaming pipeline.
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
@ -170,7 +164,7 @@ public sealed class GameWindow :
_liveEntityProjectionWithdrawal;
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
private AcDream.App.Net.LiveEntitySessionController? _liveEntitySessionEvents;
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
_liveEntityMotionBindings = new();
@ -996,6 +990,51 @@ public sealed class GameWindow :
_livePresentationBindings = result.RuntimeBindings;
}
void IGameWindowSessionPlayerPublication.PublishSessionPlayer(
SessionPlayerResult result)
{
ArgumentNullException.ThrowIfNull(result);
if (_streamer is not null
|| _streamingController is not null
|| _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
|| _liveEntitySessionEvents is not null
|| _gameplayInputFrame is not null
|| _localPlayerAnimation is not null
|| _localPlayerShadowSynchronizer is not null
|| _playerModeController is not null
|| _playerModeAutoEntry is not null
|| _localPlayerTeleport is not null
|| _sessionPlayerBindings is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns session/player state.");
}
_streamer = result.Streamer;
_streamingController = result.Streaming;
_streamingOriginRecenter = result.StreamingOriginRecenter;
_worldReveal = result.WorldReveal;
_spawnClaimHydration = result.SpawnClaimHydration;
_liveSessionController = result.LiveSession;
_liveEntityHydration = result.Hydration;
_liveEntityNetworkUpdates = result.NetworkUpdates;
_liveEntityLiveness = result.Liveness;
_liveEntitySessionEvents = result.SessionEvents;
_gameplayInputFrame = result.GameplayInput;
_localPlayerAnimation = result.LocalPlayerAnimation;
_localPlayerShadowSynchronizer = result.LocalPlayerShadow;
_playerModeController = result.PlayerMode;
_playerModeAutoEntry = result.PlayerModeAutoEntry;
_localPlayerTeleport = result.LocalTeleport;
_sessionPlayerBindings = result.RuntimeBindings;
}
private static void PublishCompositionOwner<T>(
ref T? destination,
T value,
@ -1139,9 +1178,6 @@ public sealed class GameWindow :
Console.WriteLine),
this).Compose(platform, contentEffectsAudio, settingsDevTools);
string shadersDir = worldRender.Foundation.ShadersDirectory;
TerrainAtlas terrainAtlas = worldRender.Foundation.TerrainAtlas;
int centerX = worldRender.TerrainBuild.InitialCenterX;
int centerY = worldRender.TerrainBuild.InitialCenterY;
Console.WriteLine(
$"loading world view centered on " +
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
@ -1243,441 +1279,64 @@ public sealed class GameWindow :
worldRender,
interactionUi);
// Apply radii from the same immutable quality snapshot used for the
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
// Legacy ACDREAM_STREAM_RADIUS is still honoured for backward-compat.
_nearRadius = _runtimeSettings.ResolvedQuality.NearRadius;
_farRadius = _runtimeSettings.ResolvedQuality.FarRadius;
// Legacy override: ACDREAM_STREAM_RADIUS acts as nearRadius and
// ensures farRadius >= streamRadius. Parsed once into
// RuntimeOptions.LegacyStreamRadius (null when unset or invalid).
if (_options.LegacyStreamRadius is { } sr)
{
_nearRadius = sr;
_farRadius = System.Math.Max(sr, _farRadius);
}
Console.WriteLine(
$"streaming: nearRadius={_nearRadius} (window={2 * _nearRadius + 1}x{2 * _nearRadius + 1})" +
$" farRadius={_farRadius} (window={2 * _farRadius + 1}x{2 * _farRadius + 1})");
// Phase A.5 T11+: the streamer now runs on a dedicated worker thread.
// loadLandblock acquires _datLock (T10) before touching DatCollection.
// buildMeshOrNull (T12) receives the already-loaded LoadedLandblock so
// it can call LandblockMesh.Build without a dat read — _heightTable and
// _blendCtx are read-only after init, _surfaceCache is ConcurrentDictionary (T9).
var landblockBuildFactory = new AcDream.App.Streaming.LandblockBuildFactory(
_dats!,
_datLock,
_heightTable!,
_physicsDataCache,
_options.DumpSceneryZ);
_streamer = AcDream.App.Streaming.LandblockStreamer.CreateForRequests(
loadLandblock: landblockBuildFactory.Build,
buildMeshOrNull: (id, lb) =>
{
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
return null;
uint lbX = (id >> 24) & 0xFFu;
uint lbY = (id >> 16) & 0xFFu;
// _surfaceCache is ConcurrentDictionary (T9) — safe from worker thread.
// _heightTable and _blendCtx are read-only after initialization.
// lb.Heightmap is the pre-loaded LandBlock; no dat read needed here.
return AcDream.Core.Terrain.LandblockMesh.Build(
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache);
});
_streamer.Start();
_streamingController = new AcDream.App.Streaming.StreamingController(
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
new AcDream.App.Streaming.LandblockBuildRequest(
id,
kind,
generation,
new AcDream.App.Streaming.LandblockBuildOrigin(
_liveCenterX,
_liveCenterY))),
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
drainCompletions: _streamer.DrainCompletions,
state: _worldState,
nearRadius: _nearRadius,
farRadius: _farRadius,
clearPendingLoads: _streamer.ClearPendingLoads,
// Retained-object recovery runs after each
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
// objects it still considers known.
presentationPipeline: _landblockPresentationPipeline!);
_streamingOriginRecenter =
new AcDream.App.Streaming.StreamingOriginRecenterCoordinator(
_streamingController,
_liveWorldOrigin);
// A.5 T22.5: apply max-completions from resolved quality.
_streamingController.MaxCompletionsPerFrame =
_runtimeSettings.ResolvedQuality.MaxCompletionsPerFrame;
_runtimeSettings.BindRuntimeTargets(
new RuntimeSettingsTargets(
new SilkRuntimeDisplayWindowTarget(_window!),
_wbDrawDispatcher!,
terrainAtlas,
_streamingController,
_renderRange,
_uiHost?.Root,
Console.WriteLine));
_worldReveal = new AcDream.App.Streaming.WorldRevealCoordinator(
isRenderNeighborhoodReady: _streamingController.IsRenderNeighborhoodResident,
isSpawnCellReady: _physicsEngine.IsSpawnCellReady,
isTerrainNeighborhoodReady: _physicsEngine.IsNeighborhoodTerrainResident,
areCompositeTexturesReady: () => _wbDrawDispatcher?.CompositeTexturesReady == true,
prepareCompositeTextures: (destinationCell, radius) =>
_wbDrawDispatcher?.PrepareCompositeTextures(
_worldState.Entities,
_worldState.FlatViewGeneration,
destinationCell,
radius),
invalidateCompositeTextures: () =>
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness(),
isSpawnClaimUnhydratable: IsSpawnClaimUnhydratable,
log: message => Console.WriteLine(message));
var networkUpdateBridge =
new AcDream.App.World.DeferredLiveEntityNetworkUpdateSink();
var sealedDungeonCells =
new AcDream.App.Streaming.DatSealedDungeonCellClassifier(
_dats!,
_datLock);
var projectionMaterializer = new DatLiveEntityProjectionMaterializer(
_options,
_dats!,
_liveEntities!,
_physicsDataCache,
_animLoader!,
_wbEntitySpawnAdapter!,
_textureCache!,
_classificationCache,
_effectPoses,
_equippedChildRenderer!,
_worldGameState,
_worldEvents,
_physicsEngine.ShadowObjects,
_liveEntityCollisionBuilder!,
_projectileController!,
_animationPresenter,
_staticAnimationScheduler!,
_liveWorldOrigin,
_updateFrameClock);
var originCoordinator =
new AcDream.App.World.LiveEntityWorldOriginCoordinator(
_liveWorldOrigin,
_streamingController,
_worldState,
_worldReveal,
_localPlayerIdentity,
sealedDungeonCells,
Console.WriteLine);
_liveSessionController = new AcDream.App.Net.LiveSessionController();
interactionUi.LateBindings.AdoptLateOwnerBinding(
"live session",
interactionUi.LateBindings.Session.Bind(_liveSessionController));
var localPhysicsTimestamps =
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
_localPlayerIdentity,
_liveSessionController);
var liveEntityTeardown =
new AcDream.App.World.LiveEntityRuntimeTeardownController(
livePresentation.LiveEntities,
_liveEntityPresentation!,
_entityEffects!,
_remoteTeleportController!,
_selectionInteractions,
_selection,
_animatedEntities,
_remoteMovementObservations,
_translucencyFades,
_liveEntityProjectionWithdrawal!,
_equippedChildRenderer!,
_physicsEngine.ShadowObjects,
_liveEntityLights!,
_classificationCache,
_localPlayerIdentity);
livePresentation.ComponentLifecycle.Bind(liveEntityTeardown);
var liveEntityDeletion =
new AcDream.App.World.LiveEntityDeletionController(
livePresentation.LiveEntities,
Objects,
liveEntityTeardown,
_localPlayerIdentity,
_options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
livePresentation.LiveEntities,
Objects,
_datLock,
projectionMaterializer,
new AcDream.App.World.LiveEntityRelationshipProjection(
livePresentation.EquippedChildren),
new AcDream.App.World.LiveEntityReadyPublisher(
livePresentation.LiveEntities,
_entityEffects!,
_liveEntityPresentation!),
originCoordinator,
networkUpdateBridge,
localPhysicsTimestamps,
_localPlayerIdentity,
liveEntityDeletion,
_options.DumpLiveSpawns ? Console.WriteLine : null);
livePresentation.RuntimeBindings.Adopt(
"landblock-loaded hydration",
livePresentation.LandblockLoaded.Bind(_liveEntityHydration));
_liveEntityNetworkUpdates =
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
livePresentation.LiveEntities,
Objects,
_liveEntityHydration,
_entityEffects!,
_liveEntityPresentation!,
_liveEntityLights!,
_equippedChildRenderer!,
_projectileController!,
_remoteTeleportController!,
_animatedEntities,
new LiveEntityRemoteMotionRuntimeView<RemoteMotion>(
_liveEntityRuntimeSlot),
_remoteMovementObservations,
_remotePhysicsUpdater,
_remoteInboundMotion,
livePresentation.MotionRuntime,
_physicsEngine,
_dats!,
_animLoader!,
_combatTargetController,
_liveWorldOrigin,
_localPlayerTeleportSink,
_playerControllerSlot,
_localPlayerOutbound,
_playerHostSlot,
_localPlayerIdentity,
_updateFrameClock,
_liveSessionController,
localPhysicsTimestamps.Publish,
_movementTruthDiagnostics);
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
livePresentation.LiveEntities,
_localPlayerIdentity,
liveEntityDeletion);
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
_inboundEntityEvents,
_liveEntityHydration,
_liveEntityNetworkUpdates,
_localPlayerTeleportSink,
_entityEffects!);
livePresentation.ParentAcceptance.Bind(
_liveEntityHydration.TryAcceptParentForProjection);
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
livePresentation.EquippedChildren.EntityReady += candidate =>
_liveEntityHydration.OnEntityReady(candidate);
_liveEntityHydration.AppearanceApplied += guid =>
{
if (guid == _playerServerGuid)
_paperdollFramePresenter?.MarkDirty();
};
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
// enter the world as the first character on the account, and stream
// CreateObject messages into _worldGameState as they arrive. Entirely
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
_combatAttackOperations.Bind(
new AcDream.App.Combat.LiveCombatAttackOperations(
Combat,
new AcDream.App.Combat.CombatAttackTargetSource(
_selection,
_liveEntities!,
Objects,
_localPlayerIdentity),
_runtimeSettings,
_playerControllerSlot,
_localPlayerOutbound,
_liveSessionController,
_liveSessionController,
_combatFeedback));
AcDream.App.Input.MouseLookController? mouseLookController =
_mouseSource is not null && _mouseLookCursor is not null
? new AcDream.App.Input.MouseLookController(
_mouseSource,
_pointerPosition,
_localPlayerMode,
_playerControllerSlot,
_cameraController!,
_chaseCameraInput,
_movementInput,
_localPlayerOutbound,
_liveSessionController,
_mouseLookCursor,
new AcDream.App.Input.EnvironmentInputMonotonicClock())
: null;
_gameplayInputFrame = new AcDream.App.Input.GameplayInputFrameController(
_inputDispatcher,
_movementInput,
mouseLookController,
new AcDream.App.Input.CombatAttackInputFrameAdapter(
_combatAttackController!));
_cameraPointerInput?.BindGameplayFrame(_gameplayInputFrame);
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
_options.LiveMode,
_localPlayerMode,
_playerControllerSlot,
_liveSessionController,
_liveWorldOrigin,
_liveEntityNetworkUpdates!,
new AcDream.App.Streaming.FlyCameraStreamingObserverSource(
_cameraController!),
new AcDream.App.Streaming.PhysicsStreamingDungeonCellSource(
_physicsEngine),
_streamingOriginRecenter!,
_streamingController!,
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
_worldState,
livePresentation.LiveEntities));
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
_translucencyFades,
_animationHookFrames!,
_entityEffects!,
_particleSink!,
_liveEntityLights!,
_particleVisibility,
_particleSystem!,
_scriptRunner!,
_updateFrameClock,
new AcDream.App.Update.SettingsParticleRangeSource(
_runtimeSettings));
var liveSpatialReconciler =
new AcDream.App.Update.LiveSpatialPresentationReconciler(
_entityEffects!,
_equippedChildRenderer!,
_particleSink!,
_liveEntityLights!);
_localPlayerAnimation =
new AcDream.App.Input.LocalPlayerAnimationController(
livePresentation.LiveEntities,
_localPlayerIdentity,
_animatedEntities,
_animationPresenter,
_animationHookFrames!);
_localPlayerShadowSynchronizer =
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
_physicsEngine,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_localPlayerShadow);
var localPlayerProjection =
new AcDream.App.Input.LocalPlayerProjectionController(
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_localPlayerShadowSynchronizer));
var localPlayerFrameRuntime =
new AcDream.App.Input.LiveLocalPlayerFrameRuntime(
_cameraController!,
_localPlayerMode,
_playerControllerSlot,
_chaseCameraInput,
_movementInput,
_inputCapture,
livePresentation.LiveEntities,
_localPlayerIdentity,
_playerHostSlot,
localPlayerProjection,
_localPlayerOutbound,
_liveSessionController);
var localPlayerFrame =
new AcDream.App.Input.RetailLocalPlayerFrameController(
localPlayerFrameRuntime,
_movementInput);
var liveObjectFrame = new AcDream.App.Update.LiveObjectFrameController(
_inboundEntityEvents,
localPlayerFrame,
_selectionInteractions,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_liveAnimationScheduler,
livePresentation.StaticAnimationScheduler,
_animationPresenter,
_animatedEntities,
_equippedChildRenderer!,
liveEffectFrame);
_playerModeController = new AcDream.App.Input.PlayerModeController(
_localPlayerMode,
_playerControllerSlot,
_playerHostSlot,
_chaseCameraInput,
_cameraController!,
_physicsEngine,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_liveEntityMotionBindings,
_dats!,
_datLock,
_physicsDataCache,
_animatedEntities,
_localPlayerAnimation,
_localPlayerShadowSynchronizer,
_playerApproachCompletions,
_gameplayInputFrame,
_liveSessionController,
_movementTruthDiagnostics,
_localPlayerSkills,
_viewportAspect);
_devToolsComposition?.LateBindings.PlayerModeCommands.Bind(
_playerModeController);
_devToolsCommandBus?.Bind(_liveSessionController);
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
_liveSessionController,
livePresentation.LiveEntities,
_localPlayerIdentity,
_worldReveal
?? throw new InvalidOperationException(
"World reveal was not composed before player auto-entry."),
_localPlayerMode,
_playerModeController));
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
_localPlayerTeleport = _portalTunnelFallback.Transfer(
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
livePresentation.LiveEntities,
_localPlayerIdentity),
_gameplayInputFrame,
_playerModeController,
new AcDream.App.Streaming.LocalPlayerTeleportStreamingOperations(
_liveWorldOrigin,
_streamingOriginRecenter!,
_streamingController!,
sealedDungeonCells),
_worldReveal,
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
SessionPlayerResult sessionPlayer =
new SessionPlayerCompositionPhase(
new SessionPlayerDependencies(
_options,
_window!,
_datLock,
_runtimeSettings,
settingsDevTools,
_physicsEngine,
livePresentation.LiveEntities,
_physicsDataCache,
_worldGameState,
_worldEvents,
_selection,
Objects,
_classificationCache,
_liveEntityRuntimeSlot,
_animatedEntities,
_remoteMovementObservations,
_remotePhysicsUpdater,
_remoteInboundMotion,
_inboundEntityEvents,
_liveEntityMotionBindings,
_localPlayerIdentity,
_playerControllerSlot,
_playerHostSlot,
_localPlayerMode,
_chaseCameraInput,
_localPlayerOutbound,
_localPlayerTeleportSink,
_liveWorldOrigin,
liveSpatialReconciler),
new AcDream.App.Streaming.LocalPlayerTeleportSession(
_liveSessionController),
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
portalTunnel)));
_localPlayerTeleportSink.Bind(_localPlayerTeleport);
interactionUi.LateBindings.AdoptLateOwnerBinding(
"selection view plane",
interactionUi.LateBindings.SelectionViewPlane.Bind(
_localPlayerTeleport));
_renderRange,
_localPlayerShadow,
_localPlayerSkills,
_viewportAspect,
_playerApproachCompletions,
_pointerPosition,
_movementInput,
_inputCapture,
_particleVisibility,
_translucencyFades,
_effectPoses,
_updateFrameClock,
_movementTruthDiagnostics,
_combatAttackOperations,
Combat,
_combatFeedback,
_portalTunnelFallback,
Console.WriteLine),
this).Compose(
hostInputCamera,
contentEffectsAudio,
worldRender,
interactionUi,
livePresentation);
var teleportRenderState =
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
_localPlayerTeleport);
sessionPlayer.LocalTeleport);
var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource(
_options.LiveMode,
_localPlayerMode);
@ -1743,7 +1402,7 @@ public sealed class GameWindow :
new AcDream.App.Rendering.WorldRenderFrameBuilder(
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
_cameraController!,
_localPlayerTeleport),
sessionPlayer.LocalTeleport),
new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation(
_retailSelectionScene,
_particleVisibility,
@ -1876,7 +1535,7 @@ public sealed class GameWindow :
var privatePresentation =
new AcDream.App.Rendering.PrivatePresentationRenderer(
new AcDream.App.Rendering.LocalPlayerPortalViewport(
_localPlayerTeleport!,
sessionPlayer.LocalTeleport,
_cameraController!),
renderFrameResources,
_paperdollFramePresenter,
@ -1899,19 +1558,19 @@ public sealed class GameWindow :
_devToolsFramePresenter
?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance);
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame,
sessionPlayer.LiveObjectFrame,
_worldState,
_liveSessionController,
localPlayerFrame,
liveSpatialReconciler);
sessionPlayer.LiveSession,
sessionPlayer.LocalPlayerFrame,
sessionPlayer.LiveSpatialReconciler);
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
_cameraController!,
_inputCapture,
_cameraInput,
localPlayerFrameRuntime,
sessionPlayer.LocalPlayerFrameRuntime,
_chaseCameraInput,
localPlayerFrame,
liveSpatialReconciler,
sessionPlayer.LocalPlayerFrame,
sessionPlayer.LiveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
_runtimeSettings,
Combat,
@ -1923,15 +1582,15 @@ public sealed class GameWindow :
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
_updateFrameClock,
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
streamingFrame,
_gameplayInputFrame,
sessionPlayer.StreamingFrame,
sessionPlayer.GameplayInput,
liveFrameCoordinator,
new AcDream.App.Update.LiveEntityLivenessFramePhase(
_liveEntityLiveness,
sessionPlayer.Liveness,
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
_localPlayerTeleport,
sessionPlayer.LocalTeleport,
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
_playerModeAutoEntry),
sessionPlayer.PlayerModeAutoEntry),
cameraFrame);
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
_liveSessionHost = CreateLiveSessionHost();
@ -1988,7 +1647,7 @@ public sealed class GameWindow :
runtimeDiagnostics,
new AcDream.App.Input.PlayerModeGameplayCommands(
_localPlayerMode,
_playerModeController),
sessionPlayer.PlayerMode),
new AcDream.App.Input.ItemTargetModeCommands(
_itemInteractionController!),
new AcDream.App.Input.GameplayCameraModeCommands(
@ -1996,7 +1655,7 @@ public sealed class GameWindow :
combatCommand,
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
_gameplayInputFrame!,
sessionPlayer.GameplayInput,
pointer,
_retailUiRuntime,
_selectionInteractions,
@ -2095,7 +1754,7 @@ public sealed class GameWindow :
private void ResetSessionPlayerPresentation()
{
_playerModeController?.ResetSession();
_spawnClaimRangeMemo = null;
_spawnClaimHydration?.Reset();
}
private void ResetSessionIdentity()
@ -2157,7 +1816,10 @@ public sealed class GameWindow :
return new AcDream.App.Net.LiveSessionEventRouter(
session,
_liveEntitySessionEvents.CreateSink(),
(_liveEntitySessionEvents
?? throw new InvalidOperationException(
"Live entity session routing was not composed."))
.CreateSink(),
new AcDream.App.Net.LiveEnvironmentSessionSink(
_worldEnvironment.ApplyAdminEnvirons,
_worldEnvironment.SynchronizeFromServer),
@ -2392,34 +2054,6 @@ public sealed class GameWindow :
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
=> _framebufferResize.Resize(newSize);
// #107 (2026-06-10): memoized "this indoor spawn claim can never hydrate"
// check against the dat's LandBlockInfo.NumCells. Used by the auto-entry
// hold so a garbage claim doesn't stall login forever; the Resolve-head
// safety net demotes it loudly once entry proceeds.
private (uint Claim, bool Unhydratable)? _spawnClaimRangeMemo;
private bool IsSpawnClaimUnhydratable(uint claim)
{
if ((claim & 0xFFFFu) < 0x0100u) return false;
if (_spawnClaimRangeMemo is { } m && m.Claim == claim) return m.Unhydratable;
bool unhydratable = false;
if (_dats is not null)
{
DatReaderWriter.DBObjs.LandBlockInfo? lbInfo;
lock (_datLock)
{
lbInfo = _dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
(claim & 0xFFFF0000u) | 0xFFFEu);
}
uint low = claim & 0xFFFFu;
unhydratable = lbInfo is null || lbInfo.NumCells == 0
|| low >= 0x0100u + lbInfo.NumCells;
}
_spawnClaimRangeMemo = (claim, unhydratable);
return unhydratable;
}
private void OnClosing()
{
_hostQuiescence.StopAccepting();
@ -2565,11 +2199,18 @@ public sealed class GameWindow :
// the later render-frontend stage still disposes the GL owners.
new ResourceShutdownStage("frame borrowers",
[
new("runtime settings targets", _runtimeSettings.UnbindRuntimeTargets),
new("world frame composition", () =>
{
_frameGraphs.Withdraw();
}),
new("session/player bindings", () =>
{
SessionPlayerRuntimeBindings? bindings = _sessionPlayerBindings;
if (bindings is null)
return;
bindings.Dispose();
_sessionPlayerBindings = null;
}),
]),
new ResourceShutdownStage("session dependents",
[
@ -2587,8 +2228,6 @@ public sealed class GameWindow :
_cameraPointerInput;
if (pointer is null) return;
pointer.ReleaseMouseLookAfterSessionRetirement();
if (_gameplayInputFrame is { } gameplayFrame)
pointer.UnbindGameplayFrame(gameplayFrame);
_cameraPointerInput = null;
}),
new("retail UI", () =>