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, IGameWindowSessionPlayerPublication, IGameWindowFrameRootPublication { 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 SessionPlayerRuntimeBindings? _sessionPlayerBindings; private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots; private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation; private FrameRootRuntimeBindings? _frameRootBindings; private IDisposable? _frameGraphPublication; 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 AcDream.App.Streaming.DatSpawnClaimHydrationClassifier? _spawnClaimHydration; private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink _localPlayerTeleportSink = new(); private AcDream.App.Streaming.LocalPlayerTeleportController? _localPlayerTeleport; private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange = new(nearRadius: 4, farRadius: 12); // 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; 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(); private readonly DesiredComponentSnapshotState _desiredComponents = new(); public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents => _desiredComponents.Items; // 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; } void IGameWindowSessionPlayerPublication.PublishSessionPlayer( SessionPlayerResult result) { ArgumentNullException.ThrowIfNull(result); if (_streamer is not null || _streamingController is not null || _streamingOriginRecenter is not null || _worldReveal is not null || _spawnClaimHydration is not null || _liveSessionController is not null || _liveEntityHydration is not null || _liveEntityNetworkUpdates is not null || _liveEntityLiveness is not null || _liveEntitySessionEvents is not null || _gameplayInputFrame is not null || _localPlayerAnimation is not null || _localPlayerShadowSynchronizer is not null || _playerModeController is not null || _playerModeAutoEntry is not null || _localPlayerTeleport is not null || _liveSessionHost is not null || _gameplayInputActions is not null || _sessionPlayerBindings is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns session/player state."); } _streamer = result.Streamer; _streamingController = result.Streaming; _streamingOriginRecenter = result.StreamingOriginRecenter; _worldReveal = result.WorldReveal; _spawnClaimHydration = result.SpawnClaimHydration; _liveSessionController = result.LiveSession; _liveEntityHydration = result.Hydration; _liveEntityNetworkUpdates = result.NetworkUpdates; _liveEntityLiveness = result.Liveness; _liveEntitySessionEvents = result.SessionEvents; _gameplayInputFrame = result.GameplayInput; _localPlayerAnimation = result.LocalPlayerAnimation; _localPlayerShadowSynchronizer = result.LocalPlayerShadow; _playerModeController = result.PlayerMode; _playerModeAutoEntry = result.PlayerModeAutoEntry; _localPlayerTeleport = result.LocalTeleport; _liveSessionHost = result.SessionHost; _gameplayInputActions = result.GameplayActions; _sessionPlayerBindings = result.RuntimeBindings; } void IGameWindowFrameRootPublication.PublishFrameRoots( FrameRootResult result) { ArgumentNullException.ThrowIfNull(result); if (_worldLifecycleAutomation is not null || _frameRootBindings is not null || _frameGraphPublication is not null) { throw new InvalidOperationException( "The GameWindow composition shell already owns frame roots."); } _worldLifecycleAutomation = result.LifecycleAutomation; _frameRootBindings = result.RuntimeBindings; _frameGraphPublication = result.FrameGraphPublication; } 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; 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); SessionPlayerResult sessionPlayer = new SessionPlayerCompositionPhase( new SessionPlayerDependencies( _options, _window!, _datLock, _runtimeSettings, settingsDevTools, _worldEnvironment, _worldSceneDebugState, _runtimeDiagnosticCommands, _liveCombatModeCommands, _hostQuiescence, _physicsEngine, _physicsDataCache, _worldGameState, _worldEvents, _selection, Objects, _classificationCache, _liveEntityRuntimeSlot, _animatedEntities, _remoteMovementObservations, _remotePhysicsUpdater, _remoteInboundMotion, _inboundEntityEvents, _liveEntityMotionBindings, _localPlayerIdentity, _playerControllerSlot, _playerHostSlot, _localPlayerMode, _chaseCameraInput, _localPlayerOutbound, _localPlayerTeleportSink, _liveWorldOrigin, _renderRange, _localPlayerShadow, _localPlayerSkills, _viewportAspect, _playerApproachCompletions, _characterOptions, _shortcutSnapshots, _desiredComponents, _pointerPosition, _movementInput, _inputCapture, _particleVisibility, _translucencyFades, _effectPoses, _updateFrameClock, _movementTruthDiagnostics, _combatAttackOperations, Combat, _combatFeedback, Chat, LocalPlayer, SpellBook, ItemMana, Friends, Squelch, TurbineChat, _externalContainers, _portalTunnelFallback, Console.WriteLine), this).Compose( hostInputCamera, contentEffectsAudio, worldRender, interactionUi, livePresentation); FrameRootResult frameRoots = new FrameRootCompositionPhase( new FrameRootDependencies( _options, _gl!, _window!, _input!, WorldTime, Weather, Lighting, _worldEnvironment, _physicsEngine, _cellVisibility, _localPlayerMode, _localPlayerIdentity, _chaseCameraInput, _playerControllerSlot, _liveWorldOrigin, _particleVisibility, _effectPoses, _renderRange, _runtimeSettings, _displayFramePacing, _worldSceneDebugState, _retailAlphaQueue, _frameProfiler, _frameDiag, _renderDiagnosticLog, _debugVmRenderFacts, _inputCapture, _cameraInput, _selection, _animatedEntities, _updateFrameClock, Combat, _frameGraphs, Console.WriteLine), this).Compose( hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender, interactionUi, livePresentation, sessionPlayer); new SessionStartCompositionPhase( new SessionStartDependencies(_options, Console.WriteLine)) .Start(frameRoots); } 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 _); } // 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). /// /// 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); 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("world frame composition", () => { IDisposable? publication = _frameGraphPublication; if (publication is null) return; publication.Dispose(); _frameGraphPublication = null; }), new("frame-root bindings", () => { FrameRootRuntimeBindings? bindings = _frameRootBindings; if (bindings is null) return; bindings.Dispose(); _frameRootBindings = null; _worldLifecycleAutomation = null; }), new("session/player bindings", () => { SessionPlayerRuntimeBindings? bindings = _sessionPlayerBindings; if (bindings is null) return; bindings.Dispose(); _sessionPlayerBindings = null; }), ]), 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(); _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; } }