using AcDream.Core.Plugins; using AcDream.App.Composition; using AcDream.App.Physics; using AcDream.App.Rendering.Wb; using AcDream.App.Settings; using AcDream.App.World; using AcDream.Content; using DatReaderWriter; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Rendering; public sealed class GameWindow : IDisposable, IGameWindowPlatformPublication, IGameWindowHostInputCameraPublication, IGameWindowContentEffectsAudioPublication, IGameWindowSettingsDevToolsPublication, IGameWindowWorldRenderPublication, IGameWindowInteractionRetainedUiPublication, IGameWindowLivePresentationPublication { private static double ClientTimerNow() => System.Diagnostics.Stopwatch.GetTimestamp() / (double)System.Diagnostics.Stopwatch.Frequency; private readonly AcDream.App.RuntimeOptions _options; private readonly AnimationPresentationDiagnostics _animationDiagnostics; private readonly string _datDir; private readonly WorldGameState _worldGameState; private readonly WorldEvents _worldEvents; private readonly AcDream.Core.Selection.SelectionState _selection; private readonly HostQuiescenceGate _hostQuiescence = new(); private IWindow? _window; private SilkWindowCallbackBinding? _windowCallbacks; private GL? _gl; private IInputContext? _input; private TerrainModernRenderer? _terrain; /// Phase N.5b: terrain_modern.vert/.frag program. Owned by /// at draw time but allocated + disposed here. private Shader? _terrainModernShader; private CameraController? _cameraController; private IDatReaderWriter? _dats; private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new(); private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput; private Shader? _meshShader; private TextureCache? _textureCache; /// Phase N.4+: WB-backed rendering pipeline adapter. Always non-null /// after OnLoad completes (modern path is mandatory as of N.5). private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter; private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter; private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator; private RetailStaticAnimatingObjectScheduler? _staticAnimationScheduler; private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher; private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene; private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery; private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions; /// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters /// support. Required at startup — missing bindless throws /// in OnLoad. private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport; private SamplerCache? _samplerCache; private DebugLineRenderer? _debugLines; // K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder // wireframes are noisy outdoors and confuse first-time users into // thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel // → Diagnostics → "Toggle collision wires" button toggles too. private readonly AcDream.App.Rendering.WorldSceneDebugState _worldSceneDebugState = new(); // Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in // favor of the ImGui-backed DebugPanel (see _debugVm below). The // TextRenderer + BitmapFont fields stay alive because they're shared // with UiHost and reserved for the future world-space HUD (D.6 — // damage floaters, name plates) where ImGui can't reach into the 3D // scene. They are no longer used for any debug overlay. private TextRenderer? _textRenderer; private BitmapFont? _debugFont; // [FRAME-DIAG]: retain only the render-thread entity upload distribution. // Landblock publication timing/counts live with the focused publishers and // are read from their typed snapshots when diagnostics flush. private readonly bool _frameDiag = string.Equals( System.Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", System.StringComparison.Ordinal); // MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call // per OnRender + three stage scopes. All logic lives in // AcDream.App.Diagnostics.FrameProfiler (structure rule 1). private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new(); private readonly AcDream.App.Rendering.IRenderFrameDiagnosticLog _renderDiagnosticLog = new AcDream.App.Rendering.ConsoleRenderFrameDiagnosticLog(); private readonly AcDream.App.Rendering.DebugVmRenderFactsPublisher _debugVmRenderFacts = new(); private AcDream.App.Rendering.RenderFrameDiagnosticsController? _renderFrameDiagnostics; private LivePresentationRuntimeBindings? _livePresentationBindings; private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots; private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation; private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights; private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameRenderResourceLifetime _renderResourceLifetime = new(); private readonly AcDream.App.Rendering.GlConstructionCleanupLedger _glConstructionCleanup = new(); private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment = new(Console.WriteLine); private ResourceShutdownTransaction? _shutdown; private readonly DisplayFramePacingController _displayFramePacing; private readonly RuntimeSettingsController _runtimeSettings; // Phase A.1: streaming fields replacing the one-shot _entities list. private AcDream.App.Streaming.LandblockStreamer? _streamer; private AcDream.App.Streaming.GpuWorldState _worldState = new(); private AcDream.App.Streaming.LandblockPresentationPipeline? _landblockPresentationPipeline; private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer; private AcDream.App.Streaming.StreamingController? _streamingController; private AcDream.App.Streaming.StreamingOriginRecenterCoordinator? _streamingOriginRecenter; private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal; 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(); // Task 4: physics data cache — BSP trees + collision shapes extracted from // GfxObj/Setup dats during streaming. Populated on the worker thread; // ConcurrentDictionary inside makes cross-thread access safe. private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache = new(); // #184 Slice 2a: the per-remote dead-reckoning tick, extracted out of the // >10k-line TickAnimations (Code Structure Rule 1). Reusable setup/motion // policy is supplied through the once-bound live motion runtime, while the // stateless server-velocity cycle remains a focused Physics helper. private readonly AcDream.App.Physics.RemotePhysicsUpdater _remotePhysicsUpdater; private readonly AcDream.App.Physics.RemoteInboundMotionDispatcher _remoteInboundMotion; private readonly AcDream.App.World.RetailInboundEventDispatcher _inboundEntityEvents = new(); private LiveEntityAnimationScheduler _liveAnimationScheduler = null!; private LiveEntityAnimationPresenter _animationPresenter = null!; private readonly AcDream.App.Input.MovementTruthDiagnosticController _movementTruthDiagnostics; private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound; private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new(); 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. private AcDream.App.Physics.ProjectileController? _projectileController; private AcDream.App.World.LiveEntityProjectionWithdrawalController? _liveEntityProjectionWithdrawal; private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration; private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates; private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!; private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings _liveEntityMotionBindings = new(); // Step 4: portal-based interior cell visibility. private readonly CellVisibility _cellVisibility = new(); // Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe. // DatReaderWriter's DatBinReader uses a shared buffer position internally — // concurrent _dats.Get calls from the streaming worker thread (T11+) and // the render thread (LandblockBuildFactory on the worker; live-spawn // handlers + animation ticks on the render // thread) corrupt that state and produce half-populated LandBlock.Height[] // arrays, rendering as "a giant ball with spikes". All _dats.Get call // sites that can race with the streaming worker MUST hold this lock. // // Worker-thread DAT reads enter through LandblockBuildFactory.Build, which // holds _datLock for the complete CPU transaction. The terrain-mesh closure // consumes that completed heightmap and performs no DAT read. Render-thread // live hydration holds this lock via its outer wrapper; all remaining // render-thread // _dats.Get calls run only when no worker dat read can be in flight (during // initialization or within the same lock scope). private readonly object _datLock = new(); // Terrain build context shared by the off-thread mesh-build closure across // all streamed landblocks. private float[]? _heightTable; private AcDream.Core.Terrain.TerrainBlendingContext? _blendCtx; // Was: Dictionary. ConcurrentDictionary so the off-thread // mesh builder (A.5 T11+) can call LandblockMesh.Build without a lock. private System.Collections.Concurrent.ConcurrentDictionary? _surfaceCache; // Phase A8 (2026-05-28): WB EnvCellRenderManager port. Cells render // through this dedicated pipeline now, NOT through WbDrawDispatcher. // The dispatcher's IndoorPass still walks cell-static stabs (WorldEntity // records with real GfxObj MeshRefs); only the cell GEOMETRY (walls / // floors / ceilings) flows through here. private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer; private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum; // R1 (render redesign): portal-view draw owners are retained transitively // by the frame orchestrator rather than duplicated as GameWindow roots. private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask; private readonly AcDream.App.Rendering.TransferableResourceSlot< AcDream.App.Rendering.PortalTunnelPresentation> _portalTunnelFallback = new(); // Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain // UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and // REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip // state is re-uploaded each frame before terrain/entities draw, so the whole // scene renders ungated (identical to pre-U.3). The buffer ids are handed to the // three renderers so each re-binds binding=2 immediately before its own draw. // U.4 replaces the NoClip() frame with one built from the portal-visibility result. private ClipFrame? _clipFrame; /// /// Phase 6.4: per-entity animation playback state for entities whose /// MotionTable resolved to a real cycle. The render loop ticks each /// of these every frame, advances the current frame number, then /// rebuilds the entity's MeshRefs by re-flattening the Setup against /// the new . /// Static decorations and entities with no motion table never /// appear in this map. /// private readonly LiveEntityAnimationRuntimeView _animatedEntities; private readonly AcDream.App.World.LiveEntityRuntimeSlot _liveEntityRuntimeSlot = new(); /// /// Tier 1 cache (#53): per-entity classification results for static /// entities (those NOT in ). Conceptually /// paired with — that dictionary is the /// gating predicate, this cache is the lookup that depends on it. /// Passed to at /// construction time. Tasks 9-10 of the cache plan wire the per-entity /// miss-populate / hit-fast-path through the dispatcher's loop. /// private readonly AcDream.App.Rendering.Wb.EntityClassificationCache _classificationCache = new(); private AcDream.Core.Physics.IAnimationLoader? _animLoader; private AcDream.App.Physics.LiveEntityCollisionBuilder? _liveEntityCollisionBuilder; // Phase E.1: central fan-out for animation hooks. Audio (E.2), // particles (E.3), combat (E.4), and renderer state mutators all // register sinks at startup. The router is always non-null so the // per-entity tick loop can just call it unconditionally. private readonly AcDream.Core.Physics.AnimationHookRouter _hookRouter = new(); private AcDream.App.Composition.AnimationHookRegistrationSet? _hookRegistrations; private readonly AcDream.App.Rendering.Vfx.DeferredEntityEffectAdvanceSource _entityEffectAdvance = new(); // Phase E.2 audio. Null when ACDREAM_NO_AUDIO=1 or the OpenAL driver // failed to init; all three are set together. private AcDream.App.Audio.OpenAlAudioEngine? _audioEngine; private AcDream.Core.Audio.DatSoundCache? _soundCache; private AcDream.App.Audio.DictionaryEntitySoundTable? _entitySoundTables; private AcDream.App.Audio.AudioHookSink? _audioSink; // Phase E.3 particles. private AcDream.Core.Vfx.EmitterDescRegistry? _emitterRegistry; private AcDream.Core.Vfx.ParticleSystem? _particleSystem; private AcDream.Core.Vfx.ParticleHookSink? _particleSink; private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new(); private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new(); private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames; // Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754) // and typed PlayScriptType (0xF755) events through one effect owner, then // fans every dat-defined hook to particles, audio, lights, translucency, // and nested/default-script routing at its StartTime offset. private AcDream.Core.Vfx.PhysicsScriptRunner? _scriptRunner; private AcDream.Content.Vfx.RetailPhysicsScriptLoader? _physicsScriptLoader; private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects; private AcDream.App.Rendering.ParticleRenderer? _particleRenderer; private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new(); // Remote-entity motion inference: tracks when each remote entity last // moved meaningfully. Used in TickAnimations to swap to Ready when // position has stalled for >StopIdleMs — retail observer pattern per // ACE Player_Tick.cs line 368: the client never sends "released forward" // MoveToState, so the server never broadcasts an explicit stop. Observer // must infer it from position deltas. private readonly AcDream.App.Physics.RemoteMovementObservationTracker _remoteMovementObservations = new(); /// /// Per-remote-entity movement state for smoothing between server /// UpdatePosition broadcasts. Retail queues each received position and /// advances the body toward the queue head through /// InterpolationManager::adjust_offset every object tick. /// /// /// Each entry owns the retail interpolation queue, MovementManager, /// PhysicsBody, latest authoritative position, and the velocity estimate /// used only to select animation cycles for NPCs that receive no motion /// messages. Body translation consumes queue catch-up plus the literal /// root-motion Frame emitted by CSequence; it never guesses movement from /// a Walk/Run command or from packet cadence. /// /// /// /// L.2g S1 (DEV-6): per-entity inbound movement-event staleness gates, /// keyed by server guid. Retail keeps these stamps on CPhysicsObj /// (update_times); seeded from CreateObject's PhysicsDesc timestamp /// block during , consulted at the /// top of , and dropped by the live /// entity teardown owner. /// private AcDream.App.World.LiveEntityRuntime? _liveEntities; private AcDream.App.World.LiveEntityLivenessController? _liveEntityLiveness; // Phase F.1-H.1 — client-side state classes fed by GameEventWiring. // Exposed publicly so plugins + UI panels can bind directly. public readonly AcDream.Core.Chat.ChatLog Chat = new(); // Phase I.6 — runtime state for retail's TurbineChat (0xF7DE) global // chat rooms. Empty/disabled until the server fires // SetTurbineChatChannels (0x0295) shortly after EnterWorld. public readonly AcDream.Core.Chat.TurbineChatState TurbineChat = new(); public readonly AcDream.Core.Combat.CombatState Combat = new(); public readonly AcDream.Core.Items.ItemManaState ItemMana = new(); public readonly AcDream.Core.Social.FriendsState Friends = new(); public readonly AcDream.Core.Social.SquelchState Squelch = new(); public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; } = System.Array.Empty<(uint Id, uint Amount)>(); // Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog // installs that immutable table once DatCollection opens in OnLoad. public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata; public readonly AcDream.Core.Spells.Spellbook SpellBook = null!; public readonly AcDream.Core.Items.ClientObjectTable Objects = new(); /// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source). private readonly ShortcutSnapshotState _shortcutSnapshots = new(); public IReadOnlyList Shortcuts { get => _shortcutSnapshots.Items; private set => _shortcutSnapshots.Items = value; } // Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from // PlayerDescription so the Vitals HUD can render those bars. // Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment // buffs into the max formula via Spellbook.GetVitalMod. public readonly AcDream.Core.Player.LocalPlayerState LocalPlayer = null!; // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1. // See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy. private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter; private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus; private DevToolsCompositionOwner? _devToolsComposition; private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm; private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm; // Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.UiHost? _uiHost; private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime; private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new(); private InteractionUiLateBindings? _interactionUiLateBindings; private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new(); private readonly AcDream.App.Combat.CombatAttackOperationsSlot _combatAttackOperations = new(); private readonly AcDream.App.Combat.CombatFeedbackSlot _combatFeedback = new(); private AcDream.App.Combat.CombatAttackController? _combatAttackController; private AcDream.App.Combat.CombatTargetController? _combatTargetController; private AcDream.App.UI.ItemInteractionController? _itemInteractionController; private readonly AcDream.Core.Items.ExternalContainerState _externalContainers = new(); private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle; private AcDream.App.Spells.MagicRuntime? _magicRuntime; private AcDream.App.Spells.MagicCatalog? _magicCatalog; private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new(); // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport // widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625). private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer; private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter; // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry; // Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns // its panel; the VM remains here because runtime feedback producers bind // directly to it during composition. private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm; // DevToolsEnabled reads through typed RuntimeOptions. private bool DevToolsEnabled => _options.DevTools; // Phase G.1-G.2 world lighting/time state. The environment owner keeps // the clock, selected day group, and weather transitions coherent. public readonly AcDream.Core.World.WorldTimeService WorldTime; public readonly AcDream.Core.Lighting.LightManager Lighting = new(); public readonly AcDream.Core.World.WeatherSystem Weather; // Wired into the hook router in OnLoad so SetLightHook fires // from the animation pipeline flip the matching LightSource.IsLit. private AcDream.Core.Lighting.LightingHookSink? _lightingSink; private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights; private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation; // #188 — TransparentPartHook fires from the animation pipeline drive // a per-(entity,part) translucency ramp; WbDrawDispatcher reads it // per frame. Wired into the hook router in OnLoad, advanced once per // frame in the main loop regardless of _animatedEntities membership // (a fade must keep running even if its entity's one-shot cycle ends). private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades = new(); private AcDream.Core.Rendering.TranslucencyHookSink? _translucencySink; // Phase G.1 sky renderer + shared UBO. Created once the GL context // exists in OnLoad; shared across every other renderer via // binding = 1 so terrain/mesh/instanced/sky all read the same // sun / ambient / fog / flash data per frame. private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo; private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer; // Phase B.2: player movement mode. private readonly AcDream.App.Input.LocalPlayerControllerSlot _playerControllerSlot = new(); private AcDream.App.Input.PlayerMovementController? _playerController => _playerControllerSlot.Controller; private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new(); private AcDream.App.Rendering.ChaseCamera? _chaseCamera => _chaseCameraInput.Legacy; private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera => _chaseCameraInput.Retail; private readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new(); private bool _playerMode => _localPlayerMode.IsPlayerMode; private readonly AcDream.App.Input.LocalPlayerIdentityState _localPlayerIdentity = new(); private uint _playerServerGuid { get => _localPlayerIdentity.ServerGuid; set => _localPlayerIdentity.ServerGuid = value; } // Retail Default_CharacterOption (acclient.h:3434). PlayerDescription // replaces this before any in-world drag can normally occur. private readonly PlayerCharacterOptionsState _characterOptions = new(); private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new(); private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new(); private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new(); private readonly FramebufferResizeController _framebufferResize; private AcDream.App.Input.PlayerModeController? _playerModeController; private readonly AcDream.App.Interaction.PlayerApproachCompletionState _playerApproachCompletions = new(); private AcDream.App.Input.LocalPlayerAnimationController? _localPlayerAnimation; private AcDream.App.Physics.LocalPlayerShadowSynchronizer? _localPlayerShadowSynchronizer; // Phase D.2b-C — live character-sheet assembly + raise flow (extracted // feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider; // Phase K.2 — auto-enter player mode after a successful login. Armed // by LiveSessionHost's entered-world transition; ticked from // OnUpdate; disarmed if the user manually enters fly mode (or any // other path that pre-empts the chase camera). Skipped entirely // offline (orbit camera stays the foreground). The class internally // tracks IsArmed; we read it via the guard rather than mirroring // the bool here. private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry; // Phase K.1b / Slice 8 F — one semantic input path. Transitional actions // flow through GameplayInputActionRouter; held movement is polled through // InputDispatcher. Raw axis motion belongs to CameraPointerInputController. private AcDream.App.Input.SilkKeyboardSource? _kbSource; private AcDream.App.Input.SilkMouseSource? _mouseSource; private AcDream.UI.Abstractions.Input.InputDispatcher? _inputDispatcher; private readonly AcDream.App.Input.RetainedUiInputCaptureSlot _retainedInputCapture; private readonly AcDream.App.Input.CompositeInputCaptureSource _inputCapture; private readonly AcDream.App.Input.DispatcherMovementInputSource _movementInput; private readonly AcDream.App.Input.DispatcherCameraInputSource _cameraInput = new(); private AcDream.App.Input.IMouseLookCursor? _mouseLookCursor; private AcDream.App.Input.GameplayInputFrameController? _gameplayInputFrame; private AcDream.App.Input.GameplayInputActionRouter? _gameplayInputActions; private AcDream.App.Input.RetainedUiGameplayBinding? _retainedUiGameplayBinding; private readonly AcDream.App.Diagnostics.RuntimeDiagnosticCommandSlot _runtimeDiagnosticCommands = new(); private readonly AcDream.App.Combat.LiveCombatModeCommandSlot _liveCombatModeCommands = new(); // K.1c: load user-customized bindings from %LOCALAPPDATA%\acdream\keybinds.json, // falling back to the retail-faithful defaults if the file is missing // or corrupt. This is THE single source of truth for the keymap at // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults() // should land in the GameWindow construction path. private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings = LoadStartupKeyBindings(); private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings() { var path = AcDream.UI.Abstractions.Input.KeyBindings.DefaultPath(); var bindings = AcDream.UI.Abstractions.Input.KeyBindings.LoadOrDefault(path); Console.WriteLine($"keybinds: loaded {bindings.All.Count} bindings from {path}"); return bindings; } // Phase 4.7: optional live connection to an ACE server. Enabled only when // ACDREAM_LIVE=1 is in the environment — fully backward compatible with // the offline rendering pipeline. // Slice 3: the controller is the sole App owner. Outbound feature // callbacks resolve this borrowed handle at call time and never cache it. private AcDream.App.Net.LiveSessionController? _liveSessionController; private AcDream.App.Net.LiveSessionHost? _liveSessionHost; private AcDream.Core.Net.WorldSession? LiveSession => _liveSessionHost?.CurrentSession; private readonly AcDream.App.World.LiveWorldOriginState _liveWorldOrigin = new(); private int _liveCenterX => _liveWorldOrigin.CenterX; private int _liveCenterY => _liveWorldOrigin.CenterY; // K-fix1 (2026-04-26): cached at startup so per-frame branches are // single-flag reads instead of env-var lookups. True iff // ACDREAM_LIVE=1 was set when the window came up. // Backed by RuntimeOptions.LiveMode via the _options field. /// /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so /// UpdateMotion and UpdatePosition handlers can find the entity the /// server is talking about. This is the canonical materialized top-level /// projection, including live objects parked in pending landblocks; /// visibility must never suppress authoritative mutation. /// private static readonly IReadOnlyDictionary EmptyLiveEntityMap = new Dictionary(); private static readonly IReadOnlyDictionary EmptyLiveSpawnMap = new Dictionary(); private IReadOnlyDictionary _entitiesByServerGuid => _liveEntities?.MaterializedWorldEntities ?? EmptyLiveEntityMap; /// Visible-only view for radar, picking, status, and targets. private IReadOnlyDictionary _visibleEntitiesByServerGuid => _liveEntities?.WorldEntities ?? EmptyLiveEntityMap; /// /// Latest for each /// guid. Captured before the renderability gate so no-position inventory / /// parented children retain Setup and parent metadata; hydration rereads /// this canonical snapshot after every relationship callback. /// reuses the cached /// position/setup/motion fields when a 0xF625 ObjDescEvent carries only /// updated visuals. /// private IReadOnlyDictionary LastSpawns => _liveEntities?.Snapshots ?? EmptyLiveSpawnMap; // R5-V2: the local player's CPhysicsObj stand-in — owns the player's // TargetManager voyeur system, stored on the exact live record so remote // entities chasing the player resolve it. Replaces the AP-79 // _playerMoveToTarget* poll fields. private readonly AcDream.App.Input.LocalPlayerPhysicsHostSlot _playerHostSlot = new(); private EntityPhysicsHost? _playerHost => _playerHostSlot.Host; // R5-V2: guid → per-entity IPhysicsObjHost registry (retail's // CObjectMaint::GetObjectA lookup). Backs every host's GetObjectA seam, // giving the TargetManager voyeur round-trip its cross-entity delivery // path. Populated for remotes plus the PlayerModeController local entry // (player); pruned only by logical LiveEntityRuntime teardown. // ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184 // Slice 2a — the DR tick's stale-velocity anim-stop was its only user). public GameWindow( AcDream.App.RuntimeOptions options, WorldGameState worldGameState, WorldEvents worldEvents, AcDream.Core.Selection.SelectionState selection, AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null) { _options = options ?? throw new System.ArgumentNullException(nameof(options)); WorldTime = _worldEnvironment.WorldTime; Weather = _worldEnvironment.Weather; _retainedInputCapture = new AcDream.App.Input.RetainedUiInputCaptureSlot(); _inputCapture = new AcDream.App.Input.CompositeInputCaptureSource( new AcDream.App.Input.DevToolsInputCaptureSource(options.DevTools), _retainedInputCapture); _movementInput = new AcDream.App.Input.DispatcherMovementInputSource( _inputCapture); _framebufferResize = new FramebufferResizeController(_viewportAspect); _datDir = options.DatDir; _worldGameState = worldGameState; _worldEvents = worldEvents; _displayFramePacing = new DisplayFramePacingController( options.UncappedRendering, _frameProfiler); _runtimeSettings = new RuntimeSettingsController( new JsonRuntimeSettingsStorage( AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath()), log: Console.WriteLine); _selection = selection ?? throw new System.ArgumentNullException(nameof(selection)); _animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment(); _uiRegistry = uiRegistry; _animatedEntities = new LiveEntityAnimationRuntimeView( _liveEntityRuntimeSlot); SpellBook = new AcDream.Core.Spells.Spellbook(); LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook); // #184 Slice 2a: the extracted per-remote DR tick. Its stateful // setup/motion dependency crosses the fail-fast construction bridge; // the server-velocity animation cycle is stateless shared policy. _remotePhysicsUpdater = new AcDream.App.Physics.RemotePhysicsUpdater( _physicsEngine, _liveEntityMotionBindings.GetSetupCylinder, AcDream.App.Physics.RemoteServerControlledVelocityCycle.Apply); _remoteInboundMotion = new AcDream.App.Physics.RemoteInboundMotionDispatcher( (movement, cellId, update) => _liveEntityMotionBindings.RouteServerMoveTo( movement, cellId, update), (host, targetGuid) => _liveEntityMotionBindings.StickToObjectFromWire(host, targetGuid)); _movementTruthDiagnostics = new AcDream.App.Input.MovementTruthDiagnosticController( options.DumpMoveTruth, _playerControllerSlot, _localPlayerIdentity); _localPlayerOutbound = new AcDream.App.Input.LocalPlayerOutboundController( _movementTruthDiagnostics); } public void Run() { // The runtime-settings owner was constructed before Window.Create and // exposes the one immutable startup snapshot. MSAA is a context // attribute, so it must come from this same snapshot rather than a // second settings load during OnLoad. RuntimeSettingsSnapshot startup = _runtimeSettings.Startup; FramePacingPolicy startupPacing = _displayFramePacing.InitializeStartup(startup.Display.VSync); var options = WindowOptions.Default with { Size = new Vector2D(1280, 720), Title = "acdream — phase 1", API = new GraphicsAPI( ContextAPI.OpenGL, ContextProfile.Core, ContextFlags.ForwardCompatible, new APIVersion(4, 3)), VSync = startupPacing.UseVSync, // A.5 T22.5: MSAA from quality preset (0 = disabled, 2/4/8 = multisample). // Silk.NET passes this to SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES). // Cannot be changed at runtime; Quality changes mid-session that would // alter MsaaSamples are logged as a restart-required warning. Samples = startup.Quality.MsaaSamples, // #117 (2026-06-11): the aperture punch's depth gate needs a // stencil buffer (PortalDepthMaskRenderer two-pass mark+punch). // GLFW defaults to 8 stencil bits, but make the requirement // explicit rather than platform-implicit. PreferredStencilBufferBits = 8, }; _window = Window.Create(options); _displayFramePacing.BindSurface( new SilkDisplayFramePacingSurface(_window)); // The fixed binding preserves main Render before post-render pacing, // owns rollback/reverse-detach, and gates every native entry point. _windowCallbacks = SilkWindowCallbackBinding.Create( _window, new WindowCallbackTargets( OnLoad, OnUpdate, OnRender, OnClosing, OnFocusChanged, OnFramebufferResize), _displayFramePacing, _hostQuiescence); _windowCallbacks.Attach(); try { _window.Run(); } catch (Exception failure) { _glConstructionCleanup.RetainFrom(failure); throw; } } void IGameWindowPlatformPublication.PublishGraphics( GL graphics) => PublishCompositionOwner(ref _gl, graphics, "graphics API"); void IGameWindowPlatformPublication.PublishInput( IInputContext input) => PublishCompositionOwner(ref _input, input, "input context"); void IGameWindowHostInputCameraPublication.PublishGpuFrameFlights( GpuFrameFlightController value) => PublishCompositionOwner(ref _gpuFrameFlights, value, "GPU frame flights"); void IGameWindowHostInputCameraPublication.PublishKeyboardSource( AcDream.App.Input.SilkKeyboardSource value) => PublishCompositionOwner(ref _kbSource, value, "keyboard source"); void IGameWindowHostInputCameraPublication.PublishMouseSource( AcDream.App.Input.SilkMouseSource value) => PublishCompositionOwner(ref _mouseSource, value, "mouse source"); void IGameWindowHostInputCameraPublication.PublishMouseLookCursor( AcDream.App.Input.IMouseLookCursor value) => PublishCompositionOwner(ref _mouseLookCursor, value, "mouse-look cursor"); void IGameWindowHostInputCameraPublication.PublishInputDispatcher( AcDream.UI.Abstractions.Input.InputDispatcher value) => PublishCompositionOwner(ref _inputDispatcher, value, "input dispatcher"); void IGameWindowHostInputCameraPublication.PublishCameraController( CameraController value) => PublishCompositionOwner(ref _cameraController, value, "camera controller"); void IGameWindowHostInputCameraPublication.PublishCameraPointerInput( AcDream.App.Input.CameraPointerInputController value) => PublishCompositionOwner(ref _cameraPointerInput, value, "camera pointer input"); void IGameWindowContentEffectsAudioPublication.PublishDatCollection( IDatReaderWriter value) => PublishCompositionOwner(ref _dats, value, "DAT collection"); void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog( AcDream.App.Spells.MagicCatalog value) => PublishCompositionOwner(ref _magicCatalog, value, "magic catalog"); void IGameWindowContentEffectsAudioPublication.PublishAnimationLoader( AcDream.Core.Physics.IAnimationLoader value) => PublishCompositionOwner(ref _animLoader, value, "animation loader"); void IGameWindowContentEffectsAudioPublication.PublishLiveEntityCollisionBuilder( AcDream.App.Physics.LiveEntityCollisionBuilder value) => PublishCompositionOwner( ref _liveEntityCollisionBuilder, value, "live-entity collision builder"); void IGameWindowContentEffectsAudioPublication.PublishEmitterRegistry( AcDream.Core.Vfx.EmitterDescRegistry value) => PublishCompositionOwner(ref _emitterRegistry, value, "emitter registry"); void IGameWindowContentEffectsAudioPublication.PublishParticleSystem( AcDream.Core.Vfx.ParticleSystem value) => PublishCompositionOwner(ref _particleSystem, value, "particle system"); void IGameWindowContentEffectsAudioPublication.PublishParticleSink( AcDream.Core.Vfx.ParticleHookSink value) => PublishCompositionOwner(ref _particleSink, value, "particle hook sink"); void IGameWindowContentEffectsAudioPublication.PublishAnimationHookFrames( AcDream.App.Rendering.Vfx.AnimationHookFrameQueue value) => PublishCompositionOwner( ref _animationHookFrames, value, "animation-hook frame queue"); void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptLoader( AcDream.Content.Vfx.RetailPhysicsScriptLoader value) => PublishCompositionOwner( ref _physicsScriptLoader, value, "physics-script loader"); void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptRunner( AcDream.Core.Vfx.PhysicsScriptRunner value) => PublishCompositionOwner(ref _scriptRunner, value, "physics-script runner"); void IGameWindowContentEffectsAudioPublication.PublishLightingSink( AcDream.Core.Lighting.LightingHookSink value) => PublishCompositionOwner(ref _lightingSink, value, "lighting hook sink"); void IGameWindowContentEffectsAudioPublication.PublishTranslucencySink( AcDream.Core.Rendering.TranslucencyHookSink value) => PublishCompositionOwner( ref _translucencySink, value, "translucency hook sink"); void IGameWindowContentEffectsAudioPublication.PublishHookRegistrations( AcDream.App.Composition.AnimationHookRegistrationSet value) => PublishCompositionOwner( ref _hookRegistrations, value, "animation-hook registrations"); void IGameWindowContentEffectsAudioPublication.PublishAudio( ContentAudioGraph value) { ArgumentNullException.ThrowIfNull(value); if (_soundCache is not null || _audioEngine is not null || _entitySoundTables is not null || _audioSink is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns audio state."); } _soundCache = value.SoundCache; _audioEngine = value.Engine; _entitySoundTables = value.EntitySoundTables; _audioSink = value.HookSink; } void IGameWindowSettingsDevToolsPublication.PublishDevTools( DevToolsCompositionOwner value) { ArgumentNullException.ThrowIfNull(value); if (_devToolsComposition is not null || _devToolsFramePresenter is not null || _devToolsCommandBus is not null || _debugVm is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns developer tools."); } _devToolsComposition = value; _devToolsFramePresenter = value.Presenter; _devToolsCommandBus = value.CommandBus; _vitalsVm = value.Vitals; _debugVm = value.Debug; } void IGameWindowWorldRenderPublication.PublishBindlessSupport( BindlessSupport value) => PublishCompositionOwner( ref _bindlessSupport, value, "bindless support"); void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) => PublishCompositionOwner( ref _terrainModernShader, value, "terrain shader"); void IGameWindowWorldRenderPublication.PublishSceneLighting( SceneLightingUboBinding value) => PublishCompositionOwner( ref _sceneLightingUbo, value, "scene lighting"); void IGameWindowWorldRenderPublication.PublishDebugLines( DebugLineRenderer value) => PublishCompositionOwner(ref _debugLines, value, "debug lines"); void IGameWindowWorldRenderPublication.PublishHudResources( BitmapFont font, TextRenderer text) { ArgumentNullException.ThrowIfNull(font); ArgumentNullException.ThrowIfNull(text); if (_debugFont is not null || _textRenderer is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns world HUD resources."); } _debugFont = font; _textRenderer = text; } void IGameWindowWorldRenderPublication.PublishTerrain( TerrainModernRenderer value) => PublishCompositionOwner(ref _terrain, value, "terrain renderer"); void IGameWindowWorldRenderPublication.PublishTerrainBuildState( float[] heightTable, AcDream.Core.Terrain.TerrainBlendingContext blending, System.Collections.Concurrent.ConcurrentDictionary< uint, AcDream.Core.Terrain.SurfaceInfo> surfaceCache) { ArgumentNullException.ThrowIfNull(heightTable); ArgumentNullException.ThrowIfNull(blending); ArgumentNullException.ThrowIfNull(surfaceCache); if (_heightTable is not null || _blendCtx is not null || _surfaceCache is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns terrain build state."); } _heightTable = heightTable; _blendCtx = blending; _surfaceCache = surfaceCache; } void IGameWindowWorldRenderPublication.PublishMeshShader(Shader value) => PublishCompositionOwner(ref _meshShader, value, "mesh shader"); void IGameWindowWorldRenderPublication.PublishWbMeshAdapter( WbMeshAdapter value) => PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter"); void IGameWindowWorldRenderPublication.PublishTextureCache( TextureCache value) => PublishCompositionOwner(ref _textureCache, value, "texture cache"); void IGameWindowWorldRenderPublication.PublishSamplerCache( SamplerCache value) => PublishCompositionOwner(ref _samplerCache, value, "sampler cache"); void IGameWindowInteractionRetainedUiPublication.PublishInteractionRetainedUi( InteractionRetainedUiResult result) { ArgumentNullException.ThrowIfNull(result); if (_combatAttackController is not null || _combatTargetController is not null || _externalContainerLifecycle is not null || _itemInteractionController is not null || _interactionUiLateBindings is not null || _uiHost is not null || _retailUiRuntime is not null || _retailChatVm is not null || _characterSheetProvider is not null || _magicRuntime is not null || _frameScreenshots is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns interaction/UI state."); } _combatAttackController = result.CombatAttack; _combatTargetController = result.CombatTarget; _externalContainerLifecycle = result.ExternalContainerLifecycle; _itemInteractionController = result.ItemInteraction; _interactionUiLateBindings = result.LateBindings; if (result.RetainedUi is { } retained) { _uiHost = retained.Host; _retailUiRuntime = retained.Runtime; _vitalsVm ??= retained.Vitals; _retailChatVm = retained.Chat; _characterSheetProvider = retained.CharacterSheet; _magicRuntime = retained.Magic; _frameScreenshots = retained.Screenshots; } } void IGameWindowLivePresentationPublication.PublishLivePresentation( LivePresentationResult result) { ArgumentNullException.ThrowIfNull(result); if (_liveEntities is not null || _wbEntitySpawnAdapter is not null || _entityScriptActivator is not null || _staticAnimationScheduler is not null || _wbDrawDispatcher is not null || _retailSelectionScene is not null || _worldSelectionQuery is not null || _selectionInteractions is not null || _retainedUiGameplayBinding is not null || _equippedChildRenderer is not null || _entityEffects is not null || _liveEntityPresentation is not null || _remoteTeleportController is not null || _projectileController is not null || _liveEntityProjectionWithdrawal is not null || _liveEntityLights is not null || _liveAnimationScheduler is not null || _animationPresenter is not null || _paperdollViewportRenderer is not null || _paperdollFramePresenter is not null || _envCellRenderer is not null || _envCellFrustum is not null || _landblockPresentationPipeline is not null || _portalDepthMask is not null || _clipFrame is not null || _skyRenderer is not null || _particleRenderer is not null || _renderFrameDiagnostics is not null || _livePresentationBindings is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns live presentation state."); } _wbEntitySpawnAdapter = result.EntitySpawnAdapter; _entityScriptActivator = result.EntityScriptActivator; _staticAnimationScheduler = result.StaticAnimationScheduler; _worldState = result.WorldState; _liveEntities = result.LiveEntities; _projectileController = result.ProjectileController; _liveEntityProjectionWithdrawal = result.ProjectionWithdrawal; _liveEntityLights = result.Lights; _liveAnimationScheduler = result.AnimationScheduler; _animationPresenter = result.AnimationPresenter; _equippedChildRenderer = result.EquippedChildren; _entityEffects = result.EntityEffects; _liveEntityPresentation = result.Presentation; _remoteTeleportController = result.RemoteTeleport; _wbDrawDispatcher = result.DrawDispatcher; _retailSelectionScene = result.SelectionScene; _worldSelectionQuery = result.SelectionQuery; _selectionInteractions = result.SelectionInteractions; _retainedUiGameplayBinding = result.RetainedGameplay; _paperdollViewportRenderer = result.PaperdollRenderer; _paperdollFramePresenter = result.PaperdollPresenter; _envCellFrustum = result.EnvCellFrustum; _envCellRenderer = result.EnvCellRenderer; _landblockPresentationPipeline = result.LandblockPipeline; _clipFrame = result.ClipFrame; _portalDepthMask = result.PortalDepthMask; _skyRenderer = result.SkyRenderer; _particleRenderer = result.ParticleRenderer; _renderFrameDiagnostics = result.FrameDiagnostics; _livePresentationBindings = result.RuntimeBindings; } private static void PublishCompositionOwner( ref T? destination, T value, string name) where T : class { ArgumentNullException.ThrowIfNull(value); if (destination is not null) throw new InvalidOperationException( $"The GameWindow composition shell already owns {name}."); destination = value; } [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_gl), nameof(_input))] private GameWindowPlatformResult AcquirePlatform() { GameWindowPlatformResult platform = GameWindowPlatformAcquisition.Acquire( () => GL.GetApi(_window!), static gl => gl.Dispose(), () => _window!.CreateInput(), static input => input.Dispose(), this); if (_gl is null || _input is null) throw new InvalidOperationException( "Platform acquisition returned without publishing both owners."); return platform; } private void OnLoad() { // Task 7: wire the physics data cache into the engine so Transition can // run narrow-phase BSP tests during FindObjCollisions. _physicsEngine.DataCache = _physicsDataCache; GameWindowPlatformResult platform = AcquirePlatform(); HostInputCameraResult hostInputCamera = new HostInputCameraCompositionPhase( new HostInputCameraDependencies( _framebufferResize, _window!.FramebufferSize, _hostQuiescence, _inputCapture, _keyBindings, _movementInput, _cameraInput, _localPlayerMode, _chaseCameraInput, _pointerPosition, _renderDiagnosticLog), this).Compose(platform); WorldRenderDiagnostics worldRenderDiagnostics = hostInputCamera.WorldRenderDiagnostics; ContentEffectsAudioResult contentEffectsAudio = new ContentEffectsAudioCompositionPhase( new ContentEffectsAudioDependencies( _datDir, _physicsDataCache, _animationDiagnostics.DumpMotionEnabled, SpellBook, _hookRouter, _effectPoses, _entityEffectAdvance, Lighting, _translucencyFades, _options.NoAudio, Console.WriteLine, Console.Error.WriteLine), this).Compose(platform, hostInputCamera); SettingsDevToolsOptionalDependencies? optionalDevTools = null; if (DevToolsEnabled) { var devToolsWorldEntities = new DeferredCanonicalWorldEntityCountSource(); var devToolsFrameDiagnostics = new DeferredRenderFrameDiagnosticsSource(); var devToolsPlayerModeCommands = new DeferredDevToolsPlayerModeCommands(); var devToolsFacts = new DevToolsRuntimeFacts( _localPlayerMode, _playerControllerSlot, hostInputCamera.CameraController, devToolsWorldEntities, _animatedEntities, _debugVmRenderFacts, _physicsEngine, _worldSceneDebugState, _renderRange, hostInputCamera.CameraPointerInput, _worldEnvironment, Lighting, contentEffectsAudio.ParticleSystem, devToolsFrameDiagnostics); IRuntimeKeyBindingTarget? keyBindingTarget = hostInputCamera.InputDispatcher is { } settingsDispatcher ? new RuntimeKeyBindingTarget(settingsDispatcher) : null; optionalDevTools = new SettingsDevToolsOptionalDependencies( devToolsFacts, keyBindingTarget, devToolsWorldEntities, devToolsFrameDiagnostics, devToolsPlayerModeCommands); } SettingsDevToolsResult settingsDevTools = new SettingsDevToolsCompositionPhase( new SettingsDevToolsDependencies( _window!, _runtimeSettings, new RuntimeSettingsStartupTargets( new SilkRuntimeDisplayWindowTarget(_window!), _displayFramePacing, hostInputCamera.CameraController, contentEffectsAudio.Audio?.Engine), _hostQuiescence, Chat, Combat, LocalPlayer, optionalDevTools, _runtimeDiagnosticCommands, _combatFeedback, _keyBindings, _frameProfiler, _framebufferResize, Console.WriteLine), this).Compose(platform, hostInputCamera, contentEffectsAudio); Action? compositionToast = settingsDevTools.DevTools is { } composedDevTools ? text => composedDevTools.Debug.AddToast(text) : null; const uint initialCenterLandblockId = 0xA9B4FFFFu; WorldRenderResult worldRender = new WorldRenderCompositionPhase( new WorldRenderDependencies( _worldEnvironment, _renderResourceLifetime, _gpuFrameFlights!, initialCenterLandblockId, 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}"); InteractionRetainedUiResult interactionUi = new InteractionRetainedUiCompositionPhase( new InteractionRetainedUiDependencies( _options, platform.Graphics, _window!, platform.Input, shadersDir, contentEffectsAudio.Dats, _datLock, worldRender.Foundation.TextureCache, worldRender.Foundation.DebugFont, _hostQuiescence, _retainedInputCapture, hostInputCamera.InputDispatcher, _runtimeSettings, Combat, _combatAttackOperations, _selection, _externalContainers, Objects, contentEffectsAudio.MagicCatalog, SpellBook, Chat, LocalPlayer, ItemMana, _stackSplitQuantity, _uiRegistry, _liveCombatModeCommands, _localPlayerIdentity, _playerControllerSlot, _localPlayerMode, _characterOptions, _shortcutSnapshots, viewPlane => new SelectionCameraSource( hostInputCamera.CameraController, _window!, viewPlane), _uiFrameDiagnostics, settingsDevTools.DevTools?.Vitals, compositionToast, ClientTimerNow, Console.WriteLine), _retailUiLease, this).Compose( hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender); LivePresentationResult livePresentation = new LivePresentationCompositionPhase( new LivePresentationDependencies( _options, platform.Graphics, _window!, _datLock, _runtimeSettings, _hostQuiescence, _physicsEngine, _physicsDataCache, _worldGameState, _worldEvents, _selection, Objects, _liveEntityRuntimeSlot, _liveEntityMotionBindings, _entityEffectAdvance, _effectPoses, _remotePhysicsUpdater, _localPlayerShadow, _animatedEntities, _animationDiagnostics, _classificationCache, _translucencyFades, _retailAlphaQueue, _cellVisibility, _liveWorldOrigin, _localPlayerIdentity, _playerControllerSlot, _pointerPosition, _playerApproachCompletions, _renderResourceLifetime, _portalTunnelFallback, _hookRouter, _renderDiagnosticLog, WorldTime, settingsDevTools.DevTools?.LateBindings.WorldEntities, settingsDevTools.DevTools?.LateBindings.FrameDiagnostics, _uiFrameDiagnostics, compositionToast), this).Compose( hostInputCamera, contentEffectsAudio, 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( _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( _physicsEngine, livePresentation.LiveEntities, _localPlayerIdentity, _playerControllerSlot, _playerHostSlot, _chaseCameraInput, _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)); var teleportRenderState = new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource( _localPlayerTeleport); var renderLoginState = new AcDream.App.Rendering.RenderLoginStateSource( _options.LiveMode, _localPlayerMode); var renderFrameGlState = new AcDream.App.Rendering.RenderFrameGlStateController( new AcDream.App.Rendering.SilkRenderFrameGlStateApi(_gl!)); var renderFrameLivePreparation = new AcDream.App.Rendering.RuntimeRenderFrameLivePreparation( _textureCache, _wbMeshAdapter, _worldReveal, teleportRenderState, renderLoginState, new AcDream.App.Rendering.LiveLoginRevealCellSource( _liveEntities!, _localPlayerIdentity), _particleRenderer, _frameProfiler, _frameDiag); var renderFrameResources = new AcDream.App.Rendering.RenderFrameResourceController( _gpuFrameFlights!, new AcDream.App.Rendering.RuntimeRenderFrameBeginResources( _textureCache, _wbDrawDispatcher, _envCellRenderer, _portalDepthMask, _textRenderer, _uiHost?.TextRenderer, _clipFrame, _terrain, _sceneLightingUbo, _frameProfiler, _gl!), new AcDream.App.Rendering.RuntimeRenderFrameClearPhase( _gl!, WorldTime, Weather, teleportRenderState, _particleVisibility, worldRenderDiagnostics, renderFrameGlState), renderFrameLivePreparation); var renderWeatherFrame = new AcDream.App.Rendering.RenderWeatherFrameController( WorldTime, Weather); var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController( _scriptRunner!, _particleSink!, _effectPoses, _entityEffects); var worldFrameEnvironment = new AcDream.App.Rendering.RuntimeWorldFrameEnvironmentPreparation( _options, WorldTime, Lighting, _wbDrawDispatcher, _envCellRenderer, _sceneLightingUbo, _renderRange, skyPesFrame); var worldRenderFrameBuilder = new AcDream.App.Rendering.WorldRenderFrameBuilder( new AcDream.App.Rendering.RuntimeWorldFrameCameraSource( _cameraController!, _localPlayerTeleport), new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation( _retailSelectionScene, _particleVisibility, _terrain, _worldReveal, _envCellFrustum), new AcDream.App.Rendering.RuntimeWorldFrameSettingsPreview( _runtimeSettings, _audioEngine, _cameraController!, _displayFramePacing), new AcDream.App.Rendering.RuntimeWorldFrameRootSource( _physicsEngine, _cellVisibility, _localPlayerMode, _chaseCameraInput, _playerControllerSlot, _liveWorldOrigin), worldFrameEnvironment, new AcDream.App.Rendering.RuntimeWorldFrameAnimatedEntitySource( _animatedEntities, _staticAnimationScheduler, _equippedChildRenderer), new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource( _landblockPresentationPipeline!, _cellVisibility)); var terrainDrawDiagnostics = new AcDream.App.Rendering.TerrainDrawDiagnosticsController( _frameDiag, worldRenderDiagnostics, new AcDream.App.Rendering.RuntimeFramePipelineDiagnosticFactsSource( _terrain, _landblockPresentationPipeline, renderFrameLivePreparation, _wbDrawDispatcher, _streamingController, _liveEntities!, _worldState), _renderDiagnosticLog); var retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource( _cellVisibility); var retailPViewPassExecutor = new AcDream.App.Rendering.RetailPViewPassExecutor( _gl!, renderFrameGlState, new AcDream.App.Rendering.SilkRetailPViewFramebufferSource( _window!), _clipFrame!, _terrain, _envCellRenderer!, _wbDrawDispatcher!, _skyRenderer, _particleSystem, _particleRenderer, _portalDepthMask, _retailAlphaQueue, worldRenderDiagnostics, terrainDrawDiagnostics); var worldSceneDiagnostics = new AcDream.App.Rendering.WorldSceneDiagnosticsController( worldRenderDiagnostics, new AcDream.App.Rendering.RuntimeWorldScenePViewDiagnosticSource( _playerControllerSlot, _physicsEngine, _cellVisibility), _worldSceneDebugState, _debugLines, _physicsEngine, _localPlayerMode, _playerControllerSlot, _debugVmRenderFacts, _debugVm is not null); var worldScenePasses = new AcDream.App.Rendering.WorldScenePassExecutor( _gl!, renderFrameGlState, _clipFrame!, _wbDrawDispatcher!, _envCellRenderer!, _terrain, terrainDrawDiagnostics, _skyRenderer, _particleSystem, _particleRenderer); var worldSceneRenderer = new AcDream.App.Rendering.WorldSceneRenderer( renderFrameResources, renderLoginState, _worldEnvironment, worldRenderFrameBuilder, new AcDream.App.Rendering.RuntimeWorldSceneEntitySource(_worldState), _retailSelectionScene, _retailAlphaQueue, _particleVisibility, new AcDream.App.Rendering.WorldScenePViewRenderer( new AcDream.App.Rendering.RetailPViewRenderer(), retailPViewPassExecutor, retailPViewPassExecutor), retailPViewCells, worldScenePasses, _renderRange, worldSceneDiagnostics); if (_frameScreenshots is { } screenshots && _options.AutomationArtifactDirectory is { } artifactDirectory) { void UiProbeLog(string message) => Console.WriteLine("[UI-PROBE] " + message); _worldLifecycleAutomation = new AcDream.App.Diagnostics.WorldLifecycleAutomationController( () => _worldReveal?.Snapshot ?? default, () => _worldReveal?.PortalMaterializationCount ?? 0, CaptureWorldLifecycleResourceSnapshot, screenshots, artifactDirectory, UiProbeLog); interactionUi.LateBindings.AdoptLateOwnerBinding( "world lifecycle automation", interactionUi.LateBindings.Automation.Bind( _worldLifecycleAutomation)); } AcDream.App.Rendering.IRetainedGameplayUiFrame? retainedGameplayUi = _options.RetailUi && _retailUiRuntime is not null ? new AcDream.App.Rendering.RetainedGameplayUiFrame( _retailUiRuntime, _input) : null; AcDream.App.Rendering.IPrivateFrameScreenshot? privateScreenshot = _frameScreenshots is not null ? new AcDream.App.Rendering.PrivateFrameScreenshot( _frameScreenshots) : null; var privatePresentation = new AcDream.App.Rendering.PrivatePresentationRenderer( new AcDream.App.Rendering.LocalPlayerPortalViewport( _localPlayerTeleport!, _cameraController!), renderFrameResources, _paperdollFramePresenter, retainedGameplayUi, _devToolsFramePresenter, privateScreenshot); var framePreparation = new AcDream.App.Rendering.RenderFramePreparationController( renderFrameResources, _devToolsFramePresenter, renderWeatherFrame); var renderFrameOrchestrator = new AcDream.App.Rendering.RenderFrameOrchestrator( _gpuFrameFlights!, framePreparation, worldSceneRenderer, privatePresentation, _renderFrameDiagnostics!, (AcDream.App.Rendering.IRenderFrameFailureRecovery?) _devToolsFramePresenter ?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance); var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator( liveObjectFrame, _worldState, _liveSessionController, localPlayerFrame, liveSpatialReconciler); var cameraFrame = new AcDream.App.Rendering.CameraFrameController( _cameraController!, _inputCapture, _cameraInput, localPlayerFrameRuntime, _chaseCameraInput, localPlayerFrame, liveSpatialReconciler, new AcDream.App.Combat.CombatCameraTargetSource( _runtimeSettings, Combat, _selection, _worldSelectionQuery!)); var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator( new AcDream.App.Update.LiveEntityTeardownFramePhase( livePresentation.LiveEntities), new AcDream.App.Update.ConsoleUpdateFrameFailureSink(), _updateFrameClock, new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!), streamingFrame, _gameplayInputFrame, liveFrameCoordinator, new AcDream.App.Update.LiveEntityLivenessFramePhase( _liveEntityLiveness, new AcDream.App.Update.StopwatchClientMonotonicTimeSource()), _localPlayerTeleport, new AcDream.App.Update.PlayerModeAutoEntryFramePhase( _playerModeAutoEntry), cameraFrame); _frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator); _liveSessionHost = CreateLiveSessionHost(); AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm; Action? debugToast = debugVm is null ? null : message => debugVm.AddToast(message); AcDream.Core.Chat.ChatLog chat = Chat; var combatCommand = new AcDream.App.Combat.LiveCombatModeCommandController( new AcDream.App.Combat.LiveSessionCombatModeAuthority( _liveSessionHost), new AcDream.App.Combat.LocalPlayerCombatEquipmentSource( Objects, _localPlayerIdentity), Combat, new AcDream.App.Combat.ItemInteractionCombatModeIntentSink( _itemInteractionController!), Console.WriteLine, debugToast, text => chat.OnSystemMessage(text, 0x1Au)); _liveCombatModeCommands.Bind(combatCommand); var nearbyDiagnostics = new AcDream.App.Diagnostics.NearbyWorldDiagnosticDumper( new AcDream.App.Diagnostics.RuntimeNearbyWorldDiagnosticSource( _localPlayerMode, _playerControllerSlot, _cameraController!, _liveWorldOrigin, _worldState, _physicsEngine), Console.WriteLine); var runtimeDiagnostics = new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController( _worldEnvironment, _worldSceneDebugState, _cameraPointerInput, nearbyDiagnostics, debugToast); _runtimeDiagnosticCommands.Bind(runtimeDiagnostics); if (_inputDispatcher is { } dispatcher) { var pointer = _cameraPointerInput ?? throw new InvalidOperationException( "Gameplay action routing requires the composed pointer owner."); var commands = new AcDream.App.Input.GameplayInputCommandController( new AcDream.App.Input.RetainedGameplayWindowCommands( _retailUiRuntime), new AcDream.App.Input.DevToolsGameplayCommands( _devToolsFramePresenter), runtimeDiagnostics, new AcDream.App.Input.PlayerModeGameplayCommands( _localPlayerMode, _playerModeController), new AcDream.App.Input.ItemTargetModeCommands( _itemInteractionController!), new AcDream.App.Input.GameplayCameraModeCommands( _cameraController!), combatCommand, new AcDream.App.Input.GameplayWindowCommands(_window!.Close)); var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets( _gameplayInputFrame!, pointer, _retailUiRuntime, _selectionInteractions, commands); _gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create( dispatcher, Combat, targets, _hostQuiescence, Console.WriteLine); _gameplayInputActions.Attach(); } AcDream.App.Net.LiveSessionStartResult liveStart = _liveSessionHost.Start(_options); switch (liveStart.Status) { case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials: Console.WriteLine( "live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping"); break; case AcDream.App.Net.LiveSessionStartStatus.Failed: Console.WriteLine($"live: session failed: {liveStart.Error}"); break; } } private AcDream.App.Net.LiveSessionHost CreateLiveSessionHost() => new(_liveSessionController!, new AcDream.App.Net.LiveSessionHostBindings( Routing: new( CreateLiveSessionEventRouter, session => new AcDream.App.Net.LiveSessionCommandRouter( CreateLiveSessionCommandBindings(session))), Reset: CreateLiveSessionResetBindings(), Selection: new( SetPlayerIdentity: id => _playerServerGuid = id, SetVitalsIdentity: id => _vitalsVm?.SetLocalPlayerGuid(id), SetChatIdentity: Chat.SetLocalPlayerGuid, MarkPersistent: _worldState.MarkPersistent, SetVanishProbeIdentity: id => AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = id, ClearCombat: Combat.Clear), EnteredWorld: new( SetActiveCharacter: _runtimeSettings.SetActiveCharacter, RestoreLayout: () => _retailUiRuntime?.RestoreLayout(), SyncToolbar: SyncToolbarWindowButtons, LoadCharacterSettings: _runtimeSettings.LoadCharacterContext, ArmPlayerModeAutoEntry: () => _playerModeAutoEntry?.Arm()), Connecting: (host, port, user) => Chat.OnSystemMessage( $"connecting to {host}:{port} as {user}", chatType: 1), Connected: () => Chat.OnSystemMessage( "connected — character list received", chatType: 1))); private AcDream.App.Net.LiveSessionResetBindings CreateLiveSessionResetBindings() => new() { MouseCapture = ResetSessionMouseCapture, PlayerPresentation = ResetSessionPlayerPresentation, TeleportTransit = _localPlayerTeleportSink.ResetSession, SessionDialogs = () => _retailUiRuntime?.ResetSessionDialogs(), ChatCommandTargets = () => _retailChatVm?.ResetSessionTargets(), SettingsCharacterContext = _runtimeSettings.RestoreDefaultCharacterContext, EquippedChildren = () => _equippedChildRenderer?.Clear(), ExternalContainer = () => _externalContainers.Reset(), InteractionAndSelection = ResetSessionInteraction, SelectionPresentation = () => _retailSelectionScene?.Reset(), ObjectTable = Objects.Clear, Spellbook = SpellBook.Clear, MagicRuntime = () => _magicRuntime?.Reset(), CombatAttack = () => _combatAttackController?.ResetSession(), CombatState = Combat.Clear, ItemMana = ItemMana.Clear, LocalPlayer = LocalPlayer.Clear, Friends = Friends.Clear, Squelch = Squelch.Clear, TurbineChat = TurbineChat.Reset, ParticleVisibility = _particleVisibility.Reset, InboundEventFifo = _inboundEntityEvents.Clear, LiveLiveness = () => _liveEntityLiveness?.Clear(), LiveRuntime = ResetSessionLiveRuntime, SessionIdentity = ResetSessionIdentity, RemoteTeleport = () => _remoteTeleportController?.Clear(), NetworkEffects = () => _entityEffects?.ClearNetworkState(), AnimationHookFrames = () => _animationHookFrames?.Clear(), LivePresentation = () => _liveEntityPresentation?.Clear(), RemoteMovementDiagnostics = _remoteMovementObservations.Clear, }; private void ResetSessionMouseCapture() => _gameplayInputFrame?.ResetSession(); private void ResetSessionPlayerPresentation() { _playerModeController?.ResetSession(); _spawnClaimRangeMemo = null; } private void ResetSessionIdentity() { AcDream.App.Net.LiveSessionEntityRuntimeReset.RequireConvergence(_liveEntities); // PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired // components, and character option objects. CPlayerSystem::Begin // @ 0x0055D410 resets player identity and session fields. The option // default comes from PlayerModule::PlayerModule @ 0x005D51F0. _playerServerGuid = 0u; _vitalsVm?.SetLocalPlayerGuid(0u); Chat.ResetSessionIdentity(); AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = 0u; _runtimeSettings.ResetActiveCharacterKey(); _characterOptions.Reset(); _localPlayerSkills.ResetSession(); _liveEntityNetworkUpdates?.ResetSessionState(); _liveEntityHydration?.ResetSessionState(); Shortcuts = Array.Empty(); DesiredComponents = Array.Empty<(uint Id, uint Amount)>(); _paperdollFramePresenter?.MarkDirty(); // X/Y are ignored until the next logical player CreateObject claims a // new origin. Keeping the last values avoids inventing a synthetic // landblock while still forcing first-spawn initialization. _liveWorldOrigin.Reset(); } private void ResetSessionLiveRuntime() { AcDream.App.Net.LiveSessionEntityRuntimeReset.ClearAndRequireConvergence( _liveEntities); } private void ResetSessionInteraction() { if (_selectionInteractions is { } interactions) interactions.ResetSession(); else { _itemInteractionController?.ResetSession(); _selection.Reset(); } } private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter( AcDream.Core.Net.WorldSession session) { var skillTable = _dats?.Get(0x0E000004u); if (_characterSheetProvider is not null && _dats is not null) { _characterSheetProvider.SkillTable = skillTable; _characterSheetProvider.ExperienceTable = AcDream.App.UI.Layout.CharacterSheetProvider.LoadExperienceTable( _dats, Console.WriteLine); } return new AcDream.App.Net.LiveSessionEventRouter( session, _liveEntitySessionEvents.CreateSink(), new AcDream.App.Net.LiveEnvironmentSessionSink( _worldEnvironment.ApplyAdminEnvirons, _worldEnvironment.SynchronizeFromServer), CreateLiveInventorySessionBindings(), CreateLiveCharacterSessionBindings(skillTable), CreateLiveSocialSessionBindings()); } private AcDream.App.Net.LiveInventorySessionBindings CreateLiveInventorySessionBindings() => new( Objects, LocalPlayer, PlayerGuid: () => _playerServerGuid, OnShortcuts: list => Shortcuts = list, OnUseDone: error => { _externalContainers.ApplyUseDone(error); _itemInteractionController?.CompleteUse(error); }, ItemMana, OnDesiredComponents: components => DesiredComponents = components, ExternalContainers: _externalContainers); private AcDream.App.Net.LiveCharacterSessionBindings CreateLiveCharacterSessionBindings( DatReaderWriter.DBObjs.SkillTable? skillTable) { var skillCreditResolver = new AcDream.App.Net.LiveSkillCreditResolver(skillTable); return new( Combat, SpellBook, ResolveSkillFormulaBonus: skillCreditResolver.Resolve, OnSkillsUpdated: (runSkill, jumpSkill) => { _localPlayerSkills.Update(runSkill, jumpSkill, _playerController); if (_localPlayerSkills.IsComplete && _playerController is not null) { Console.WriteLine( $"player: applied server skills run={_localPlayerSkills.RunSkill} " + $"jump={_localPlayerSkills.JumpSkill}"); } }, OnConfirmationRequest: request => _retailUiRuntime?.HandleConfirmationRequest(request), OnConfirmationDone: done => _retailUiRuntime?.HandleConfirmationDone(done), OnCharacterOptions: (options1, _) => _characterOptions.Options = (AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1) options1, ClientTime: ClientTimerNow); } private AcDream.App.Net.LiveSocialSessionBindings CreateLiveSocialSessionBindings() => new( Chat, TurbineChat, Friends, Squelch); private AcDream.App.Net.LiveSessionCommandBindings CreateLiveSessionCommandBindings( AcDream.Core.Net.WorldSession session) => new( ClientCommands: new AcDream.App.UI.ClientCommandController.Bindings( TeleportToLifestone: session.SendTeleportToLifestone, TeleportToMarketplace: session.SendTeleportToMarketplace, TeleportToPkArena: session.SendTeleportToPkArena, TeleportToPkLiteArena: session.SendTeleportToPkLiteArena, TeleportToHouse: session.SendTeleportToHouse, TeleportToMansion: session.SendTeleportToMansion, QueryAge: session.SendQueryAge, QueryBirth: session.SendQueryBirth, ToggleFrameRate: _runtimeSettings.ToggleFrameRate, ToggleUiLock: () => _runtimeSettings.SetUiLocked(!_runtimeSettings.Gameplay.LockUI), ShowSystemMessage: text => Chat.OnSystemMessage(text, 0u), ShowWeenieError: code => Chat.OnWeenieError(code, null), PlayerPublicWeenieBitfield: () => Objects.Get(_playerServerGuid)?.PublicWeenieBitfield, ClientVersion: () => typeof(GameWindow).Assembly.GetName().Version?.ToString(3) ?? "unknown", CurrentPosition: () => _playerController?.CellPosition, LastOutsideCorpsePosition: () => LocalPlayer.GetPosition(0x0Eu), ShowConfirmation: (message, completed) => _retailUiRuntime?.ShowConfirmation(message, completed), Suicide: session.SendSuicide, ClearChat: _ => Chat.Clear(), SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name), LoadUi: name => _retailUiRuntime?.RestoreNamedLayout(name), SaveAutoUi: () => _retailUiRuntime?.SaveLayout(), LoadAutoUi: () => _retailUiRuntime?.RestoreLayout(), IsAway: () => Objects.Get(_playerServerGuid)?.Properties.GetBool(0x6Eu) == true, SetAway: away => SetRetailAway(session, away), SetAwayMessage: session.SendSetAfkMessage, AcceptLootPermits: () => _runtimeSettings.Gameplay.AcceptLootPermits, SetAcceptLootPermits: _runtimeSettings.SetAcceptLootPermits, DisplayConsent: session.SendDisplayConsent, ClearConsent: session.SendClearConsent, RemoveConsent: session.SendRemoveConsent, SendEmote: session.SendEmote, Friends, AddFriend: session.SendAddFriend, RemoveFriend: session.SendRemoveFriend, ClearFriends: session.SendClearFriends, RequestLegacyFriends: session.SendLegacyFriendsListRequest, Squelch, ModifyCharacterSquelch: session.SendModifyCharacterSquelch, ModifyAccountSquelch: session.SendModifyAccountSquelch, ModifyGlobalSquelch: session.SendModifyGlobalSquelch, LastTeller: () => _retailChatVm?.LastIncomingTellSender, ClearDesiredComponents: () => { session.SendClearDesiredComponents(); DesiredComponents = Array.Empty<(uint Id, uint Amount)>(); }, HasOpenVendor: () => false, FillComponentBuyList: (_, _) => { }), Chat, TurbineChat, PlayerGuid: () => _playerServerGuid, SendTalk: session.SendTalk, SendTell: session.SendTell, SendChannel: session.SendChannel, SendTurbineChat: (roomId, chatType, dispatchType, senderGuid, text, cookie) => session.SendTurbineChatTo( roomId, chatType, dispatchType, senderGuid, text, cookie), Log: Console.WriteLine); private void OnUpdate(double dt) { using var _updStage = _frameProfiler.BeginStage( AcDream.App.Diagnostics.FrameStage.Update); _frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt)); } // Performance overlay state — updated every ~0.5s and written to the // window title so there's zero rendering cost (no font/overlay needed). private void OnRender(double deltaSeconds) { Vector2D size = _window!.Size; _frameGraphs.Render( new AcDream.App.Rendering.RenderFrameInput( deltaSeconds, size.X, size.Y), out _); } private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot CaptureWorldLifecycleResourceSnapshot() { var meshManager = _wbMeshAdapter?.MeshManager; var mesh = meshManager?.Diagnostics ?? default; AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot render = _renderFrameDiagnostics?.Snapshot ?? AcDream.App.Rendering.RenderFrameDiagnosticsSnapshot.Initial; GCMemoryInfo memory = GC.GetGCMemoryInfo(); return new AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot( LoadedLandblocks: _worldState.LoadedLandblockIds.Count, WorldEntities: _worldState.Entities.Count, AnimatedEntities: _animatedEntities.Count, VisibleLandblocks: render.VisibleLandblocks, TotalLandblocks: render.TotalLandblocks, LiveEntities: _liveEntities?.Count ?? 0, MaterializedLiveEntities: _liveEntities?.MaterializedCount ?? 0, PendingLiveTeardowns: _liveEntities?.PendingTeardownCount ?? 0, PendingLandblockRetirements: _streamingController?.PendingRetirementCount ?? 0, ParticleEmitters: _particleSystem?.ActiveEmitterCount ?? 0, Particles: _particleSystem?.ActiveParticleCount ?? 0, ParticleBindings: _particleSink?.ActiveBindingCount ?? 0, ParticleOwners: _particleSink?.TrackedOwnerCount ?? 0, EffectOwners: _entityEffects?.ReadyOwnerCount ?? 0, LightOwners: _liveEntityLights?.TrackedOwnerCount ?? 0, ScriptOwners: _scriptRunner?.ActiveOwnerCount ?? 0, ActiveScripts: _scriptRunner?.ActiveScriptCount ?? 0, MeshRenderData: mesh.RenderData, MeshAtlasArrays: mesh.AtlasArrays, MeshEstimatedBytes: mesh.EstimatedBytes, StagedMeshUploads: _wbMeshAdapter?.StagedUploadBacklog ?? 0, StagedMeshBytes: _wbMeshAdapter?.StagedUploadBytes ?? 0, TrackedGpuBytes: AcDream.App.Rendering.Wb.GpuMemoryTracker.AllocatedBytes, TrackedGpuBuffers: AcDream.App.Rendering.Wb.GpuMemoryTracker.BufferCount, TrackedGpuTextures: AcDream.App.Rendering.Wb.GpuMemoryTracker.TextureCount, OwnedCompositeTextures: _textureCache?.OwnedBindlessTextureCount ?? 0, CompositeTextureOwners: _textureCache?.TextureOwnerCount ?? 0, ActiveParticleTextures: _textureCache?.ActiveParticleTextureCount ?? 0, ParticleTextureOwners: _textureCache?.ParticleTextureOwnerCount ?? 0, CompositeWarmupPending: _wbDrawDispatcher?.LastCompositeWarmupPendingCount ?? 0, ManagedBytes: GC.GetTotalMemory(forceFullCollection: false), ManagedCommittedBytes: memory.TotalCommittedBytes, Fps: render.Fps, FrameMilliseconds: render.FrameMilliseconds, LastFrameProfile: _frameProfiler.LastReport); } // IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass // narrowing that dropped settled-open doors / faded-out walls back onto the // stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the // animatedIds build site — every Sequencer entity is now added // unconditionally, which is the known-good pre-optimization behavior. // R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is // driven through the SAME MotionTableDispatchSink/DefaultSink funnel // remotes use (edge-driven DoMotion/StopMotion/set_hold_run in // PlayerMovementController; airborne-Falling falls out of // contact_allows_move + apply_current_movement). The #45 sidestep // 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it — // EXPECTED-DIFF: local sidestep pacing now matches how remotes have // always played (w6-cutover-map.md R3). private void SyncToolbarWindowButtons() { _retailUiRuntime?.SyncToolbarWindowButtons(); } private void SetRetailAway(AcDream.Core.Net.WorldSession session, bool away) { session.SendSetAfkMode(away); } /// /// L.0 Display tab: framebuffer-resize handler — update GL viewport /// + camera aspect when the window is resized (by the user dragging /// the corner OR by the runtime display target applying a saved /// Resolution). Without this, the viewport stays pinned at the /// startup size, producing a small render inside a big window. /// Also force-resets ImGui panel layout so panels that were /// previously off the new viewport snap back to default positions. /// private void OnFramebufferResize(Silk.NET.Maths.Vector2D 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( (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(); CompleteShutdown(); } private void CompleteShutdown() { _hostQuiescence.StopAccepting(); _shutdown ??= CreateShutdownTransaction(); try { _shutdown.CompleteOrThrow(); } catch (Exception error) { // The session/network stage runs first, so ACE is disconnected // gracefully even if a persistent driver failure prevents full GL // convergence. Later dependencies remain intact and the native // context/process teardown is the final safety net. Console.Error.WriteLine($"[shutdown] {error}"); } } private ResourceShutdownTransaction CreateShutdownTransaction() => new( // Logical cutoff precedes the live session's potentially long graceful // close. Physical event removal follows session retirement, so a bad // Silk accessor cannot prevent F653/transport teardown. The input // context and UI owners remain alive for reset, while copied callbacks // are already inert. new ResourceShutdownStage("input callback deactivation", [ new("combat command slot", _liveCombatModeCommands.Deactivate), new("diagnostic command slot", _runtimeDiagnosticCommands.Deactivate), new("retained gameplay", () => _retainedUiGameplayBinding?.Deactivate()), new("gameplay actions", () => _gameplayInputActions?.Deactivate()), new("camera pointer", () => _cameraPointerInput?.Deactivate()), new("dispatcher", () => _inputDispatcher?.Deactivate()), new("mouse source", () => _mouseSource?.Deactivate()), new("keyboard source", () => _kbSource?.Deactivate()), new("retained UI input", _retailUiLease.QuiesceInput), new("developer tools input", () => _devToolsComposition?.DeactivateInput()), ]), // Live-session reset retires the complete streaming window. Every // reset callback owner, especially LandblockStreamer, must remain live // until that transaction converges. new ResourceShutdownStage("session lifetime", [ new("live session", () => { AcDream.App.Net.LiveSessionController? controller = _liveSessionController; if (controller is null) return; controller.Dispose(); if (!controller.IsDisposalComplete) { throw new InvalidOperationException( "Live-session disposal was deferred by a reentrant lifecycle callback."); } if (ReferenceEquals(_liveSessionController, controller)) { _liveSessionHost = null; _liveSessionController = null; } }), ]), new ResourceShutdownStage("input callback detach", [ new("settings view model", () => _runtimeSettings.UnbindViewModel()), new("retained gameplay", () => { AcDream.App.Input.RetainedUiGameplayBinding? binding = _retainedUiGameplayBinding; if (binding is null) return; binding.Dispose(); if (!binding.IsDisposalComplete) throw new InvalidOperationException( "Retained gameplay callback removal remains pending."); _retainedUiGameplayBinding = null; }), new("gameplay actions", () => { AcDream.App.Input.GameplayInputActionRouter? actions = _gameplayInputActions; if (actions is null) return; actions.Dispose(); if (!actions.IsDisposalComplete) throw new InvalidOperationException( "Gameplay action callback removal remains pending."); _gameplayInputActions = null; }), new("retained UI input", _retailUiLease.DeactivateInput), new("developer tools input", () => _devToolsComposition?.DetachInput()), new("camera pointer", () => { AcDream.App.Input.CameraPointerInputController? pointer = _cameraPointerInput; if (pointer is null) return; pointer.Dispose(); if (!pointer.IsDisposalComplete) throw new InvalidOperationException( "Camera pointer callback removal remains pending."); }), new("dispatcher", () => { AcDream.UI.Abstractions.Input.InputDispatcher? dispatcher = _inputDispatcher; if (dispatcher is null) return; _movementInput.Unbind(dispatcher); _cameraInput.Unbind(dispatcher); dispatcher.Dispose(); if (!dispatcher.IsDisposalComplete) throw new InvalidOperationException( "Input dispatcher source removal remains pending."); _inputDispatcher = null; }), new("mouse source", () => { AcDream.App.Input.SilkMouseSource? source = _mouseSource; if (source is null) return; source.Dispose(); if (!source.IsDisposalComplete) throw new InvalidOperationException( "Mouse source callback removal remains pending."); _mouseSource = null; }), new("keyboard source", () => { AcDream.App.Input.SilkKeyboardSource? source = _kbSource; if (source is null) return; source.Dispose(); if (!source.IsDisposalComplete) throw new InvalidOperationException( "Keyboard source callback removal remains pending."); _kbSource = null; }), ]), // Frame composition borrows equipped, effect, audio, and render // owners. Withdraw the graph before the first borrowed owner closes; // 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 ResourceShutdownStage("session dependents", [ new("interaction/UI late bindings", () => { InteractionUiLateBindings? bindings = _interactionUiLateBindings; if (bindings is null) return; bindings.Dispose(); _interactionUiLateBindings = null; }), new("mouse capture", () => { AcDream.App.Input.CameraPointerInputController? pointer = _cameraPointerInput; if (pointer is null) return; pointer.ReleaseMouseLookAfterSessionRetirement(); if (_gameplayInputFrame is { } gameplayFrame) pointer.UnbindGameplayFrame(gameplayFrame); _cameraPointerInput = null; }), new("retail UI", () => { _retailUiLease.Dispose(); if (!_retailUiLease.IsDisposalComplete) throw new InvalidOperationException( "The retained UI ownership lease did not complete disposal."); _retailUiRuntime = null; _uiHost = null; }), new("combat target", () => { _combatTargetController?.Dispose(); _combatTargetController = null; }), new("combat attack", () => { _combatAttackController?.Dispose(); _combatAttackController = null; }), new("item interaction", () => { _itemInteractionController?.Dispose(); _itemInteractionController = null; }), new("external containers", () => { _externalContainerLifecycle?.Dispose(); _externalContainerLifecycle = null; }), new("magic runtime", () => { _magicRuntime = null; _magicCatalog = null; }), new("streamer", () => _streamer?.Dispose()), new("equipped children", () => _equippedChildRenderer?.Dispose()), ]), // Retained tombstones are retried while every callback owner is alive. new ResourceShutdownStage("live entities", [ new("live entity runtime", () => _liveEntities?.Clear()), ]), new ResourceShutdownStage("effect dispatch edges", [ new("live-presentation bindings", () => { LivePresentationRuntimeBindings? bindings = _livePresentationBindings; if (bindings is null) return; bindings.Dispose(); _livePresentationBindings = null; }), new("entity-effect advance source", () => { if (_entityEffects is { } effects) _entityEffectAdvance.Unbind(effects); _entityEffectAdvance.Deactivate(); }), new("animation-hook registrations", () => { AnimationHookRegistrationSet? registrations = _hookRegistrations; if (registrations is null) return; registrations.Dispose(); if (!registrations.IsCleanupComplete) { throw new InvalidOperationException( "Animation-hook registration cleanup remains pending."); } _hookRegistrations = null; }), ]), new ResourceShutdownStage("live entity dependents", [ new("live lights", () => { _liveEntityLights?.Dispose(); _liveEntityLights = null; }), new("live presentation", () => _liveEntityPresentation?.Dispose()), new("remote teleport", () => { _remoteTeleportController?.Dispose(); _remoteTeleportController = null; }), new("effect network state", () => { _entityEffects?.ClearNetworkState(); _animationHookFrames?.Clear(); _effectPoses.Clear(); }), new("audio", () => { AcDream.App.Audio.OpenAlAudioEngine? engine = _audioEngine; if (engine is null) return; engine.Dispose(); if (!engine.IsDisposalComplete) { throw new InvalidOperationException( "OpenAL native-resource cleanup remains pending."); } _audioEngine = null; _audioSink = null; _soundCache = null; _entitySoundTables = null; }), ]), new ResourceShutdownStage("submitted GPU work", [ new("frame flight drain", () => _gpuFrameFlights?.WaitForSubmittedWork()), ]), new ResourceShutdownStage("render frontends", [ new("developer tools", () => { DevToolsCompositionOwner? owner = _devToolsComposition; if (owner is null) return; owner.DisposeFrontend(); if (!owner.IsDisposalComplete) throw new InvalidOperationException( "Developer-tools cleanup remains incomplete."); _devToolsComposition = null; _devToolsFramePresenter = null; _devToolsCommandBus = null; _debugVm = null; }), new("portal tunnel", () => { _localPlayerTeleport?.Dispose(); _localPlayerTeleport = null; _portalTunnelFallback.ReleaseFallback(); }), new("paperdoll viewport", () => { _paperdollViewportRenderer?.Dispose(); _paperdollViewportRenderer = null; _paperdollFramePresenter = null; }), new("mesh draw dispatcher", () => _wbDrawDispatcher?.Dispose()), new("environment cells", () => _envCellRenderer?.Dispose()), new("portal depth mask", () => _portalDepthMask?.Dispose()), new("clip frame", () => _clipFrame?.Dispose()), new("sky", () => _skyRenderer?.Dispose()), new("particles", () => _particleRenderer?.Dispose()), ]), new ResourceShutdownStage("shared texture owners", [ new("sampler cache", () => _samplerCache?.Dispose()), new("texture cache", () => _textureCache?.Dispose()), ]), new ResourceShutdownStage("mesh adapter", [ new("WB mesh adapter", () => { _wbMeshAdapter?.Dispose(); _wbMeshAdapter = null; }), ]), new ResourceShutdownStage("remaining render owners", [ new("mesh shader", () => _meshShader?.Dispose()), new("terrain", () => { _terrain?.Dispose(); _terrain = null; }), new("terrain shader", () => _terrainModernShader?.Dispose()), new("scene lighting", () => _sceneLightingUbo?.Dispose()), new("debug lines", () => _debugLines?.Dispose()), new("text renderer", () => _textRenderer?.Dispose()), new("debug font", () => _debugFont?.Dispose()), new("frame pacing", _displayFramePacing.Dispose), new("frame profiler", _frameProfiler.Dispose), ]), new ResourceShutdownStage("dedicated render resources", [ new("sky shader", _renderResourceLifetime.ReleaseSkyShader), new("terrain atlas", _renderResourceLifetime.ReleaseTerrainAtlas), ]), new ResourceShutdownStage("failed render construction cleanup", [ new("GL construction ledger", _glConstructionCleanup.Dispose), ]), new ResourceShutdownStage("frame flight owner", [ new("frame flights", () => { _gpuFrameFlights?.Dispose(); _gpuFrameFlights = null; }), ]), new ResourceShutdownStage("content mappings", [ new("DAT collection", () => { _dats?.Dispose(); _dats = null; }), ]), new ResourceShutdownStage("input", [ new("native window callbacks", () => { SilkWindowCallbackBinding? binding = _windowCallbacks; if (binding is null) return; binding.Dispose(); if (!binding.IsDisposalComplete) throw new NativeWindowCallbackCleanupDeferredException(); if (ReferenceEquals(_windowCallbacks, binding)) _windowCallbacks = null; }), new("input context", () => { _input?.Dispose(); _input = null; }), ]), new ResourceShutdownStage("OpenGL context", [ new("OpenGL", () => { _gl?.Dispose(); _gl = null; }), ])); private void OnFocusChanged(bool focused) => _cameraPointerInput?.HandleFocusChanged(focused); public void Dispose() { // Closing is the normal path and runs while the render context is // current. This direct call also covers a constructed-but-never-run // window and exceptions during Window.Create/Run, so owned kernel // handles (including the frame timer) never depend on a native window // event for disposal. CompleteShutdown(); _window?.Dispose(); _window = null; } }