Move the exact retryable shutdown manifest, typed root snapshot, terminal reporting, and native-window-last release out of GameWindow. Keep session and GPU convergence as hard barriers while reporting persistent physical callback cleanup without stranding dependent owners. Co-authored-by: Codex <codex@openai.com>
1625 lines
77 KiB
C#
1625 lines
77 KiB
C#
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<GL, IInputContext>,
|
|
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;
|
|
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
|
|
/// <see cref="_terrain"/> at draw time but allocated + disposed here.</summary>
|
|
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;
|
|
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
|
/// after <c>OnLoad</c> completes (modern path is mandatory as of N.5).</summary>
|
|
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;
|
|
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
|
/// support. Required at startup — missing bindless throws
|
|
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
|
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 readonly GameWindowLifetime _lifetime = new();
|
|
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<T> 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<T> 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<uint, SurfaceInfo>. ConcurrentDictionary so the off-thread
|
|
// mesh builder (A.5 T11+) can call LandblockMesh.Build without a lock.
|
|
private System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.Core.Terrain.SurfaceInfo>? _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;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="DatReaderWriter.Types.AnimationFrame"/>.
|
|
/// Static decorations and entities with no motion table never
|
|
/// appear in this map.
|
|
/// </summary>
|
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
|
|
private readonly AcDream.App.World.LiveEntityRuntimeSlot _liveEntityRuntimeSlot = new();
|
|
|
|
/// <summary>
|
|
/// Tier 1 cache (#53): per-entity classification results for static
|
|
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
|
|
/// paired with <see cref="_animatedEntities"/> — that dictionary is the
|
|
/// gating predicate, this cache is the lookup that depends on it.
|
|
/// Passed to <see cref="AcDream.App.Rendering.Wb.WbDrawDispatcher"/> at
|
|
/// construction time. Tasks 9-10 of the cache plan wire the per-entity
|
|
/// miss-populate / hit-fast-path through the dispatcher's loop.
|
|
/// </summary>
|
|
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();
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <c>InterpolationManager::adjust_offset</c> every object tick.
|
|
///
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </summary>
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="LiveEntityHydrationController.OnCreate"/>, consulted at the
|
|
/// top of <see cref="LiveEntityNetworkUpdateController.OnMotion"/>, and dropped by the live
|
|
/// entity teardown owner.
|
|
/// </summary>
|
|
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();
|
|
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
|
private readonly ShortcutSnapshotState _shortcutSnapshots = new();
|
|
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> 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.
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static readonly IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> EmptyLiveEntityMap =
|
|
new Dictionary<uint, AcDream.Core.World.WorldEntity>();
|
|
private static readonly IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> EmptyLiveSpawnMap =
|
|
new Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn>();
|
|
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid =>
|
|
_liveEntities?.MaterializedWorldEntities ?? EmptyLiveEntityMap;
|
|
/// <summary>Visible-only view for radar, picking, status, and targets.</summary>
|
|
private IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity> _visibleEntitiesByServerGuid =>
|
|
_liveEntities?.WorldEntities ?? EmptyLiveEntityMap;
|
|
/// <summary>
|
|
/// Latest <see cref="AcDream.Core.Net.WorldSession.EntitySpawn"/> 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.
|
|
/// <see cref="LiveEntityHydrationController.OnAppearance"/> reuses the cached
|
|
/// position/setup/motion fields when a 0xF625 ObjDescEvent carries only
|
|
/// updated visuals.
|
|
/// </summary>
|
|
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> 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<LiveEntityAnimationState>(
|
|
_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<int>(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);
|
|
_lifetime.PublishNativeWindow(_window);
|
|
_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<GL, IInputContext>.PublishGraphics(
|
|
GL graphics) =>
|
|
PublishCompositionOwner(ref _gl, graphics, "graphics API");
|
|
|
|
void IGameWindowPlatformPublication<GL, IInputContext>.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<T>(
|
|
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<GL, IInputContext> AcquirePlatform()
|
|
{
|
|
GameWindowPlatformResult<GL, IInputContext> 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<GL, IInputContext> platform = AcquirePlatform();
|
|
|
|
GameWindowCompositionPipeline.Run<
|
|
GameWindowPlatformResult<GL, IInputContext>,
|
|
HostInputCameraResult,
|
|
ContentEffectsAudioResult,
|
|
SettingsDevToolsResult,
|
|
WorldRenderResult,
|
|
InteractionRetainedUiResult,
|
|
LivePresentationResult,
|
|
SessionPlayerResult,
|
|
FrameRootResult>(
|
|
platform,
|
|
platformResult => new HostInputCameraCompositionPhase(
|
|
new HostInputCameraDependencies(
|
|
_framebufferResize,
|
|
_window!.FramebufferSize,
|
|
_hostQuiescence,
|
|
_inputCapture,
|
|
_keyBindings,
|
|
_movementInput,
|
|
_cameraInput,
|
|
_localPlayerMode,
|
|
_chaseCameraInput,
|
|
_pointerPosition,
|
|
_renderDiagnosticLog),
|
|
this).Compose(platformResult),
|
|
(platformResult, hostInputCamera) =>
|
|
new ContentEffectsAudioCompositionPhase(
|
|
new ContentEffectsAudioDependencies(
|
|
_datDir,
|
|
_physicsDataCache,
|
|
_animationDiagnostics.DumpMotionEnabled,
|
|
SpellBook,
|
|
_hookRouter,
|
|
_effectPoses,
|
|
_entityEffectAdvance,
|
|
Lighting,
|
|
_translucencyFades,
|
|
_options.NoAudio,
|
|
Console.WriteLine,
|
|
Console.Error.WriteLine),
|
|
this).Compose(platformResult, hostInputCamera),
|
|
(platformResult, hostInputCamera, contentEffectsAudio) =>
|
|
{
|
|
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);
|
|
}
|
|
|
|
return 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(platformResult, hostInputCamera, contentEffectsAudio);
|
|
},
|
|
(platformResult, contentEffectsAudio, settingsDevTools) =>
|
|
{
|
|
const uint initialCenterLandblockId = 0xA9B4FFFFu;
|
|
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
|
|
new WorldRenderDependencies(
|
|
_worldEnvironment,
|
|
_renderResourceLifetime,
|
|
_gpuFrameFlights!,
|
|
initialCenterLandblockId,
|
|
Console.WriteLine),
|
|
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
|
Console.WriteLine(
|
|
$"loading world view centered on " +
|
|
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
|
return worldRender;
|
|
},
|
|
(platformResult, hostInputCamera, contentEffectsAudio, settingsDevTools, worldRender) =>
|
|
{
|
|
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
|
|
? text => devTools.Debug.AddToast(text)
|
|
: null;
|
|
return new InteractionRetainedUiCompositionPhase(
|
|
new InteractionRetainedUiDependencies(
|
|
_options,
|
|
platformResult.Graphics,
|
|
_window!,
|
|
platformResult.Input,
|
|
worldRender.Foundation.ShadersDirectory,
|
|
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(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender);
|
|
},
|
|
(platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi) =>
|
|
{
|
|
Action<string>? compositionToast = settingsDevTools.DevTools is { } devTools
|
|
? text => devTools.Debug.AddToast(text)
|
|
: null;
|
|
return new LivePresentationCompositionPhase(
|
|
new LivePresentationDependencies(
|
|
_options,
|
|
platformResult.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(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi);
|
|
},
|
|
(hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation) =>
|
|
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,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation),
|
|
(platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation,
|
|
sessionPlayer) => new FrameRootCompositionPhase(
|
|
new FrameRootDependencies(
|
|
_options,
|
|
platformResult.Graphics,
|
|
_window!,
|
|
platformResult.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(
|
|
platformResult,
|
|
hostInputCamera,
|
|
contentEffectsAudio,
|
|
settingsDevTools,
|
|
worldRender,
|
|
interactionUi,
|
|
livePresentation,
|
|
sessionPlayer),
|
|
frameRoots => 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<int> 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).
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void OnFramebufferResize(Silk.NET.Maths.Vector2D<int> newSize)
|
|
=> _framebufferResize.Resize(newSize);
|
|
|
|
private void OnClosing() => CompleteShutdown(releaseNativeWindow: false);
|
|
|
|
private void CompleteShutdown(bool releaseNativeWindow)
|
|
{
|
|
if (!_lifetime.HasShutdownRoots)
|
|
_lifetime.PublishShutdownRoots(CaptureShutdownRoots());
|
|
|
|
GameWindowLifetimeReport report = releaseNativeWindow
|
|
? _lifetime.CompleteAndReleaseNativeWindow()
|
|
: _lifetime.TryComplete();
|
|
if (report.Status == GameWindowLifetimeStatus.Complete)
|
|
return;
|
|
|
|
Console.Error.WriteLine(
|
|
$"[shutdown] status={report.Status}, blocked={report.BlockedStage ?? "none"}");
|
|
foreach (ResourceShutdownCleanupFailure cleanup in report.CleanupFailures)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[shutdown] cleanup '{cleanup.Operation}' in '{cleanup.Stage}': " +
|
|
cleanup.Error);
|
|
}
|
|
|
|
if (report.Error is not null)
|
|
Console.Error.WriteLine($"[shutdown] {report.Error}");
|
|
}
|
|
|
|
private GameWindowShutdownRoots CaptureShutdownRoots() => new(
|
|
new IngressShutdownRoots(
|
|
_hostQuiescence,
|
|
_liveCombatModeCommands,
|
|
_runtimeDiagnosticCommands,
|
|
_retainedUiGameplayBinding,
|
|
_gameplayInputActions,
|
|
_cameraPointerInput,
|
|
_inputDispatcher,
|
|
_mouseSource,
|
|
_kbSource,
|
|
_retailUiLease,
|
|
_uiHost,
|
|
_devToolsComposition,
|
|
_liveSessionController,
|
|
_runtimeSettings,
|
|
_movementInput,
|
|
_cameraInput,
|
|
_windowCallbacks),
|
|
new FrameShutdownRoots(
|
|
_frameGraphPublication,
|
|
_frameRootBindings,
|
|
_sessionPlayerBindings,
|
|
_interactionUiLateBindings),
|
|
new LiveShutdownRoots(
|
|
_cameraPointerInput,
|
|
_retailUiLease,
|
|
_combatTargetController,
|
|
_combatAttackController,
|
|
_itemInteractionController,
|
|
_externalContainerLifecycle,
|
|
_streamer,
|
|
_equippedChildRenderer,
|
|
_liveEntities,
|
|
_livePresentationBindings,
|
|
_entityEffectAdvance,
|
|
_entityEffects,
|
|
_hookRegistrations,
|
|
_liveEntityLights,
|
|
_liveEntityPresentation,
|
|
_remoteTeleportController,
|
|
_animationHookFrames,
|
|
_effectPoses,
|
|
_audioEngine),
|
|
new RenderShutdownRoots(
|
|
_gpuFrameFlights,
|
|
_devToolsComposition,
|
|
_localPlayerTeleport,
|
|
_portalTunnelFallback,
|
|
_paperdollViewportRenderer,
|
|
_wbDrawDispatcher,
|
|
_envCellRenderer,
|
|
_portalDepthMask,
|
|
_clipFrame,
|
|
_skyRenderer,
|
|
_particleRenderer,
|
|
_samplerCache,
|
|
_textureCache,
|
|
_wbMeshAdapter,
|
|
_meshShader,
|
|
_terrain,
|
|
_terrainModernShader,
|
|
_sceneLightingUbo,
|
|
_debugLines,
|
|
_textRenderer,
|
|
_debugFont,
|
|
_displayFramePacing,
|
|
_frameProfiler,
|
|
_renderResourceLifetime,
|
|
_glConstructionCleanup),
|
|
new PlatformShutdownRoots(
|
|
_dats,
|
|
_input,
|
|
_gl));
|
|
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(releaseNativeWindow: true);
|
|
_window = null;
|
|
}
|
|
|
|
}
|