diff --git a/src/AcDream.App/Audio/AudioHookSink.cs b/src/AcDream.App/Audio/AudioHookSink.cs index 9f91b8cc..a687fafc 100644 --- a/src/AcDream.App/Audio/AudioHookSink.cs +++ b/src/AcDream.App/Audio/AudioHookSink.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Audio; @@ -19,33 +19,33 @@ namespace AcDream.App.Audio; /// Wiring: /// /// -/// → direct play of SoundHook.Id (a +/// → direct play of SoundHook.Id (a /// Wave dat id) at the entity's world position. Used for custom / /// per-animation audio like weapon swoosh or spell chant. /// /// -/// → look up the entity's SoundTable + +/// → look up the entity's SoundTable + /// the hook's SoundType, roll one /// via /// , play its wave. Retail's "footstep -/// that varies slightly" mechanism — also how attack / damage sounds +/// that varies slightly" mechanism — also how attack / damage sounds /// pick a creature-specific variant. /// /// -/// → same as SoundHook but with +/// → same as SoundHook but with /// pitch / volume overrides baked into the hook. /// /// /// /// /// -/// Entity → SoundTable id is resolved via an +/// Entity → SoundTable id is resolved via an /// callback passed in at construction; the renderer's per-entity state /// bag knows the PhysicsObj's SoundTableId (retail: /// PhysicsObj.soundtable_id). /// /// -internal sealed class AudioHookSink : IAnimationHookSink +public sealed class AudioHookSink : IAnimationHookSink { private readonly OpenAlAudioEngine _engine; private readonly DatSoundCache _cache; @@ -81,7 +81,7 @@ internal sealed class AudioHookSink : IAnimationHookSink case SoundTweakedHook stw: // SoundTweakedHook is a direct wave play with volume + // priority overrides baked into the hook itself (NOT a - // SoundTable lookup — that's SoundTableHook). Retail uses + // SoundTable lookup — that's SoundTableHook). Retail uses // this for the rare "explicit wave + explicit volume" case. Play(entityId, entityWorldPosition, waveId: (uint)stw.SoundId, @@ -90,7 +90,7 @@ internal sealed class AudioHookSink : IAnimationHookSink pitch: 1f); break; - // All the visual-only hooks (Scale, Luminous, Diffuse, …) + // All the visual-only hooks (Scale, Luminous, Diffuse, …) // are for other sinks to handle. } } @@ -138,7 +138,7 @@ internal sealed class AudioHookSink : IAnimationHookSink /// entity. Retail stores this on PhysicsObj.soundtable_id; our /// renderer keeps per-entity state that includes it. /// -internal interface IEntitySoundTable +public interface IEntitySoundTable { /// /// Return the SoundTable dat id (0x20xxxxxx) for , @@ -151,7 +151,7 @@ internal interface IEntitySoundTable /// Simple dictionary-backed ; the renderer /// assigns entries as it hydrates entities. /// -internal sealed class DictionaryEntitySoundTable : IEntitySoundTable +public sealed class DictionaryEntitySoundTable : IEntitySoundTable { private readonly Dictionary _table = new(); diff --git a/src/AcDream.App/Audio/OpenAlAudioEngine.cs b/src/AcDream.App/Audio/OpenAlAudioEngine.cs index e94409e2..959415ea 100644 --- a/src/AcDream.App/Audio/OpenAlAudioEngine.cs +++ b/src/AcDream.App/Audio/OpenAlAudioEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Audio; @@ -7,8 +7,8 @@ using Silk.NET.OpenAL; namespace AcDream.App.Audio; /// -/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's -/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3). +/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's +/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3). /// /// /// Architecture: @@ -16,7 +16,7 @@ namespace AcDream.App.Audio; /// /// Single + bound to the /// system default device. Cross-platform (WASAPI / WinMM / -/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks). +/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks). /// /// /// Fixed 16-source pool for 3D positional sounds. When all 16 are @@ -27,13 +27,13 @@ namespace AcDream.App.Audio; /// /// /// Separate UI source pool (4 sources) for flat 2D UI clicks / -/// wooshes — not subject to the 3D eviction game. +/// wooshes — not subject to the 3D eviction game. /// /// /// PCM buffer cache keyed by Wave dat id so the same footstep isn't /// re-uploaded to the GL-equivalent AL buffers on every hit. Bounded /// by a byte budget () enforced -/// with LRU eviction — see . A +/// with LRU eviction — see . A /// buffer still attached to a live source is never evicted (AL /// rejects deleting a bound buffer); eviction re-queries live AL /// source state rather than tracking a second copy of it. @@ -60,15 +60,15 @@ internal interface IWorldAudioQuiescence void ResumeWorldAudio(); } -internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence +public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence { - // ── Backends ───────────────────────────────────────────────────────────── + // ── Backends ───────────────────────────────────────────────────────────── private AL? _al; private OpenAlResourceLifetime? _resources; private bool _available; private bool _disposed; - // ── Pools ──────────────────────────────────────────────────────────────── + // ── Pools ──────────────────────────────────────────────────────────────── private const int PoolSize3D = 16; // retail 16-slot voice pool private const int PoolSizeUi = 4; @@ -88,7 +88,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc private readonly uint[] _poolUi = new uint[PoolSizeUi]; - // ── Buffer cache (Wave dat id → AL buffer) ─────────────────────────────── + // ── Buffer cache (Wave dat id → AL buffer) ─────────────────────────────── // Budget rationale: decoded PCM waves run ~100-500 KB each (same sizing // as DatSoundCache's payload LRU, which this cache re-uploads from). 48 // MiB gives comfortable headroom for the working set of a play session @@ -99,11 +99,11 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc private readonly Dictionary _bufferByWaveId = new(); private readonly AlBufferBudgetTracker _bufferBudget = new(DefaultBufferByteBudget); - // ── Ambient handles (StartAmbient/StopAmbient) ─────────────────────────── + // ── Ambient handles (StartAmbient/StopAmbient) ─────────────────────────── private readonly Dictionary _ambientSources = new(); private int _nextAmbientHandle = 1; - // ── Public volume knobs ────────────────────────────────────────────────── + // ── Public volume knobs ────────────────────────────────────────────────── public float MasterVolume { get; set; } = 1f; public float SfxVolume { get; set; } = 1f; public float MusicVolume { get; set; } = 0.7f; @@ -219,7 +219,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc _al = null; } - // ── IAudioEngine ───────────────────────────────────────────────────────── + // ── IAudioEngine ───────────────────────────────────────────────────────── public void SetListener( float posX, float posY, float posZ, @@ -241,7 +241,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc } /// - /// Not exposed on IAudioEngine but used by the hook sink — play a raw + /// Not exposed on IAudioEngine but used by the hook sink — play a raw /// WaveData blob at a 3D position with full priority/volume controls. /// Returns true on success, false if the buffer was rejected. /// @@ -278,7 +278,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc if (_pool3D[idx].PlayingGain < effectiveGain) { slotIdx = idx; break; } } } - if (slotIdx < 0) return false; // no slot quieter than us — drop + if (slotIdx < 0) return false; // no slot quieter than us — drop var slot = _pool3D[slotIdx]; _al.SourceStop(slot.SourceId); @@ -355,7 +355,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc return true; } - // IAudioEngine implementations — the enum-based overloads are less + // IAudioEngine implementations — the enum-based overloads are less // useful than the raw-Wave overloads above, since the hook sink already // has access to decoded WaveData. Left as no-ops for now; R5 defines // SoundId as a sparse subset of retail enums. @@ -366,7 +366,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc public int StartAmbient(SoundId id, float x, float y, float z) { - // Looping ambient — needs a decoded wave + WaveId. The hook sink + // Looping ambient — needs a decoded wave + WaveId. The hook sink // doesn't route ambient; a separate landblock-attached ambient // system (outside R5) will drive this. For now: reserve a handle. int handle = _nextAmbientHandle++; @@ -383,17 +383,17 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc } } - public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } + public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } public void StopMusic() { /* ditto */ } - // ── Private helpers ────────────────────────────────────────────────────── + // ── Private helpers ────────────────────────────────────────────────────── private uint EnsureBuffer(uint waveId, WaveData wave) { if (!_available || _al is null) return 0; if (_bufferByWaveId.TryGetValue(waveId, out var existing)) { - // Buffer id 0 is the "unsupported format" negative marker — no + // Buffer id 0 is the "unsupported format" negative marker — no // payload, not tracked by the budget, nothing to touch. if (existing != 0) _bufferBudget.Touch(waveId); @@ -425,15 +425,15 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc /// /// Evict least-recently-used AL buffers until the resident-byte budget /// is satisfied again. A buffer still bound to a live source (3D pool, - /// UI pool, or an ambient source) is protected — alDeleteBuffers - /// fails on a buffer that's still attached to a source — so eviction + /// UI pool, or an ambient source) is protected — alDeleteBuffers + /// fails on a buffer that's still attached to a source — so eviction /// never targets one; nor does it target , /// the buffer just created for this call and /// hasn't attached to a source yet. If every resident buffer is /// protected the budget is temporarily exceeded rather than looping /// forever; the bounded pool sizes (16 + 4 + ambient) cap how large /// that overage can get. Evicted waves replay through - /// again on next use — re-upload from + /// again on next use — re-upload from /// , identical to a first play. /// private void EvictBuffersOverBudget(uint protectedBufferId) diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 4caa35fa..0319914f 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -263,6 +263,8 @@ internal sealed class FrameRootCompositionPhase live.DrawDispatcher, live.EnvCellRenderer, live.PortalDepthMask, + foundation.TextRenderer, + interaction.RetainedUi?.Host.TextRenderer, live.ClipFrame, foundation.Terrain, foundation.SceneLighting), @@ -361,7 +363,6 @@ internal sealed class FrameRootCompositionPhase d.CellVisibility), d.WorldSceneDebugState, foundation.DebugLines, - host.GpuFrameLifetime, d.PhysicsEngine, d.PlayerMode, d.PlayerController, @@ -508,7 +509,7 @@ internal sealed class FrameRootCompositionPhase : (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation ?? NullRenderFramePostDiagnosticsPhase.Instance; var renderFrame = new RenderFrameOrchestrator( - host.GpuFrameLifetime, + host.GpuFrameFlights, new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl), framePreparation, worldSceneRenderer, diff --git a/src/AcDream.App/Composition/HostInputCameraComposition.cs b/src/AcDream.App/Composition/HostInputCameraComposition.cs index b3f0ca91..52e0592b 100644 --- a/src/AcDream.App/Composition/HostInputCameraComposition.cs +++ b/src/AcDream.App/Composition/HostInputCameraComposition.cs @@ -12,7 +12,6 @@ internal interface IGameWindowHostInputCameraPublication { void PublishGpuFrameFlights(GpuFrameFlightController value); void PublishGpuDevice(IGpuDevice value); - void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value); void PublishKeyboardSource(SilkKeyboardSource value); void PublishMouseSource(SilkMouseSource value); void PublishMouseLookCursor(IMouseLookCursor value); @@ -24,7 +23,6 @@ internal interface IGameWindowHostInputCameraPublication internal sealed record HostInputCameraResult( GpuFrameFlightController GpuFrameFlights, IGpuDevice GpuDevice, - GpuDeviceFrameLifetime GpuFrameLifetime, WorldRenderDiagnostics WorldRenderDiagnostics, SilkKeyboardSource? KeyboardSource, SilkMouseSource? MouseSource, @@ -237,10 +235,9 @@ internal sealed class HostInputCameraCompositionPhase : // exist — it owns its own BindlessSupport detection (see // GlGpuDevice's class comment), so unlike the legacy WB render path it // has no dependency on WorldRenderCompositionPhase running first. - // Campaign V slice V4a is the first real consumer (TextRenderer, - // BitmapFont, DebugLineRenderer, TextureCache's UI upload path); it is + // Nothing consumes this device yet (Campaign V slice V1); it is // proven against the real driver here and torn down with the render - // stack. + // stack so later slices (starting at V4a) have somewhere to plug in. IGpuDevice gpuDevice = scope.Acquire( "GPU device (RHI)", () => _factory.CreateGpuDevice(gl, gpuFrames), @@ -248,14 +245,6 @@ internal sealed class HostInputCameraCompositionPhase : _publication.PublishGpuDevice); Fault(HostInputCameraCompositionPoint.GpuDevicePublished); - // V4a's structural addition: the shared IGpuFrame lifecycle that - // ported renderers allocate rings and open passes against. Owns no - // disposable resource of its own — it wraps gpuDevice, whose scope - // above already tears it down — so it is published as a plain value, - // not a second acquisition. - var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice); - _publication.PublishGpuFrameLifetime(gpuFrameLifetime); - WorldRenderDiagnostics diagnostics = _factory.CreateWorldRenderDiagnostics( gl, @@ -356,7 +345,6 @@ internal sealed class HostInputCameraCompositionPhase : return new HostInputCameraResult( gpuFrames, gpuDevice, - gpuFrameLifetime, diagnostics, keyboard, mouse, diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index e2b9e7e8..7f189414 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -33,10 +33,9 @@ namespace AcDream.App.Composition; internal sealed record InteractionRetainedUiDependencies( RuntimeOptions Options, GL Gl, - IGpuDevice GpuDevice, - Func CurrentGpuFrame, IView Window, IInputContext Input, + string ShadersDirectory, IDatReaderWriter Dats, object DatLock, TextureCache TextureCache, @@ -390,8 +389,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory d.Character.LocalPlayer); UiHost host = lease.AcquireHost( () => new UiHost( - d.GpuDevice, - d.CurrentGpuFrame, + d.Gl, + d.ShadersDirectory, d.DebugFont, d.HostQuiescence)); checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired); @@ -478,9 +477,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory host.WireKeyboard(keyboard); checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired); - (GpuTextureSlot, int, int) ResolveChrome(uint id) + (uint, int, int) ResolveChrome(uint id) { - GpuTextureSlot texture = d.TextureCache.GetOrUploadRenderSurface( + uint texture = d.TextureCache.GetOrUploadRenderSurface( id, out int width, out int height); diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index ead0ec98..0e3cfe73 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -38,7 +38,6 @@ namespace AcDream.App.Composition; internal sealed record LivePresentationDependencies( RuntimeOptions Options, GL Gl, - IGpuDevice GpuDevice, IWindow Window, object DatLock, RuntimeSettingsController Settings, @@ -796,8 +795,7 @@ internal sealed class LivePresentationCompositionPhase paperdollLease.Resource, new RetailPaperdollFrameView( viewport, - new PaperdollInventoryVisibility(inventoryFrame), - d.GpuDevice), + new PaperdollInventoryVisibility(inventoryFrame)), new RetailPaperdollDollFactory( new LivePaperdollEntityLookup(liveEntities), d.PlayerIdentity, @@ -844,8 +842,7 @@ internal sealed class LivePresentationCompositionPhase new RetailCreatureAppraisalFrameView( creatureViewport, examinationFrame, - appraisalController, - d.GpuDevice), + appraisalController), new RetailCreatureAppraisalCloneFactory( new LiveCreatureAppraisalEntityLookup(liveEntities))); } diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs index 4decbc81..bdbc10f6 100644 --- a/src/AcDream.App/Composition/WorldRenderComposition.cs +++ b/src/AcDream.App/Composition/WorldRenderComposition.cs @@ -52,7 +52,6 @@ internal sealed record WorldRenderDependencies( WorldEnvironmentController Environment, IGameRenderResourceLifetime RenderResources, IGpuResourceRetirementQueue ResourceRetirement, - IGpuDevice GpuDevice, ResidencyBudgetOptions ResidencyBudgets, uint InitialCenterLandblockId, string DiagnosticsDirectory, @@ -90,10 +89,10 @@ internal interface IWorldRenderCompositionFactory void SetTerrainAnisotropic(TerrainAtlas atlas, int level); Shader CreateTerrainShader(GL gl, string shadersDirectory); SceneLightingUboBinding CreateSceneLighting(GL gl); - DebugLineRenderer CreateDebugLines(IGpuDevice device); + DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory); byte[]? TryLoadDebugFont(); - BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes); - TextRenderer CreateTextRenderer(IGpuDevice device); + BitmapFont CreateDebugFont(GL gl, byte[] bytes); + TextRenderer CreateTextRenderer(GL gl, string shadersDirectory); TerrainModernRenderer CreateTerrain( GL gl, BindlessSupport bindless, @@ -113,7 +112,6 @@ internal interface IWorldRenderCompositionFactory ResidencyBudgetOptions budgets); TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, @@ -216,15 +214,21 @@ internal sealed class RetailWorldRenderCompositionFactory public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl); - public DebugLineRenderer CreateDebugLines(IGpuDevice device) => new(device); + public DebugLineRenderer CreateDebugLines( + GL gl, + string shadersDirectory) => + new(gl, shadersDirectory); public byte[]? TryLoadDebugFont() => BitmapFont.TryLoadSystemMonospaceFont(); - public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) => - new(device, bytes, pixelHeight: 15f, atlasSize: 512); + public BitmapFont CreateDebugFont(GL gl, byte[] bytes) => + new(gl, bytes, pixelHeight: 15f, atlasSize: 512); - public TextRenderer CreateTextRenderer(IGpuDevice device) => new(device); + public TextRenderer CreateTextRenderer( + GL gl, + string shadersDirectory) => + new(gl, shadersDirectory); public TerrainModernRenderer CreateTerrain( GL gl, @@ -290,7 +294,6 @@ internal sealed class RetailWorldRenderCompositionFactory public TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, @@ -298,7 +301,6 @@ internal sealed class RetailWorldRenderCompositionFactory ResidencyBudgetOptions budgets) => new( gl, - device, dats, bindless, retirement, @@ -480,12 +482,12 @@ internal sealed class WorldRenderCompositionPhase DebugLineRenderer debugLines = AcquireAndPublish( scope, "debug lines", - () => _factory.CreateDebugLines(_dependencies.GpuDevice), + () => _factory.CreateDebugLines(gl, shadersDirectory), _publication.PublishDebugLines, WorldRenderCompositionPoint.DebugLinesPublished); (BitmapFont? debugFont, TextRenderer? textRenderer) = - ComposeOptionalHudResources(scope); + ComposeOptionalHudResources(scope, gl, shadersDirectory); TerrainModernRenderer terrain = AcquireAndPublish( scope, @@ -533,7 +535,6 @@ internal sealed class WorldRenderCompositionPhase "texture cache", () => _factory.CreateTextureCache( gl, - _dependencies.GpuDevice, content.Dats, bindless, _dependencies.ResourceRetirement, @@ -585,7 +586,9 @@ internal sealed class WorldRenderCompositionPhase } private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources( - CompositionAcquisitionScope scope) + CompositionAcquisitionScope scope, + GL gl, + string shadersDirectory) { byte[]? fontBytes = _factory.TryLoadDebugFont(); if (fontBytes is null) @@ -597,13 +600,13 @@ internal sealed class WorldRenderCompositionPhase var fontLease = scope.Acquire( "world HUD font", - () => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes), + () => _factory.CreateDebugFont(gl, fontBytes), _factory.Release); BitmapFont font = fontLease.Resource; Fault(WorldRenderCompositionPoint.DebugFontCreated); var textLease = scope.Acquire( "world HUD text renderer", - () => _factory.CreateTextRenderer(_dependencies.GpuDevice), + () => _factory.CreateTextRenderer(gl, shadersDirectory), _factory.Release); TextRenderer text = textLease.Resource; Fault(WorldRenderCompositionPoint.TextRendererCreated); diff --git a/src/AcDream.App/Diagnostics/FrameProfiler.cs b/src/AcDream.App/Diagnostics/FrameProfiler.cs index f49c1810..69d02add 100644 --- a/src/AcDream.App/Diagnostics/FrameProfiler.cs +++ b/src/AcDream.App/Diagnostics/FrameProfiler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -10,11 +10,11 @@ using Silk.NET.OpenGL; namespace AcDream.App.Diagnostics; /// Stage indices for per-frame CPU attribution. -internal enum FrameStage +public enum FrameStage { /// Whole OnUpdate body (simulation + streaming apply). Update = 0, - /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain. + /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain. Upload = 1, /// ImGui Render (dev overlay). ImGui = 2, @@ -23,11 +23,11 @@ internal enum FrameStage } /// -/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated +/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated /// [frame-prof] report discards when its 5-second window resets. /// Stage fields mirror positionally (Update / /// Upload / ImGui / Pacing, matching 's -/// names array) — if grows, extend this +/// names array) — if grows, extend this /// record, , and the CSV header /// together. GpuUs is -1 for a frame with no available GPU /// sample (warm-up, or ACDREAM_WB_DIAG=1 self-disable). @@ -44,7 +44,7 @@ internal readonly record struct FrameHistoryRecord( long PacingUs); /// -/// MP0 (2026-07-05) — the permanent honest frame profiler. One +/// MP0 (2026-07-05) — the permanent honest frame profiler. One /// FrameBoundary call at the top of the accepted render transaction /// measures CPU frame time as the delta between consecutive boundaries /// (captures the FULL frame including present) and samples per-frame allocated @@ -56,29 +56,29 @@ internal readonly record struct FrameHistoryRecord( /// is true; costs one /// bool check per frame when off. /// -/// Permanent apparatus — every MP-track gate reads it; do not strip. +/// Permanent apparatus — every MP-track gate reads it; do not strip. /// Whole-frame GPU timing self-disables under ACDREAM_WB_DIAG=1 /// (nested TimeElapsed is illegal GL; see GpuFrameTimer). /// -/// 2026-07-24 measurement-tooling review — the aggregated report +/// 2026-07-24 measurement-tooling review — the aggregated report /// resets its ring buffers every ~5 s (), /// so route-wide p50/p95/p99 distributions across a whole soak cannot be /// reconstructed after the fact. /// (ACDREAM_FRAME_HISTORY=<path>) opts into a SEPARATE /// per-frame history: one per frame in a /// preallocated, grow-as-needed (1 int + 1 double + -/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly -/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run, +/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly +/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run, /// not for unattended day-long capture). Its initial capacity is about 9 MiB /// and covers the canonical route above 300 FPS without a frame-thread resize. /// ZERO frame-thread I/O: the CSV is written once, from /// , at shutdown. Recording only takes effect while -/// is ALSO true — it +/// is ALSO true — it /// reuses that instrumentation rather than duplicating it. Does not /// change the [frame-prof] report format or any existing metric. -/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. +/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. /// -internal sealed class FrameProfiler : IDisposable +public sealed class FrameProfiler : IDisposable { private const int WindowCapacity = 2048; // ~12 s at 165 fps private const int HistoryInitialCapacity = 131072; @@ -136,7 +136,7 @@ internal sealed class FrameProfiler : IDisposable if (_wasEnabled) { // Dispose (not just Stop) so a later re-enable rebuilds the - // query ring fresh — a kept instance would poll slots left + // query ring fresh — a kept instance would poll slots left // pending from BEFORE the pause and report temporally stale // GPU samples. Safe here: this runs at the top of OnRender // with the GL context current. @@ -163,7 +163,7 @@ internal sealed class FrameProfiler : IDisposable { // First enabled frame (startup or runtime toggle-on): establish // baselines, emit nothing. Clear any stage ticks a StageScope - // disposed after toggle-off may have accumulated mid-pause — + // disposed after toggle-off may have accumulated mid-pause — // EndStage still runs on scopes that were live when the flag // flipped, and that partial delta must not leak into the first // re-enabled frame. @@ -274,7 +274,7 @@ internal sealed class FrameProfiler : IDisposable internal void EndStage(FrameStage stage, long startTimestamp) => _stageAccumTicks[(int)stage] += Stopwatch.GetTimestamp() - startTimestamp; - /// Pure report formatter — unit-tested; invariant culture. + /// Pure report formatter — unit-tested; invariant culture. public static string FormatReport( int frameCount, FrameStatsBuffer cpu, FrameStatsBuffer gpu, bool gpuActive, @@ -301,7 +301,7 @@ internal sealed class FrameProfiler : IDisposable return sb.ToString(); } - /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record. + /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record. internal static void WriteHistoryCsv( IEnumerable records, TextWriter writer, @@ -330,7 +330,7 @@ internal sealed class FrameProfiler : IDisposable /// /// Shutdown-only write of the accumulated as CSV - /// (the ONLY I/O this feature performs — never from ). + /// (the ONLY I/O this feature performs — never from ). /// Failures are logged, not thrown: a history-export problem must never /// block the rest of the render-owner shutdown chain /// (GameWindowLifetime's Hard("frame profiler", ...) stage). @@ -355,7 +355,7 @@ internal sealed class FrameProfiler : IDisposable } /// Disposable stage scope; default instance is a no-op. -internal readonly struct StageScope : IDisposable +public readonly struct StageScope : IDisposable { private readonly FrameProfiler? _owner; private readonly FrameStage _stage; diff --git a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs index d94936ce..60773235 100644 --- a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs +++ b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs @@ -1,16 +1,16 @@ -using System; +using System; namespace AcDream.App.Diagnostics; /// -/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples +/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples /// (microseconds or bytes) with percentile/max over the current window. /// Pure and allocation-free after construction: /// sorts into a preallocated scratch array, so the 5-second report path -/// allocates nothing. Not thread-safe — owned by the window loop thread. -/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. +/// allocates nothing. Not thread-safe — owned by the window loop thread. +/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5. /// -internal sealed class FrameStatsBuffer +public sealed class FrameStatsBuffer { private readonly long[] _samples; private readonly long[] _scratch; @@ -41,7 +41,7 @@ internal sealed class FrameStatsBuffer /// /// Nearest-rank percentile over the current window: element at - /// ceil(q·n) in the ascending sort (1-based), 0 when empty. + /// ceil(q·n) in the ascending sort (1-based), 0 when empty. /// public long Percentile(double q) { diff --git a/src/AcDream.App/GlobalUsings.cs b/src/AcDream.App/GlobalUsings.cs index 7e8a6845..5213b6cc 100644 --- a/src/AcDream.App/GlobalUsings.cs +++ b/src/AcDream.App/GlobalUsings.cs @@ -2,8 +2,3 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; -// Campaign V slice V4a: GpuTextureSlot (and the rest of the RHI contract) is -// used pervasively once TextRenderer/TextureCache's UI path stop dealing in -// raw GL texture names, so it is imported project-wide rather than adding a -// `using AcDream.App.Rendering.Gpu;` to every UI file that resolves a sprite. -global using AcDream.App.Rendering.Gpu; diff --git a/src/AcDream.App/Input/LocalPlayerProjectionController.cs b/src/AcDream.App/Input/LocalPlayerProjectionController.cs index 0e6cf13f..8a32c4d7 100644 --- a/src/AcDream.App/Input/LocalPlayerProjectionController.cs +++ b/src/AcDream.App/Input/LocalPlayerProjectionController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; using AcDream.App.Physics; using AcDream.App.World; @@ -59,7 +59,7 @@ internal sealed class LiveLocalPlayerProjectionRuntime /// Projects the canonical local physics body into world rendering and spatial /// buckets without advancing it. /// -internal sealed class LocalPlayerProjectionController +public sealed class LocalPlayerProjectionController { private readonly ILocalPlayerProjectionRuntime _runtime; diff --git a/src/AcDream.App/Input/PlayerModeAutoEntry.cs b/src/AcDream.App/Input/PlayerModeAutoEntry.cs index 2c629a11..cc158601 100644 --- a/src/AcDream.App/Input/PlayerModeAutoEntry.cs +++ b/src/AcDream.App/Input/PlayerModeAutoEntry.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.App.Net; using AcDream.App.Streaming; using AcDream.App.World; @@ -6,7 +6,7 @@ using AcDream.App.World; namespace AcDream.App.Input; /// -/// Phase K.2 — one-shot guard that auto-enters player mode after a +/// Phase K.2 — one-shot guard that auto-enters player mode after a /// successful login once every prerequisite is satisfied. The update-frame /// orchestrator ticks it through a typed production context. /// @@ -16,7 +16,7 @@ namespace AcDream.App.Input; /// entity has been streamed into the world dictionary, the player /// movement controller is constructible, and the initial world is fully /// drawable and collidable) plus a manual-override path -/// (the user can flip into fly mode before the auto-entry fires — +/// (the user can flip into fly mode before the auto-entry fires — /// their choice wins). All five interact with each other in a way /// that's painful to test through GameWindow but trivial here against /// fakes. @@ -25,11 +25,11 @@ namespace AcDream.App.Input; /// /// The public surface is: /// -/// — call after EnterWorld succeeds to +/// — call after EnterWorld succeeds to /// arm the entry trigger. -/// — call when the user manually enters +/// — call when the user manually enters /// fly mode (or any other code path that pre-empts the auto-entry). -/// — call once per frame; runs the +/// — call once per frame; runs the /// guard and fires the entry callback when armed AND every /// precondition is satisfied; returns true on the firing tick. /// @@ -101,7 +101,7 @@ internal sealed class LivePlayerModeAutoEntryContext } } -internal sealed class PlayerModeAutoEntry +public sealed class PlayerModeAutoEntry { private sealed class DelegateContext : IPlayerModeAutoEntryContext { @@ -163,7 +163,7 @@ internal sealed class PlayerModeAutoEntry /// Retail keeps position completion behind one blocking cell-load edge; /// acdream's asynchronous domains must converge before entry. /// Action invoked on the firing - /// tick. The same routine the manual Tab handler invokes (fly → + /// tick. The same routine the manual Tab handler invokes (fly → /// player transition). Must construct the controller + chase /// camera and switch the active camera; the auto-entry doesn't /// reach inside. @@ -202,7 +202,7 @@ internal sealed class PlayerModeAutoEntry /// /// Disarm the trigger without firing the callback. Call when the /// user has manually entered fly mode (or any other code path - /// that pre-empts the auto-entry) — the user's choice wins. + /// that pre-empts the auto-entry) — the user's choice wins. /// public void Cancel() => _armed = false; diff --git a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs index f66bee33..ca4ee1b9 100644 --- a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs +++ b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Update; +using AcDream.App.Update; using AcDream.Runtime; namespace AcDream.App.Input; @@ -17,7 +17,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram { private readonly RuntimeLocalPlayerFrameController _runtime; - internal readonly record struct PresentationFrame( + public readonly record struct PresentationFrame( MovementResult Movement, bool Hidden, bool AdvancedBeforeNetwork); diff --git a/src/AcDream.App/Input/SilkKeyboardSource.cs b/src/AcDream.App/Input/SilkKeyboardSource.cs index 061e258e..bf1c17d1 100644 --- a/src/AcDream.App/Input/SilkKeyboardSource.cs +++ b/src/AcDream.App/Input/SilkKeyboardSource.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering; +using AcDream.App.Rendering; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; @@ -73,7 +73,7 @@ internal sealed class SilkKeyboardEventSurface : IKeyboardEventSurface /// subscription. Logical deactivation makes copied delegates inert while /// physical removal is retried. /// -internal sealed class SilkKeyboardSource : IKeyboardSource, IDisposable +public sealed class SilkKeyboardSource : IKeyboardSource, IDisposable { private readonly IKeyboardEventSurface _surface; private readonly HostQuiescenceGate _quiescence; diff --git a/src/AcDream.App/Input/SilkMouseSource.cs b/src/AcDream.App/Input/SilkMouseSource.cs index d0df94cb..a8530a64 100644 --- a/src/AcDream.App/Input/SilkMouseSource.cs +++ b/src/AcDream.App/Input/SilkMouseSource.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; @@ -100,7 +100,7 @@ internal sealed class SilkMouseEventSurface : IMouseEventSurface } /// Reversible Silk mouse bridge with immediate logical cutoff. -internal sealed class SilkMouseSource : IMouseSource, IDisposable +public sealed class SilkMouseSource : IMouseSource, IDisposable { private readonly IMouseEventSurface _surface; private readonly IInputCaptureSource _capture; diff --git a/src/AcDream.App/Physics/LocalPlayerShadowState.cs b/src/AcDream.App/Physics/LocalPlayerShadowState.cs index db419aee..777384da 100644 --- a/src/AcDream.App/Physics/LocalPlayerShadowState.cs +++ b/src/AcDream.App/Physics/LocalPlayerShadowState.cs @@ -1,11 +1,11 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Physics; /// Session-scoped cache of the local player's last published shadow pose. internal sealed class LocalPlayerShadowState { - internal readonly record struct Snapshot( + public readonly record struct Snapshot( Vector3 Position, Quaternion Orientation, uint CellId); diff --git a/src/AcDream.App/Physics/RemoteTeleportHook.cs b/src/AcDream.App/Physics/RemoteTeleportHook.cs index ed689267..a0b54769 100644 --- a/src/AcDream.App/Physics/RemoteTeleportHook.cs +++ b/src/AcDream.App/Physics/RemoteTeleportHook.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Physics; +using AcDream.Core.Physics; namespace AcDream.App.Physics; @@ -8,7 +8,7 @@ namespace AcDream.App.Physics; /// testable while the concrete movement/interpolation/target owners remain /// in the composition root. /// -internal static class RemoteTeleportHook +public static class RemoteTeleportHook { private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu; @@ -46,7 +46,7 @@ internal static class RemoteTeleportHook } } -internal sealed record RemoteTeleportHookActions( +public sealed record RemoteTeleportHookActions( Action CancelMoveTo, Action UnStick, Action StopInterpolating, diff --git a/src/AcDream.App/Plugins/AppPluginHost.cs b/src/AcDream.App/Plugins/AppPluginHost.cs index c8b99ed2..bfabab86 100644 --- a/src/AcDream.App/Plugins/AppPluginHost.cs +++ b/src/AcDream.App/Plugins/AppPluginHost.cs @@ -1,8 +1,8 @@ -using AcDream.Plugin.Abstractions; +using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; -internal sealed class AppPluginHost : IPluginHost +public sealed class AppPluginHost : IPluginHost { public AppPluginHost( IPluginLogger log, diff --git a/src/AcDream.App/Plugins/BufferedUiRegistry.cs b/src/AcDream.App/Plugins/BufferedUiRegistry.cs index b8c431f4..bcab04fb 100644 --- a/src/AcDream.App/Plugins/BufferedUiRegistry.cs +++ b/src/AcDream.App/Plugins/BufferedUiRegistry.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; @@ -8,9 +8,9 @@ namespace AcDream.App.Plugins; /// Program.cs before the GL window opens) until GameWindow drains them into the /// UiHost tree after construction. /// -internal sealed class BufferedUiRegistry : IUiRegistry +public sealed class BufferedUiRegistry : IUiRegistry { - internal readonly record struct Pending(string MarkupPath, object Binding); + public readonly record struct Pending(string MarkupPath, object Binding); private readonly List _pending = new(); diff --git a/src/AcDream.App/Plugins/SerilogAdapter.cs b/src/AcDream.App/Plugins/SerilogAdapter.cs index d336b019..9f0b4f09 100644 --- a/src/AcDream.App/Plugins/SerilogAdapter.cs +++ b/src/AcDream.App/Plugins/SerilogAdapter.cs @@ -1,8 +1,8 @@ -using AcDream.Plugin.Abstractions; +using AcDream.Plugin.Abstractions; namespace AcDream.App.Plugins; -internal sealed class SerilogAdapter : IPluginLogger +public sealed class SerilogAdapter : IPluginLogger { private readonly Serilog.ILogger _log; public SerilogAdapter(Serilog.ILogger log) => _log = log; diff --git a/src/AcDream.App/Rendering/BitmapFont.cs b/src/AcDream.App/Rendering/BitmapFont.cs index 73ecb214..9306b4bd 100644 --- a/src/AcDream.App/Rendering/BitmapFont.cs +++ b/src/AcDream.App/Rendering/BitmapFont.cs @@ -1,20 +1,20 @@ -using System; +using System; using System.IO; +using Silk.NET.OpenGL; using StbTrueTypeSharp; namespace AcDream.App.Rendering; /// /// A pixel-font atlas rasterized from a TTF at load time using stb_truetype. -/// Glyphs are packed into a single-channel (R8) texture registered in the -/// device's global texture table. Call to resolve an -/// ASCII codepoint to UV + metrics. +/// Glyphs are packed into a single-channel (R8) GL texture. Call +/// to resolve an ASCII codepoint to UV + metrics. /// /// Only printable ASCII (32..127) is supported for the debug overlay. /// -internal sealed unsafe class BitmapFont : IDisposable +public sealed unsafe class BitmapFont : IDisposable { - internal readonly struct Glyph + public readonly struct Glyph { public readonly float UvMinX; public readonly float UvMinY; @@ -34,23 +34,23 @@ internal sealed unsafe class BitmapFont : IDisposable } } + private readonly GL _gl; private readonly Glyph[] _glyphs; private readonly int _firstChar; private readonly int _numChars; - private readonly IGpuDevice _device; - private readonly IGpuTexture _texture; + private readonly ResourceCleanupGroup _resources; - public GpuTextureSlot TextureId { get; } + public uint TextureId { get; } public float PixelHeight { get; } public float LineHeight { get; } public float Ascent { get; } public int AtlasWidth { get; } public int AtlasHeight { get; } - public BitmapFont(IGpuDevice device, byte[] ttfBytes, float pixelHeight, + public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight, int atlasSize = 512, int firstChar = 32, int numChars = 96) { - _device = device ?? throw new ArgumentNullException(nameof(device)); + _gl = gl; PixelHeight = pixelHeight; AtlasWidth = atlasSize; AtlasHeight = atlasSize; @@ -96,31 +96,65 @@ internal sealed unsafe class BitmapFont : IDisposable adv: bc.xadvance); } - // Upload atlas as a single-channel texture (R8) and register it in the - // device's global texture table. Linear + clamp-to-edge matches the - // GL path's prior fixed sampler state exactly (mip filtering is moot — - // the atlas is a single mip level). - IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription( - "bitmap-font-atlas", - GpuTextureKind.Texture2D, - GpuTextureFormat.R8Unorm, - Width: AtlasWidth, - Height: AtlasHeight, - LayerCount: 1, - MipLevelCount: 1)); + // Upload atlas as a single-channel GL texture (R8). Publish the GL + // name into the construction ledger before any later upload/state + // command can fail. + var resources = new ResourceCleanupGroup(); + uint texture = 0; try { - texture.Upload(0, 0, pixels); - IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp); - TextureId = _device.RegisterTexture(texture, sampler); + texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas"); + uint ownedTexture = texture; + resources.Add( + "bitmap-font atlas", + () => GlResourceCommand.DeleteTexture( + _gl, + ownedTexture, + $"delete BitmapFont atlas {ownedTexture}")); + _gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture); + _gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment); + GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () => + { + try + { + _gl.BindTexture(TextureTarget.Texture2D, texture); + _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1); + fixed (byte* ptr = pixels) + { + _gl.TexImage2D(TextureTarget.Texture2D, 0, + (int)InternalFormat.R8, + (uint)AtlasWidth, (uint)AtlasHeight, 0, + PixelFormat.Red, PixelType.UnsignedByte, ptr); + } + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, + (int)TextureMinFilter.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, + (int)TextureMagFilter.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, + (int)TextureWrapMode.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, + (int)TextureWrapMode.ClampToEdge); + } + finally + { + _gl.PixelStore( + PixelStoreParameter.UnpackAlignment, + previousAlignment); + _gl.BindTexture( + TextureTarget.Texture2D, + unchecked((uint)previousTexture)); + } + }); } - catch + catch (Exception constructionFailure) { - texture.Dispose(); - throw; + resources.RollbackConstructionAndThrow( + "BitmapFont construction failed and its GL atlas did not cleanly roll back.", + constructionFailure); } - _texture = texture; + TextureId = texture; + _resources = resources; } public bool TryGetGlyph(char c, out Glyph g) @@ -149,8 +183,7 @@ internal sealed unsafe class BitmapFont : IDisposable public void Dispose() { - _device.ReleaseTextureSlot(TextureId); - _texture.Dispose(); + _resources.RetryCleanup(); } /// diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs index 0944e344..60b84ac5 100644 --- a/src/AcDream.App/Rendering/CameraController.cs +++ b/src/AcDream.App/Rendering/CameraController.cs @@ -1,9 +1,9 @@ -// src/AcDream.App/Rendering/CameraController.cs +// src/AcDream.App/Rendering/CameraController.cs using AcDream.Core.Rendering; namespace AcDream.App.Rendering; -internal sealed class CameraController +public sealed class CameraController { internal readonly record struct CameraState( int ModeCode, @@ -19,7 +19,7 @@ internal sealed class CameraController /// The renderer-facing active camera. Both the legacy and retail /// chase cameras are held simultaneously so that flipping /// takes effect - /// on the very next access to this property — no re-entry required, + /// on the very next access to this property — no re-entry required, /// no notification mechanism, no stale state. /// public ICamera Active @@ -59,7 +59,7 @@ internal sealed class CameraController /// /// Store both cameras simultaneously; picks - /// between them per-read via the flag — no re-entry needed on flip. + /// between them per-read via the flag — no re-entry needed on flip. /// public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail) { diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs index 544d60cd..31b9ebca 100644 --- a/src/AcDream.App/Rendering/CellVisibility.cs +++ b/src/AcDream.App/Rendering/CellVisibility.cs @@ -1,11 +1,11 @@ -// CellVisibility.cs — portal-based interior cell visibility system. +// CellVisibility.cs — portal-based interior cell visibility system. // // Stage 3 (2026-06-02): FindCameraCell + grace-frame AABB fallback deleted. // The physics membership answer (CellGraph.CurrCell) is now the mandatory root; -// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than +// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than // falling back to an independent AABB position resolve. This matches retail's // CellManager::ChangePosition (0x004559B0) which does not re-derive the cell -// from a static position — it reads the swept transition-owned CurrCell. +// from a static position — it reads the swept transition-owned CurrCell. // // This file is intentionally free of GL / rendering types. It depends only on // System.Numerics so it can be unit-tested without a GPU context. @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering; /// A loaded EnvCell with portal connectivity and spatial data, used by /// for portal-traversal visibility decisions. /// -internal sealed class LoadedCell +public sealed class LoadedCell { /// Full 32-bit cell ID, e.g. 0xA9B40105. public uint CellId; @@ -87,7 +87,7 @@ internal sealed class LoadedCell public uint? BuildingId { get; internal set; } /// - /// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail + /// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail /// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially /// visible from this cell, precomputed by the AC content tools. Refreshed only at /// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878). @@ -98,7 +98,7 @@ internal sealed class LoadedCell public IReadOnlyList VisibleCells = System.Array.Empty(); /// - /// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees + /// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees /// the exterior (an exit portal is reachable from it). Retail gates the landscape /// data + draw decision on the camera cell's value (RenderNormalMode decomp:92649, /// grab_visible_cells decomp:311878). The stable anchor for the terrain-draw test. @@ -107,7 +107,7 @@ internal sealed class LoadedCell /// /// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by - /// — the outdoor world modelled as a flood-graph cell whose + /// — the outdoor world modelled as a flood-graph cell whose /// shell is the landscape. seeds OutsideView /// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell). /// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but @@ -123,19 +123,19 @@ internal sealed class LoadedCell /// is the dat's reciprocal back-link: the index of /// the portal WITHIN the neighbour cell's portal list that points back through /// this same opening. Retail indexes the reciprocal directly via this field -/// (arg2->other_portal_id, decomp:433557) rather than scanning — which +/// (arg2->other_portal_id, decomp:433557) rather than scanning — which /// is what lets a cell with TWO portals to the same neighbour resolve each /// opening against its OWN reciprocal polygon instead of the first match. /// /// -internal readonly record struct CellPortalInfo( +public readonly record struct CellPortalInfo( ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId); /// /// Clip plane derived from a portal polygon, in cell-local space. /// Plane equation: Normal.X*x + Normal.Y*y + Normal.Z*z + D = 0. /// -internal struct PortalClipPlane +public struct PortalClipPlane { /// Plane normal (cell-local space, unit length). public Vector3 Normal; @@ -146,8 +146,8 @@ internal struct PortalClipPlane /// /// Which half-space is "inside" this cell (the side from which you look outward /// through the portal): - /// 0 → camera dot-product must be >= 0 (positive half-space is inside) - /// 1 → camera dot-product must be <= 0 (negative half-space is inside) + /// 0 → camera dot-product must be >= 0 (positive half-space is inside) + /// 1 → camera dot-product must be <= 0 (negative half-space is inside) /// Determined from cell centroid position relative to the portal plane. /// Ported from ACME EnvCellManager.cs ~line 404. /// @@ -155,12 +155,12 @@ internal struct PortalClipPlane } /// -/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked +/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked /// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained /// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that /// still prints (always None post-Stage 3). /// -internal enum CameraCellResolution +public enum CameraCellResolution { /// No cell contains the eye (outdoors), or not yet resolved. None, @@ -171,14 +171,14 @@ internal enum CameraCellResolution /// The eye is inside a cell found by the full brute-force scan. BruteForce, /// The eye is inside NO cell, but the previous cell is kept alive for a - /// few grace frames — the "stale root" case the flap probe watches for. + /// few grace frames — the "stale root" case the flap probe watches for. Grace, } /// /// Result of a portal-based visibility BFS from the camera cell. /// -internal sealed class VisibilityResult +public sealed class VisibilityResult { /// Full cell IDs (e.g. 0x01D90105) that should be rendered this frame. public HashSet VisibleCellIds { get; init; } = new(); @@ -207,7 +207,7 @@ internal sealed class VisibilityResult /// Ported faithfully from ACME's EnvCellManager.cs portal-visibility region. /// Constants and control flow match the ACME implementation. /// -internal sealed class CellVisibility +public sealed class CellVisibility { // ------------------------------------------------------------------ // Constants (ACME ground-truth values) @@ -234,7 +234,7 @@ internal sealed class CellVisibility public VisibilityResult? LastVisibilityResult { get; private set; } /// - /// Stage 3 (2026-06-02): always — the FindCameraCell + /// Stage 3 (2026-06-02): always — the FindCameraCell /// AABB grace-frame resolver was deleted; the physics membership answer is the sole root. /// Retained for the [flap-cam] probe log line in GameWindow.cs. /// @@ -292,7 +292,7 @@ internal sealed class CellVisibility /// /// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a /// landblock prefix. Used by LandblockRenderPublisher when building - /// the per-landblock BuildingRegistry — the per-frame + /// the per-landblock BuildingRegistry — the per-frame /// drainedCells dict misses cells loaded on prior frames, so the /// stamping loop in needs access to /// every cell currently in the landblock to ensure BuildingId is set. @@ -356,15 +356,15 @@ internal sealed class CellVisibility /// /// UCG W2/Stage 3: compute visibility from a supplied root cell (the physics membership /// answer). When is null (pre-spawn, or player outside all indoor - /// cells), returns null — the caller interprets null as the outdoor root (no portal + /// cells), returns null — the caller interprets null as the outdoor root (no portal /// frame, everything slot 0, terrain ungated). The legacy AABB FindCameraCell fallback is /// deleted as of Stage 3; is the sole authority. /// Retail anchor: CellManager::ChangePosition @ 0x004559B0 reads the transition-owned - /// curr_cell — it does NOT re-derive from a static position. + /// curr_cell — it does NOT re-derive from a static position. /// /// /// The render-registered that physics determined the player is inside, - /// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path. + /// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path. /// /// /// Used as the viewer position for the portal-side test in the BFS when root is non-null. @@ -389,13 +389,13 @@ internal sealed class CellVisibility } // ------------------------------------------------------------------ - // FindCameraCell — DELETED in Stage 3 (2026-06-02) + // FindCameraCell — DELETED in Stage 3 (2026-06-02) // ------------------------------------------------------------------ // The AABB + grace-frame camera-cell resolver was removed. Production code - // now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the + // now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the // transition-owned CellGraph.CurrCell (set by ResolveCellId/Stage 2 physics). // Retail anchor: CellManager::ChangePosition (0x004559B0) reads curr_cell - // from the sweep — it never re-derives from a static position. + // from the sweep — it never re-derives from a static position. // // GetVisibleCells (used by ComputeVisibility below for test compatibility) // still uses the brute-force AABB scan internally. @@ -471,12 +471,12 @@ internal sealed class CellVisibility /// /// UCG W2: BFS visibility traversal from a pre-resolved root cell. - /// The root is the correct membership answer (supplied by the caller — + /// The root is the correct membership answer (supplied by the caller — /// physics CurrCell via , or AABB /// scan via for test compat). /// /// The BFS body is byte-identical to the original GetVisibleCells - /// implementation — only root acquisition was extracted out. + /// implementation — only root acquisition was extracted out. /// private VisibilityResult? GetVisibleCellsFromRoot(LoadedCell cameraCell, Vector3 cameraPos) { @@ -499,7 +499,7 @@ internal sealed class CellVisibility { var portal = cell.Portals[i]; - // Exit portal → outdoor terrain should be visible. + // Exit portal → outdoor terrain should be visible. if (portal.OtherCellId == 0xFFFF) { result.HasExitPortalVisible = true; @@ -522,8 +522,8 @@ internal sealed class CellVisibility var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform); float dot = Vector3.Dot(plane.Normal, localCam) + plane.D; - // InsideSide == 0 → inside is positive half-space; reject if dot < -ε. - // InsideSide == 1 → inside is negative half-space; reject if dot > ε. + // InsideSide == 0 → inside is positive half-space; reject if dot < -ε. + // InsideSide == 1 → inside is negative half-space; reject if dot > ε. // Source: ACME EnvCellManager.cs lines 1458-1459. if (plane.InsideSide == 0 && dot < -PointInCellEpsilon) continue; diff --git a/src/AcDream.App/Rendering/ChaseCamera.cs b/src/AcDream.App/Rendering/ChaseCamera.cs index b6b33c28..f58476f2 100644 --- a/src/AcDream.App/Rendering/ChaseCamera.cs +++ b/src/AcDream.App/Rendering/ChaseCamera.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.Rendering; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// character. Implements so it plugs into the /// existing renderer pipeline. /// -internal sealed class ChaseCamera : ICamera +public sealed class ChaseCamera : ICamera { public Vector3 Position { get; private set; } public float Aspect { get; set; } = 16f / 9f; @@ -37,9 +37,9 @@ internal sealed class ChaseCamera : ICamera // (at distance * sin(Pitch)) so the player can be viewed from a low // angle. Clamped to -0.7 to avoid pushing the camera deep underground; // at -0.7 and Distance=8 the camera is ~5m below player-Z which will - // clip terrain on hills but is OK on flat ground. 1.4 ≈ looking + // clip terrain on hills but is OK on flat ground. 1.4 ≈ looking // straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the - // camera in both directions from the neutral [~20°] default. + // camera in both directions from the neutral [~20°] default. private const float PitchMin = -0.7f; private const float PitchMax = 1.4f; @@ -47,7 +47,7 @@ internal sealed class ChaseCamera : ICamera private Vector3 _lookAt; // K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is - // pinned to the LAST GROUNDED Z while the player is airborne — the + // pinned to the LAST GROUNDED Z while the player is airborne — the // character rises above the camera on screen, visually matching // retail's "you can see yourself jump" feedback. Walking on the // ground tracks Z directly (no lag on hill transitions); falling @@ -90,10 +90,10 @@ internal sealed class ChaseCamera : ICamera { _trackedZ = playerPosition.Z; // catch up to falls / drops } - // else: airborne and rising — keep _trackedZ pinned. + // else: airborne and rising — keep _trackedZ pinned. // Look-at uses the actual player Z so the camera always points - // at the character — when the player rises above the pinned + // at the character — when the player rises above the pinned // camera the look-at tilts up to keep them centered in frame. _lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight); @@ -109,7 +109,7 @@ internal sealed class ChaseCamera : ICamera Position = new Vector3( playerPosition.X - forwardX * horizontalDist, playerPosition.Y - forwardY * horizontalDist, - _trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne) + _trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne) } /// diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs index cd02c440..76174b22 100644 --- a/src/AcDream.App/Rendering/ClipFrame.cs +++ b/src/AcDream.App/Rendering/ClipFrame.cs @@ -1,20 +1,20 @@ -// ClipFrame.cs +// ClipFrame.cs // // Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip // data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert // (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the // per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by // each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance -// buffer — it is NOT here. +// buffer — it is NOT here. // // === The contract (both shader sides obey) =================================== // binding=2 mesh SSBO holds an array of CellClip, one per "slot": // struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; }; // std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16 -// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0). +// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0). // binding=2 terrain UBO holds the single OutsideView region: // layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; }; -// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes. +// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes. // // In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a // terrain count of 0. Everything renders exactly as before. U.4 populates real @@ -40,16 +40,16 @@ namespace AcDream.App.Rendering; /// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by /// each renderer, not here. /// -internal sealed class ClipFrame : IDisposable +public sealed class ClipFrame : IDisposable { // ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ---- - /// Max planes per clip region — matches the shader's planes[8] + /// Max planes per clip region — matches the shader's planes[8] /// and GL's guaranteed GL_MAX_CLIP_DISTANCES >= 8. public const int MaxPlanes = 8; /// std430 stride of one CellClip: 16 (count + 3 pad uints) + - /// 8 × 16 (vec4 planes) = 144 bytes. + /// 8 × 16 (vec4 planes) = 144 bytes. public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144 /// Byte offset of planes[0] within a CellClip (after the @@ -57,7 +57,7 @@ internal sealed class ClipFrame : IDisposable public const int CellClipPlanesOffset = 16; /// std140 size of the terrain UBO block: int count padded to 16, then - /// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by + /// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by /// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family. public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144 @@ -66,7 +66,7 @@ internal sealed class ClipFrame : IDisposable public const uint MeshClipSsboBinding = 2; /// UBO binding index for the terrain OutsideView clip region - /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO + /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO /// binding=2 above. public const uint TerrainClipUboBinding = 2; @@ -139,23 +139,23 @@ internal sealed class ClipFrame : IDisposable /// /// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain - /// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4 + /// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4 /// replaces this with a frame built from real portal visibility. /// public static ClipFrame NoClip() { - // One slot, all zeros: count=0 ⇒ shader passes every plane. + // One slot, all zeros: count=0 ⇒ shader passes every plane. var bytes = new byte[CellClipStrideBytes]; return new ClipFrame(bytes, slotCount: 1); } - /// Number of clip slots currently packed (always >= 1 — slot 0 is + /// Number of clip slots currently packed (always >= 1 — slot 0 is /// the reserved no-clip slot). public int SlotCount => _slotCount; /// - /// Phase U.4: reset this frame back to the NoClip state — exactly slot 0 - /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new + /// Phase U.4: reset this frame back to the NoClip state — exactly slot 0 + /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new /// frame or new GL buffers. The single long-lived _clipFrame in /// GameWindow is reset + re-packed every frame by , /// then uploaded through one SSBO and one terrain arena per fenced frame slot. @@ -209,7 +209,7 @@ internal sealed class ClipFrame : IDisposable /// /// Append one clip region (becomes the next slot index) from a /// . Only the convex-plane case is supported in - /// U.3 — Count > 0 packs that many planes; Count == 0 packs a + /// U.3 — Count > 0 packs that many planes; Count == 0 packs a /// no-clip region (pass-all). The scissor / nothing-visible fallbacks that /// can carry are deferred to U.4 (which will draw /// the AABB box or skip the cell on the CPU side, not via this slot). Returns @@ -259,7 +259,7 @@ internal sealed class ClipFrame : IDisposable /// /// Set the terrain OutsideView clip region (the single region the terrain /// shader gates against). length 0 ungates terrain - /// (count 0). U.3 callers never touch this — leaves it + /// (count 0). U.3 callers never touch this — leaves it /// at count 0. U.4 calls it with the OutsideView planes. /// public void SetTerrainClip(ReadOnlySpan planes) @@ -579,7 +579,7 @@ internal sealed class ClipFrame : IDisposable } /// A single std140 terrain-clip record within a frame-slot UBO arena. -internal readonly record struct TerrainClipBufferBinding( +public readonly record struct TerrainClipBufferBinding( uint Buffer, int OffsetBytes, int SizeBytes) diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs index 2a38bff1..38b72df4 100644 --- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs +++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs @@ -1,4 +1,4 @@ -// ClipFrameAssembler.cs +// ClipFrameAssembler.cs // // Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like // view graph: one portal_view list per visible cell plus an outside_view list. @@ -21,7 +21,7 @@ namespace AcDream.App.Rendering; /// /// How the landscape-through-outside_view pass should be interpreted. /// -internal enum TerrainClipMode +public enum TerrainClipMode { /// All outside_view slices have convex plane clips. Planes, @@ -37,13 +37,13 @@ internal enum TerrainClipMode /// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained /// for passes that cannot write gl_ClipDistance and must use scissor. /// -internal readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); +public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); /// /// Result of : populated clip buffers /// plus routing data consumed by the render orchestration. /// -internal sealed class ClipFrameAssembly +public sealed class ClipFrameAssembly { public ClipFrame Frame { get; private set; } = null!; @@ -241,7 +241,7 @@ internal sealed class ClipFrameAssembly } } -internal static class ClipFrameAssembler +public static class ClipFrameAssembler { public static ClipFrameAssembly Assemble( ClipFrame frame, diff --git a/src/AcDream.App/Rendering/ClipPlaneSet.cs b/src/AcDream.App/Rendering/ClipPlaneSet.cs index 9a468fd0..dc4f0bd8 100644 --- a/src/AcDream.App/Rendering/ClipPlaneSet.cs +++ b/src/AcDream.App/Rendering/ClipPlaneSet.cs @@ -1,4 +1,4 @@ -// ClipPlaneSet.cs +// ClipPlaneSet.cs // // Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region, // in NDC) into a small set of clip-space half-space planes for the GPU's @@ -6,18 +6,18 @@ // convex plane set. // // This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and -// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e). +// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e). // Pure System.Numerics math; NO GL. The shader consumes each plane as -// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep) +// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep) // where (nx, ny, dw) = the plane's (normal.xy, offset). z is always 0 because a -// screen-space (NDC) edge is a vertical slab in clip space — independent of depth. +// screen-space (NDC) edge is a vertical slab in clip space — independent of depth. // // === The convexity rule (read before touching this file) ===================== // gl_ClipDistance planes are a CONJUNCTION of half-spaces, i.e. exactly ONE // convex region (their intersection). A CellView with MORE THAN ONE polygon is a // UNION of convex regions, which is in general NOT convex and CANNOT be // represented by one plane set. Emitting just the first/largest polygon's planes -// would clip away the others → a real visibility bug (under-inclusion). +// would clip away the others → a real visibility bug (under-inclusion). // // Therefore From() NEVER emits a single polygon's planes when the CellView holds // several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor: @@ -27,13 +27,13 @@ // // === The three Count==0 states (how a consumer tells them apart) ============= // Count == 0 can mean three different things; the consumer MUST distinguish: -// (a) Empty — IsNothingVisible == true, UseScissorFallback == false. -// The cell/region isn't visible at all → DRAW NOTHING. The +// (a) Empty — IsNothingVisible == true, UseScissorFallback == false. +// The cell/region isn't visible at all → DRAW NOTHING. The // ScissorNdcAabb is a degenerate inverted box (min > max) so that a // consumer which naively scissors on it still draws nothing. -// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false. +// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false. // The convex-plane budget was exceeded (multi-polygon or >8 edges) -// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle). +// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle). // There is no third Count==0 state produced by From(). (A separate "no-clip, // pass-all" slot 0 is constructed by the consumer directly, not via From().) // When Count > 0, Planes carries the convex gate and the scissor fields are unused. @@ -45,26 +45,26 @@ using System.Numerics; namespace AcDream.App.Rendering; /// -/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a +/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a /// scissor AABB fallback. See the file header for the convexity rule and the three /// Count==0 states. /// -internal readonly struct ClipPlaneSet +public readonly struct ClipPlaneSet { // Max simultaneous hardware clip planes we target (GL guarantees >= 8). private const int MaxPlanes = 8; // Collinear-edge merge threshold. Two consecutive edge directions are treated as - // the same edge when the turn between them is below ~0.5° (retail copy_view does a - // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265. + // the same edge when the turn between them is below ~0.5° (retail copy_view does a + // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265. private const float CollinearSinEps = 0.0087265f; - // Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate + // Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate // or near-duplicate point that would otherwise yield a garbage normalized normal. private const float DegenerateEdgeLen = 1e-6f; // A polygon whose absolute signed area (full area, not 2x) falls below this is a line - // or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far + // or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far // above this (e.g. the sliver-clip test region is 0.4); only an edge-on projection gets here. private const float MinPolygonArea = 1e-7f; private readonly Vector4[] _planes; @@ -77,7 +77,7 @@ internal readonly struct ClipPlaneSet ScissorNdcAabb = scissorNdcAabb; } - /// Number of active clip planes, 0..8. 0 ⇒ inspect + /// Number of active clip planes, 0..8. 0 ⇒ inspect /// and to decide between "draw the AABB" and "draw nothing". public int Count => _planes?.Length ?? 0; @@ -89,11 +89,11 @@ internal readonly struct ClipPlaneSet // its frame-scoped slice instead of cloning every plane payload a second time. internal Vector4[] PlaneArray => _planes ?? Array.Empty(); - /// True ⇒ the convex-plane budget was exceeded; gate on + /// True ⇒ the convex-plane budget was exceeded; gate on /// instead (draw the box). Always false when > 0 or when the region is empty. public bool UseScissorFallback { get; } - /// True ⇒ the region is not visible at all; the consumer draws NOTHING. + /// True ⇒ the region is not visible at all; the consumer draws NOTHING. /// Mutually exclusive with , and only meaningful when Count == 0. public bool IsNothingVisible { get; } @@ -106,20 +106,20 @@ internal readonly struct ClipPlaneSet public static ClipPlaneSet Empty { get; } = new(Array.Empty(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb); - // Inverted box (min > max) — any sane AABB intersection against it is empty. + // Inverted box (min > max) — any sane AABB intersection against it is empty. private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f); /// - /// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges - /// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor; - /// empty/degenerate → . See the file header for the full rule. + /// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges + /// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor; + /// empty/degenerate → . See the file header for the full rule. /// public static ClipPlaneSet From(CellView region) { if (region is null || region.IsEmpty || region.Polygons.Count == 0) return Empty; - // MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes. + // MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes. // Over-include via the union AABB (safe). region.Min/Max already track the union. if (region.Polygons.Count > 1) return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY); @@ -152,15 +152,15 @@ internal readonly struct ClipPlaneSet { int count = NormalizeAndMerge(input, verts); - // Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no + // Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no // meaningful AABB to over-include (a zero-area region), so treat it as nothing visible. if (count < 3) return Empty; ReadOnlySpan normalized = verts[..count]; - // A single convex polygon with too many edges to fit the hardware budget ⇒ scissor - // on ITS own AABB (still a superset of the polygon → over-include, safe). + // A single convex polygon with too many edges to fit the hardware budget ⇒ scissor + // on ITS own AABB (still a superset of the polygon → over-include, safe). if (count > MaxPlanes) return Scissor(normalized); @@ -173,10 +173,10 @@ internal readonly struct ClipPlaneSet Vector2 q = normalized[(i + 1) % count]; Vector2 dir = q - p; // Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's - // interior (the "left" side of the directed edge p→q). + // interior (the "left" side of the directed edge p→q). Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X)); - // Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w: - // dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep) + // Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w: + // dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep) planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p)); } return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb); @@ -233,9 +233,9 @@ internal readonly struct ClipPlaneSet if (SignedArea2(points[..count]) < 0f) points[..count].Reverse(); - // 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same - // way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new - // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal. + // 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same + // way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new + // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal. bool changed = true; while (changed && count >= 3) { @@ -260,12 +260,12 @@ internal readonly struct ClipPlaneSet d0 /= l0; d1 /= l1; - float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ - float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ + float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ + float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ if (dot > 0f && MathF.Abs(cross) < CollinearSinEps) { points[(i + 1)..count].CopyTo(points[i..]); - count--; // cur lies on the straight line prev→next + count--; // cur lies on the straight line prev→next changed = true; break; } @@ -276,7 +276,7 @@ internal readonly struct ClipPlaneSet return 0; // Final degeneracy gate: a polygon with negligible area is a line/point even if it still - // has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5° + // has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5° // merge didn't quite collapse). Emitting its planes would yield an empty half-space // intersection that silently gates out everything; report it honestly as nothing-visible. if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea) @@ -285,7 +285,7 @@ internal readonly struct ClipPlaneSet return count; } - // Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW. + // Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW. private static float SignedArea2(ReadOnlySpan poly) { float a = 0f; diff --git a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs index 713577ac..cb9899a8 100644 --- a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs +++ b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Textures; +using AcDream.Core.Textures; using AcDream.Core.World; using Silk.NET.OpenGL; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// Location of one decoded entity-material composite in a resident bindless /// texture array. The modern mesh shader consumes this exact pair. /// -internal readonly record struct BindlessTextureLocation(ulong Handle, uint Layer); +public readonly record struct BindlessTextureLocation(ulong Handle, uint Layer); internal enum CompositeTextureKind : byte { diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs index 7828a97f..1cecd9ef 100644 --- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs +++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs @@ -114,18 +114,15 @@ internal sealed class RetailCreatureAppraisalFrameView : private readonly UiViewport _viewport; private readonly UiElement _windowFrame; private readonly AppraisalUiController _controller; - private readonly ExternalViewportTextureBridge _textureBridge; public RetailCreatureAppraisalFrameView( UiViewport viewport, UiElement windowFrame, - AppraisalUiController controller, - IGpuDevice gpuDevice) + AppraisalUiController controller) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame)); _controller = controller ?? throw new ArgumentNullException(nameof(controller)); - _textureBridge = new ExternalViewportTextureBridge(gpuDevice); } public bool TryGetVisibleTarget( @@ -152,7 +149,7 @@ internal sealed class RetailCreatureAppraisalFrameView : } public void SetTextureHandle(uint textureHandle) => - _viewport.TextureHandle = _textureBridge.Resolve(textureHandle); + _viewport.TextureHandle = textureHandle; private static bool IsEffectivelyVisible(UiElement element) { diff --git a/src/AcDream.App/Rendering/DebugLineRenderer.cs b/src/AcDream.App/Rendering/DebugLineRenderer.cs index d84bb6e6..7f883a78 100644 --- a/src/AcDream.App/Rendering/DebugLineRenderer.cs +++ b/src/AcDream.App/Rendering/DebugLineRenderer.cs @@ -1,56 +1,94 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// -/// Minimal debug line renderer for visualizing collision shapes, +/// Minimal GL debug line renderer for visualizing collision shapes, /// bounding boxes, and other debug geometry. Collect lines each frame /// via / , then call -/// to upload + draw them through the current -/// . +/// to upload + draw them. /// -/// Campaign V slice V4a: ported onto . Owns one -/// pipeline (LINE_LIST topology, depth disabled — lines must show through -/// geometry, matching the prior explicit DepthTest disable) and draws -/// with the frame's ring rather than a persistent respecialized VBO. Vertex -/// format is (vec3 pos, vec3 color) = 24 bytes per vertex, unchanged. +/// Uses a single shared VBO that's respecialized each frame. Vertex +/// format is (vec3 pos, vec3 color) = 24 bytes per vertex. /// -internal sealed class DebugLineRenderer : IDisposable +public sealed unsafe class DebugLineRenderer : IDisposable { - private readonly IGpuDevice _device; - private readonly IGpuPipeline _pipeline; - - private static readonly GpuVertexLayout VertexLayout = new( - StrideBytes: 24, - [ - new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0), - new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12), - ]); + private readonly GL _gl; + private readonly Shader _shader; + private readonly uint _vao; + private readonly uint _vbo; + private readonly ResourceCleanupGroup _resources; private readonly List _buffer = new(4096); private int _vertexCount; + private int _capacityBytes; - public DebugLineRenderer(IGpuDevice device) + public DebugLineRenderer(GL gl, string shaderDir) { - _device = device ?? throw new ArgumentNullException(nameof(device)); - _pipeline = _device.CreatePipeline(new GpuPipelineDescription + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir); + var resources = new ResourceCleanupGroup(); + Shader? shader = null; + uint vao = 0; + uint vbo = 0; + try { - Name = "debug-line", - Shaders = new GpuShaderSet("debug_line"), - VertexLayout = VertexLayout, - Topology = GpuPrimitiveTopology.LineList, - Blend = GpuBlendMode.None, - // Retail debug lines are drawn visible THROUGH geometry — the - // prior GL path captured+disabled DepthTest around the draw and - // restored whatever the caller had before. A dedicated pipeline - // bakes "always visible" directly, which is simpler and exactly - // as behaviour-preserving since nothing else shares this pipeline. - Depth = GpuDepthState.Disabled, - Cull = GpuCullMode.None, - ColorWrite = true, - }); + shader = new Shader(gl, + Path.Combine(shaderDir, "debug_line.vert"), + Path.Combine(shaderDir, "debug_line.frag")); + resources.Add("debug-line shader", shader.Dispose); + vao = GlResourceCommand.CreateName( + gl, + "debug-line VAO", + gl.GenVertexArray, + gl.DeleteVertexArray); + uint ownedVao = vao; + resources.Add( + "debug-line VAO", + () => GlResourceCommand.DeleteVertexArray( + gl, + ownedVao, + $"delete debug-line VAO {ownedVao}")); + vbo = GlResourceCommand.CreateName( + gl, + "debug-line VBO", + gl.GenBuffer, + gl.DeleteBuffer); + uint ownedVbo = vbo; + resources.Add( + "debug-line VBO", + () => GlResourceCommand.DeleteBuffer( + gl, + ownedVbo, + $"delete debug-line VBO {ownedVbo}")); + + GlResourceCommand.Execute(gl, "configure debug-line vertex state", () => + { + gl.BindVertexArray(vao); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); + // 24-byte stride: vec3 pos + vec3 color + gl.EnableVertexAttribArray(0); + gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0); + gl.EnableVertexAttribArray(1); + gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float))); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + gl.BindVertexArray(0); + }); + } + catch (Exception constructionFailure) + { + resources.RollbackConstructionAndThrow( + "DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.", + constructionFailure); + } + + _resources = resources; + _shader = shader!; + _vao = vao; + _vbo = vbo; } /// Clear accumulated lines. Call at the start of each frame. @@ -131,40 +169,45 @@ internal sealed class DebugLineRenderer : IDisposable AddLine(c[2], c[6], color); AddLine(c[3], c[7], color); } - /// Upload + draw all accumulated lines against the current frame. - public void Flush(Matrix4x4 view, Matrix4x4 projection, IGpuFrame frame) + /// Upload + draw all accumulated lines. + public void Flush(Matrix4x4 view, Matrix4x4 projection) { if (_vertexCount == 0) return; - ArgumentNullException.ThrowIfNull(frame); - int byteCount = _buffer.Count * sizeof(float); - GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex); - CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan()); + _shader.Use(); + _shader.SetMatrix4("uView", view); + _shader.SetMatrix4("uProjection", projection); - using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription + _gl.BindVertexArray(_vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + + int neededBytes = _buffer.Count * sizeof(float); + if (neededBytes > _capacityBytes) { - Name = "debug-lines", - Color = new GpuColorAttachment( - Target: null, - Load: GpuLoadOp.Load, - Store: GpuStoreOp.Store, - ClearColor: default), - Depth = null, - SampleCount = 1, - }); - pass.BindPipeline(_pipeline); - pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes); - // Same combined-matrix convention every other ported shader uses - // (WbDrawDispatcher, TerrainModernRenderer, ParticleRenderer): - // C# multiplies view * projection once and uploads the single - // uViewProjection the shader now declares, replacing the separate - // uView/uProjection uniforms. - pass.SetPushConstants(GpuPushConstants.Default with { ViewProjection = view * projection }); - pass.Draw((uint)_vertexCount, instanceCount: 1, firstVertex: 0, firstInstance: 0); + fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer)) + _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw); + _capacityBytes = neededBytes; + } + else + { + fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer)) + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr); + } + + // Depth test on so lines get occluded by geometry (but we want them + // visible through geometry — disable depth test so everything shows). + bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest); + _gl.Disable(EnableCap.DepthTest); + + _gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount); + + if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest); + + _gl.BindVertexArray(0); } public void Dispose() { - _pipeline.Dispose(); + _resources.RetryCleanup(); } } diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs index 6c13f234..3a37cdb1 100644 --- a/src/AcDream.App/Rendering/DollCamera.cs +++ b/src/AcDream.App/Rendering/DollCamera.cs @@ -1,26 +1,26 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.Rendering; /// -/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport -/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by +/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport +/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by /// UIElement_Viewport::SetCamera(position, direction) with: /// -/// position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae] -/// direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to +/// position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae] +/// direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to /// IDENTITY (euler_set_rotate(0,0,0) then rotate(0,0,0)), so the camera looks -/// straight down +Y with +Z up — NO yaw, NO pitch. +/// straight down +Y with +Z up — NO yaw, NO pitch. /// /// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with -/// a Ï€/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m -/// figure even though the model origin sits at the FEET (z=0). FOV Ï€/4 is CreatureMode's default +/// a π/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m +/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is CreatureMode's default /// m_fFOVRadians (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the /// paperdoll uses UseSharpMode (not SmartboxFOV) so Render::SetFOVRad(m_fFOVRadians) applies. /// /// An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a -/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the +/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the /// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height + /// FOV, not from aiming. /// @@ -28,15 +28,15 @@ namespace AcDream.App.Rendering; /// convention as so the doll's triangle winding + back-face culling match the /// world render pass. AC up-axis = +Z. /// -internal sealed class DollCamera : ICamera +public sealed class DollCamera : ICamera { - // Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61). + // Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61). internal static readonly Vector3 RetailEye = new(0.12f, -2.4f, 0.88f); - // Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0). + // Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0). private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f); private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera - /// Vertical field of view — retail CreatureMode default m_fFOVRadians = Ï€/4 (45°). + /// Vertical field of view — retail CreatureMode default m_fFOVRadians = π/4 (45°). public float FovRadians { get; set; } = MathF.PI / 4f; public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear diff --git a/src/AcDream.App/Rendering/DollEntityBuilder.cs b/src/AcDream.App/Rendering/DollEntityBuilder.cs index eb562ba7..3d64ccb3 100644 --- a/src/AcDream.App/Rendering/DollEntityBuilder.cs +++ b/src/AcDream.App/Rendering/DollEntityBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.World; @@ -6,13 +6,13 @@ using AcDream.Core.World; namespace AcDream.App.Rendering; /// -/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player) +/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player) /// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays /// + part overrides), posed at the scene origin facing the viewer. /// /// /// The palette / part-override mapping mirrors the inline construction in -/// GameWindow.cs around lines 3390–3431. Extracted here so it is +/// GameWindow.cs around lines 3390–3431. Extracted here so it is /// unit-testable without dats and so the paperdoll renderer owns a clean /// seam: it calls with fresh player state each time the /// ObjDesc changes, and the renderer only has to swap the entity into its @@ -26,7 +26,7 @@ namespace AcDream.App.Rendering; /// server-assigned guid. /// /// -internal static class DollEntityBuilder +public static class DollEntityBuilder { /// /// Reserved synthetic guid for the paperdoll clone. High, deliberately @@ -38,18 +38,18 @@ internal static class DollEntityBuilder /// Reserved render-local entity id for the doll. FIXED (the doll is a singleton) /// and high enough never to collide with LiveEntityRuntime's local ids. The /// renderer passes this in animatedEntityIds so the dispatcher treats the - /// doll as animated — which BYPASSES the Tier-1 classification cache + /// doll as animated — which BYPASSES the Tier-1 classification cache /// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW /// WorldEntity with this SAME id; without the cache bypass the dispatcher would /// serve the previous doll's cached batches and the new gear wouldn't appear. /// public const uint DollRenderId = 0xDA11_D012u; - // retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h) - // (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC + // retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h) + // (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC // North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about - // +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is - // NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer). + // +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is + // NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer). private const float _headingDegrees = 191.367905f; private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f); private static readonly Quaternion _dollRotation = @@ -62,17 +62,17 @@ internal static class DollEntityBuilder /// Pre-resolved mesh refs (may be empty; caller fills them in). /// /// ObjDesc base palette id (0x04xxxxxx). Passed only when subpalettes are - /// also present — mirrors GameWindow which only builds a PaletteOverride + /// also present — mirrors GameWindow which only builds a PaletteOverride /// when SubPalettes.Count > 0. /// /// /// Subpalette overlays from the server ObjDesc. Each tuple carries the /// subpalette dat id, byte offset into the base palette, and byte length. - /// Null or empty → PaletteOverride on the returned entity is null. + /// Null or empty → PaletteOverride on the returned entity is null. /// /// /// AnimPartChange swaps from the server ObjDesc. Each tuple is a - /// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array. + /// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array. /// public static WorldEntity Build( uint setupId, @@ -82,7 +82,7 @@ internal static class DollEntityBuilder IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null) { // --- palette override (mirrors GameWindow:3395-3405) --- - // Only build when there are sub-palette overlays — same gate as GameWindow. + // Only build when there are sub-palette overlays — same gate as GameWindow. PaletteOverride? paletteOverride = null; if (subPalettes is { Count: > 0 } spList) { diff --git a/src/AcDream.App/Rendering/EquippedChildRenderController.cs b/src/AcDream.App/Rendering/EquippedChildRenderController.cs index efb1b568..e408afa8 100644 --- a/src/AcDream.App/Rendering/EquippedChildRenderController.cs +++ b/src/AcDream.App/Rendering/EquippedChildRenderController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.App.Rendering.Vfx; using AcDream.Core.Items; @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering; /// and recomposes it after the parent's animation /// advances each frame. /// -internal sealed class EquippedChildRenderController : IDisposable +public sealed class EquippedChildRenderController : IDisposable { private readonly IDatReaderWriter _dats; private readonly object _datLock; @@ -1674,7 +1674,7 @@ internal sealed class EquippedChildRenderController : IDisposable "has no exact projection key."); } -internal enum ChildUnparentDisposition +public enum ChildUnparentDisposition { NotAttached, Completed, diff --git a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs b/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs deleted file mode 100644 index 1be7e29c..00000000 --- a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace AcDream.App.Rendering; - -/// -/// Campaign V slice V4a bridge: memoizes a for the -/// most recent raw GL colour-texture name a still-unmigrated private-viewport -/// renderer (, -/// ) produced, so -/// UiViewport.TextureHandle — now a — has -/// something to draw. Those renderers still allocate their FBO colour -/// attachment directly on GL (V4g's scope: "PrivateEntityViewportRenderer -/// → IGpuRenderTarget"), so this bridge — not their own campaign slice — is -/// what lets the RETAINED UI side of the seam move onto the RHI now. -/// -/// Registers lazily and only re-registers when the producer hands back a -/// DIFFERENT raw name (the FBO's colour texture is stable across frames at a -/// fixed viewport size and only regenerates on resize) — a naive -/// register-every-frame would exhaust the 16384-slot table in seconds. -/// Deleted at slice V4g once those renderers publish a real -/// directly. -/// -internal sealed class ExternalViewportTextureBridge -{ - private readonly IGpuDevice _device; - private readonly IGpuSampler _sampler; - private uint _lastRawName; - private GpuTextureSlot _lastSlot = GpuTextureSlot.Unassigned; - - public ExternalViewportTextureBridge(IGpuDevice device) - { - _device = device ?? throw new ArgumentNullException(nameof(device)); - // Matches the FBO colour attachment's own fixed GL_LINEAR / CLAMP_TO_EDGE - // parameters (PrivateEntityViewportRenderer.EnsureFramebuffer, - // PaperdollViewportRenderer's equivalent): a bindless handle's - // filtering comes from the bound SAMPLER object, not the texture's - // own — now irrelevant — TEXTURE_MIN_FILTER/TEXTURE_MAG_FILTER. - _sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp); - } - - /// - /// Resolves (0 = nothing rendered - /// this call, matching the producers' existing "0 = no texture" return) - /// to a texture-table slot, registering it once per distinct name. - /// - public GpuTextureSlot Resolve(uint rawGlColorTextureName) - { - if (rawGlColorTextureName == 0) - { - Release(); - return GpuTextureSlot.Unassigned; - } - if (rawGlColorTextureName == _lastRawName && _lastSlot.IsAssigned) - return _lastSlot; - - Release(); - if (_device is not Gpu.Gl.GlGpuDevice glDevice) - { - throw new NotSupportedException( - "ExternalViewportTextureBridge only supports the GL backend. " + - "Slice V4g removes this bridge before any other backend ships."); - } - _lastSlot = glDevice.RegisterExternalColorTexture(rawGlColorTextureName, _sampler); - _lastRawName = rawGlColorTextureName; - return _lastSlot; - } - - private void Release() - { - if (_lastSlot.IsAssigned) - _device.ReleaseTextureSlot(_lastSlot); - _lastSlot = GpuTextureSlot.Unassigned; - _lastRawName = 0; - } -} diff --git a/src/AcDream.App/Rendering/FlyCamera.cs b/src/AcDream.App/Rendering/FlyCamera.cs index 39cb0f76..49a5341b 100644 --- a/src/AcDream.App/Rendering/FlyCamera.cs +++ b/src/AcDream.App/Rendering/FlyCamera.cs @@ -1,9 +1,9 @@ -// src/AcDream.App/Rendering/FlyCamera.cs +// src/AcDream.App/Rendering/FlyCamera.cs using System.Numerics; namespace AcDream.App.Rendering; -internal sealed class FlyCamera : ICamera +public sealed class FlyCamera : ICamera { public Vector3 Position { get; set; } = new(96, 96, 150); public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y diff --git a/src/AcDream.App/Rendering/FrustumCuller.cs b/src/AcDream.App/Rendering/FrustumCuller.cs index 39cbef0b..3c792bbb 100644 --- a/src/AcDream.App/Rendering/FrustumCuller.cs +++ b/src/AcDream.App/Rendering/FrustumCuller.cs @@ -1,13 +1,13 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// -/// Six normalized view-frustum planes extracted from a View×Projection matrix. +/// Six normalized view-frustum planes extracted from a View×Projection matrix. /// Each plane is represented as (normal.X, normal.Y, normal.Z, distance) where /// dot(normal, point) + distance >= 0 means the point is on the visible side. /// -internal readonly struct FrustumPlanes +public readonly struct FrustumPlanes { public readonly Vector4 Left; public readonly Vector4 Right; @@ -27,7 +27,7 @@ internal readonly struct FrustumPlanes } /// - /// Extracts the six frustum planes from a combined View×Projection matrix + /// Extracts the six frustum planes from a combined View×Projection matrix /// using the Gribb-Hartmann method. System.Numerics.Matrix4x4 is row-major, /// so rows are accessed directly via M{row}{col} fields. /// @@ -64,7 +64,7 @@ internal readonly struct FrustumPlanes /// /// Conservative AABB-vs-frustum culling. Zero allocations; suitable for per-frame use. /// -internal static class FrustumCuller +public static class FrustumCuller { /// /// Returns true if the axis-aligned bounding box defined by @@ -74,7 +74,7 @@ internal static class FrustumCuller /// public static bool IsAabbVisible(FrustumPlanes planes, Vector3 min, Vector3 max) { - // For each plane, test the AABB's "most-positive vertex" — + // For each plane, test the AABB's "most-positive vertex" — // the corner most in the direction of the plane normal. If // that corner is behind the plane, the entire box is outside. return TestPlane(planes.Left, min, max) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a27f71fe..a638ba48 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Plugins; +using AcDream.Core.Plugins; using AcDream.App.Composition; using AcDream.App.Physics; using AcDream.App.Rendering.Gpu; @@ -21,7 +21,7 @@ using Silk.NET.Windowing; namespace AcDream.App.Rendering; -internal sealed class GameWindow : +public sealed class GameWindow : IDisposable, IGameWindowPlatformPublication, IGameWindowHostInputCameraPublication, @@ -70,7 +70,7 @@ internal sealed class GameWindow : 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 + /// support. Required at startup — missing bindless throws /// in OnLoad. private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport; private SamplerCache? _samplerCache; @@ -78,14 +78,14 @@ internal sealed class GameWindow : // 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. + // → 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 — + // 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; @@ -98,7 +98,7 @@ internal sealed class GameWindow : "1", System.StringComparison.Ordinal); - // MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call + // 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(); @@ -116,7 +116,6 @@ internal sealed class GameWindow : private IDisposable? _frameGraphPublication; private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights; private IGpuDevice? _gpuDevice; - private AcDream.App.Rendering.GpuDeviceFrameLifetime? _gpuFrameLifetime; private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameRenderResourceLifetime _renderResourceLifetime = new(); @@ -145,11 +144,11 @@ internal sealed class GameWindow : _localPlayerTeleport; private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange = new(nearRadius: 4, farRadius: 12); - // Phase B.3: physics engine — populated from the streaming pipeline. + // Phase B.3: physics engine — populated from the streaming pipeline. private AcDream.Core.Physics.PhysicsEngine _physicsEngine => _runtimeEntityObjects.Physics.Engine; - // Task 4: physics data cache — BSP trees + collision shapes extracted from + // 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 AcDream.Core.Physics.PhysicsDataCache _physicsDataCache => @@ -186,7 +185,7 @@ internal sealed class GameWindow : 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 — + // 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 @@ -227,7 +226,7 @@ internal sealed class GameWindow : // 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 + // 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. @@ -249,7 +248,7 @@ internal sealed class GameWindow : /// /// Tier 1 cache (#53): per-entity classification results for static /// entities (those NOT in ). Conceptually - /// paired with — that dictionary is the + /// 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 @@ -284,7 +283,7 @@ internal sealed class GameWindow : 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) + // 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. @@ -295,7 +294,7 @@ internal sealed class GameWindow : private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue; // 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 + // 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. @@ -363,21 +362,21 @@ internal sealed class GameWindow : /// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source). public IReadOnlyList Shortcuts => _runtimeInventory.Shortcuts.Items; - // Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from + // 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 + // Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment // buffs into the max formula via Spellbook.GetVitalMod. public AcDream.Core.Player.LocalPlayerState LocalPlayer => _runtimeCharacter.LocalPlayer; - // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1. + // 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. + // 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(); @@ -399,7 +398,7 @@ internal sealed class GameWindow : private AcDream.App.Spells.MagicRuntime? _magicRuntime; private 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 + // 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; @@ -407,7 +406,7 @@ internal sealed class GameWindow : _creatureAppraisalViewportRenderer; private AcDream.App.Rendering.CreatureAppraisalFramePresenter? _creatureAppraisalFramePresenter; - // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. + // 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 @@ -428,7 +427,7 @@ internal sealed class GameWindow : private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights; private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation; - // #188 — TransparentPartHook fires from the animation pipeline drive + // #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 @@ -473,10 +472,10 @@ internal sealed class GameWindow : _localPlayerAnimation; private AcDream.App.Physics.LocalPlayerShadowSynchronizer? _localPlayerShadowSynchronizer; - // Phase D.2b-C — live character-sheet assembly + raise flow (extracted + // 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 + // 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 @@ -485,7 +484,7 @@ internal sealed class GameWindow : // the bool here. private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry; - // Phase K.1b / Slice 8 F — one semantic input path. Transitional actions + // 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; @@ -507,7 +506,7 @@ internal sealed class GameWindow : // configuration path, // 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() + // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults() // should land in the GameWindow construction path. private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings; private readonly GraphicalHostPlatformServices _platformServices; @@ -523,7 +522,7 @@ internal sealed class GameWindow : } // Phase 4.7: optional live connection to an ACE server. Enabled only when - // ACDREAM_LIVE=1 is in the environment — fully backward compatible with + // ACDREAM_LIVE=1 is in the environment — fully backward compatible with // the offline rendering pipeline. // Runtime owns the canonical session generation and transport lifetime. // The window retains only the composition handles needed for startup and @@ -539,7 +538,7 @@ internal sealed class GameWindow : // 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 + /// 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; @@ -558,7 +557,7 @@ internal sealed class GameWindow : /// private IReadOnlyDictionary LastSpawns => _liveEntities?.Snapshots ?? EmptyLiveSpawnMap; - // R5-V2: the local player's CPhysicsObj stand-in — owns the player's + // 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. @@ -567,14 +566,14 @@ internal sealed class GameWindow : private EntityPhysicsHost? _playerHost => _playerHostSlot.Host; - // R5-V2: guid → per-entity IPhysicsObjHost registry (retail's + // 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). + // Slice 2a — the DR tick's stale-velocity anim-stop was its only user). public GameWindow( AcDream.App.RuntimeOptions options, WorldGameState worldGameState, @@ -685,7 +684,7 @@ internal sealed class GameWindow : var options = WindowOptions.Default with { Size = new Vector2D(1280, 720), - Title = "acdream — phase 1", + Title = "acdream — phase 1", API = new GraphicsAPI( ContextAPI.OpenGL, ContextProfile.Core, @@ -749,10 +748,6 @@ internal sealed class GameWindow : IGpuDevice value) => PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)"); - void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime( - GpuDeviceFrameLifetime value) => - PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime"); - void IGameWindowHostInputCameraPublication.PublishKeyboardSource( AcDream.App.Input.SilkKeyboardSource value) => PublishCompositionOwner(ref _kbSource, value, "keyboard source"); @@ -1308,7 +1303,6 @@ internal sealed class GameWindow : _worldEnvironment, _renderResourceLifetime, _gpuFrameFlights!, - _gpuDevice!, _options.ResidencyBudgets, initialCenterLandblockId, _applicationPaths.DiagnosticsDirectory, @@ -1328,10 +1322,9 @@ internal sealed class GameWindow : new InteractionRetainedUiDependencies( _options, platformResult.Graphics, - hostInputCamera.GpuDevice, - () => hostInputCamera.GpuFrameLifetime.Current, _window!, platformResult.Input, + worldRender.Foundation.ShadersDirectory, contentEffectsAudio.Dats, _datLock, worldRender.Foundation.TextureCache, @@ -1381,7 +1374,6 @@ internal sealed class GameWindow : new LivePresentationDependencies( _options, platformResult.Graphics, - _gpuDevice!, _window!, _datLock, _runtimeSettings, @@ -1549,7 +1541,7 @@ internal sealed class GameWindow : _frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt)); } - // Performance overlay state — updated every ~0.5s and written to the + // 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) { @@ -1565,20 +1557,20 @@ internal sealed class GameWindow : // 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 + // 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 + // 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 — + // 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 + /// 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 diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs index 3a681b41..b416b597 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs @@ -190,26 +190,9 @@ internal sealed class GlGpuDevice : IGpuDevice string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag"); string vertexSource = File.ReadAllText(vertexPath); string fragmentSource = File.ReadAllText(fragmentPath); - - // Every RHI-created pipeline gets the same common.glsl preamble the - // legacy Shader(gl, vert, frag, includeCommonPreamble: true) call - // sites already opt into (mesh_modern, terrain_modern): the set-0 - // binding-9 texture-table buffer + ACDREAM_TEXTURE_HANDLE macro that - // slice V2 introduced. A pipeline whose shaders never reference the - // macro simply carries an unused SSBO declaration — harmless — so - // there is no reason for callers to opt in per pipeline. - string commonSource = CommonPreambleSource; - vertexSource = Shader.InjectPreamble(vertexSource, commonSource); - fragmentSource = Shader.InjectPreamble(fragmentSource, commonSource); - return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource); } - private string? _commonPreambleSource; - - private string CommonPreambleSource => _commonPreambleSource ??= - File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl")); - public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) { ThrowIfDisposed(); @@ -232,33 +215,6 @@ internal sealed class GlGpuDevice : IGpuDevice return new GpuTextureSlot(slot); } - /// - /// Registers an externally-created, externally-owned GL texture name - /// (a private-viewport FBO colour attachment) into the same texture table - /// uses. Not part of - /// — PrivateEntityViewportRenderer/PaperdollViewportRenderer - /// still allocate their own FBO textures directly on GL pre-V4g, so - /// their retained-UI consumers () need a bridge - /// that does not require the caller to own an . - /// The caller keeps deleting the GL name itself; this only ever writes - /// the bindless handle into the table and returns the slot. Deleted with - /// the GL backend once slice V4g moves those renderers onto - /// and they can call - /// like every other consumer. - /// - internal GpuTextureSlot RegisterExternalColorTexture(uint glTextureName, IGpuSampler sampler) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(sampler); - if (sampler is not GlGpuSampler glSampler) - throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler)); - - uint slot = _textureSlotAllocator.Allocate(); - ulong handle = _bindless.GetResidentHandle(glTextureName, glSampler.GlName); - WriteHandle(slot, handle); - return new GpuTextureSlot(slot); - } - public void ReleaseTextureSlot(GpuTextureSlot slot) { ThrowIfDisposed(); @@ -283,17 +239,6 @@ internal sealed class GlGpuDevice : IGpuDevice long serial = ++_nextSerial; int slot = _frameFlights.CurrentSlot; _ringStates[slot].Reset(); - // Campaign V slice V4a: during the migration, every still-legacy - // renderer (WbDrawDispatcher, terrain, particles, EnvCellRenderer, ...) - // mutates real GL program/blend/depth/cull state directly, outside this - // device's ApplyRenderState. _renderState has no way to observe those - // calls, so its cached "current" snapshot goes stale the moment any of - // them runs between two RHI binds. Resetting once per frame is the same - // defensive move BeginPass already makes after a forced clear (see the - // comment there) — it costs one redundant state application on the - // frame's first BindPipeline, in exchange for never diffing against a - // baseline the driver has since moved past. - _renderState.Reset(); return new GlGpuFrame(this, slot, serial); } @@ -338,17 +283,6 @@ internal sealed class GlGpuDevice : IGpuDevice _textureHandleTable.AsSpan(tableStart, tableEnd - tableStart)); _textureTableBuffer.Upload((long)tableStart * sizeof(ulong), bytes); } - - // Bind the device's own table to binding 9 immediately before every - // RHI-issued draw. Storage-buffer bindings are global GL context - // state, and the still-unmigrated legacy renderers (WbDrawDispatcher, - // EnvCellRenderer, TerrainModernRenderer, ParticleRenderer) rebind - // their OWN interim per-renderer table to the same binding right - // before their own draws (slice V2's documented pattern) — so this - // device must re-claim binding 9 before ITS draws too, or an RHI - // draw issued after a legacy draw in the same frame would read the - // wrong table. - _gl.BindBufferBase(GLEnum.ShaderStorageBuffer, GpuBindingModel.StorageTextureTable, _textureTableBuffer.GlName); } internal void BeginPass(GpuPassDescription description) @@ -461,24 +395,6 @@ internal sealed class GlGpuDevice : IGpuDevice { _gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite); } - if (changes.Multisample) - { - // Vulkan bakes multisample rasterization into the pipeline via its - // declared sample count — there is no separate enable bit. The GL - // backend mirrors that: GL_MULTISAMPLE tracks - // GpuPipelineDescription.SampleCount rather than being left at - // whatever the previous pass happened to leave it. This matters - // because the default framebuffer can itself be multisampled - // (QualitySettings MSAA): a single-sample UI pipeline binding - // while GL_MULTISAMPLE is still enabled from an earlier world - // pass converts each glyph's soft alpha edge into dithered MSAA - // coverage instead of a clean alpha blend (the pre-RHI - // TextRenderer explicitly disabled it for the same reason). - if (desired.Multisample) - _gl.Enable(EnableCap.Multisample); - else - _gl.Disable(EnableCap.Multisample); - } GLHelpers.ThrowOnResourceError(_gl, "apply GL render state"); } diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs index 819f8e6d..f3ac77c8 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs @@ -51,8 +51,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder description.Cull, description.FrontFace, description.AlphaToCoverage, - description.ColorWrite, - Multisample: description.SampleCount > 1); + description.ColorWrite); _device.ApplyRenderState(desired); _gl.BindVertexArray(p.GlVertexArray); diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs index 5414a9a5..846f8d35 100644 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs +++ b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs @@ -15,8 +15,7 @@ internal readonly record struct GlRenderStateSnapshot( GpuCullMode Cull, GpuFrontFace FrontFace, bool AlphaToCoverage, - bool ColorWrite, - bool Multisample); + bool ColorWrite); /// Which GL state calls are needed to move from the previous snapshot to the new one. internal readonly record struct GlRenderStateChanges( @@ -28,15 +27,14 @@ internal readonly record struct GlRenderStateChanges( bool Cull, bool FrontFace, bool AlphaToCoverage, - bool ColorWrite, - bool Multisample) + bool ColorWrite) { public bool AnyChange => Program || Blend || DepthTest || DepthWrite || DepthCompare - || Cull || FrontFace || AlphaToCoverage || ColorWrite || Multisample; + || Cull || FrontFace || AlphaToCoverage || ColorWrite; /// Every dimension reported changed — used for the first apply after a reset. - internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true, true); + internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true); } /// @@ -70,8 +68,7 @@ internal sealed class GlRenderStateCache p.Cull != desired.Cull, p.FrontFace != desired.FrontFace, p.AlphaToCoverage != desired.AlphaToCoverage, - p.ColorWrite != desired.ColorWrite, - p.Multisample != desired.Multisample); + p.ColorWrite != desired.ColorWrite); } /// Discards the cached baseline — the next reports every dimension changed. diff --git a/src/AcDream.App/Rendering/ICamera.cs b/src/AcDream.App/Rendering/ICamera.cs index 16c3a237..3aeaf987 100644 --- a/src/AcDream.App/Rendering/ICamera.cs +++ b/src/AcDream.App/Rendering/ICamera.cs @@ -1,8 +1,8 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; -internal interface ICamera +public interface ICamera { Matrix4x4 View { get; } Matrix4x4 Projection { get; } diff --git a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs index de609d66..2ad67136 100644 --- a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs +++ b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs @@ -1,14 +1,14 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// /// Result of a camera spring-arm sweep: the collided eye position AND the cell the swept /// viewer-sphere ended in (retail viewer_cell = sphere_path.curr_cell, update_viewer -/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it +/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it /// is the robust per-frame "which cell is the camera in?" answer that roots the render. /// -internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId); +public readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId); /// /// Sweeps a small sphere from the camera pivot (player head) toward the @@ -16,7 +16,7 @@ internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId /// lets collide its eye without depending on /// the physics engine directly (and stay unit-testable with a fake). /// -internal interface ICameraCollisionProbe +public interface ICameraCollisionProbe { /// /// Roll a collision sphere from to diff --git a/src/AcDream.App/Rendering/IndoorDrawPlan.cs b/src/AcDream.App/Rendering/IndoorDrawPlan.cs index 6696f09a..a3aab2a2 100644 --- a/src/AcDream.App/Rendering/IndoorDrawPlan.cs +++ b/src/AcDream.App/Rendering/IndoorDrawPlan.cs @@ -1,18 +1,18 @@ -// IndoorDrawPlan.cs +// IndoorDrawPlan.cs // // Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840): // the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a -// non-empty view is included — there is NO "drawable" filter. Dropping cells without a -// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). +// non-empty view is included — there is NO "drawable" filter. Dropping cells without a +// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). using System.Collections.Generic; namespace AcDream.App.Rendering; -internal readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); +public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); -internal static class IndoorDrawPlan +public static class IndoorDrawPlan { - /// Reverse OrderedVisibleCells (far→near), each visible cell with its view + /// Reverse OrderedVisibleCells (far→near), each visible cell with its view /// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped /// (they are not actually visible); no other cell is ever dropped. public static List ShellPass(PortalVisibilityFrame frame) diff --git a/src/AcDream.App/Rendering/InteriorEntityPartition.cs b/src/AcDream.App/Rendering/InteriorEntityPartition.cs index ad57505e..08da6b15 100644 --- a/src/AcDream.App/Rendering/InteriorEntityPartition.cs +++ b/src/AcDream.App/Rendering/InteriorEntityPartition.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using AcDream.Core.World; @@ -8,27 +8,27 @@ namespace AcDream.App.Rendering; /// Splits a frame's landblock entities into the draw buckets used by the /// retail-style DrawInside flood. /// -/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the +/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the /// frame draws STATIC world first (terrain, building shells, scenery, then /// flooded interior cells + their static object lists), and every DYNAMIC /// (server-spawned: player, NPCs, doors, items) draws LAST, depth-tested, -/// never hard-clipped. This is what makes the aperture depth punch safe — +/// never hard-clipped. This is what makes the aperture depth punch safe — /// when the punch erases depth inside a doorway, no dynamic has been drawn /// yet, so nothing visible is destroyed (retail: objects draw per cell AFTER /// cells, PView::DrawCells epilogue Ghidra 0x005a4840; the first BR-2 attempt /// punched after dynamics and erased the player, reverted 88be519). /// /// -/// — indoor STATICS (dat-baked, ServerGuid==0) +/// — indoor STATICS (dat-baked, ServerGuid==0) /// per visible cell, drawn with their cell. -/// — outdoor statics (building +/// — outdoor statics (building /// shells, scenery stabs), drawn with the world/landscape pass. -/// — ALL server-spawned entities +/// — ALL server-spawned entities /// (ServerGuid != 0) regardless of cell, plus unresolved-cell live entities; /// drawn in the frame's single LAST entity pass. /// /// -internal static class InteriorEntityPartition +public static class InteriorEntityPartition { internal enum ProjectionClass : byte { @@ -51,13 +51,13 @@ internal static class InteriorEntityPartition void AbortFrame(); } - internal sealed class Result + public sealed class Result { public Dictionary> ByCell { get; } = new(); public List OutdoorStatic { get; } = new(); public List Dynamics { get; } = new(); - // MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames + // MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames // so pruning itself doesn't allocate. private readonly List _emptyCellScratch = new(); @@ -82,7 +82,7 @@ internal static class InteriorEntityPartition /// entries (either newly emptied, or a leftover key from a previous /// frame's visible-cell set that this frame never touched). Keeps /// ByCell.Count / .Keys bit-identical to the old always-fresh- - /// Dictionary behavior — callers that inspect key presence/count + /// Dictionary behavior — callers that inspect key presence/count /// directly (not just TryGetValue) must see exactly the cells that /// actually received at least one static this frame. /// @@ -100,7 +100,7 @@ internal static class InteriorEntityPartition } /// - /// Allocating overload — always returns a brand-new . + /// Allocating overload — always returns a brand-new . /// Kept for tests and any one-shot caller; the per-frame render path /// uses the /// reuse overload instead (see ). @@ -121,7 +121,7 @@ internal static class InteriorEntityPartition /// in place (see ) and refills it, /// reusing each cell's existing List<WorldEntity> when the /// cell key survives from the previous frame instead of allocating a new - /// one — the per-cell dictionary entries persist across frames (cleared, + /// one — the per-cell dictionary entries persist across frames (cleared, /// never removed) since the visible-cell set is usually stable frame to /// frame. Identical partitioning output to the allocating overload; only /// the backing storage is reused. @@ -141,7 +141,7 @@ internal static class InteriorEntityPartition if (e.MeshRefs.Count == 0) continue; // Retail contract: every server-spawned entity is a DYNAMIC - // and draws in the last pass — indoor, outdoor, or unresolved. + // and draws in the last pass — indoor, outdoor, or unresolved. if (e.ServerGuid != 0) { result.Dynamics.Add(e); @@ -237,7 +237,7 @@ internal static class InteriorEntityPartition } } - /// Shared indoor classification — keep DrawDynamicsLast, the + /// Shared indoor classification — keep DrawDynamicsLast, the /// outside-stage assignment (#118), and the partition in lockstep. public static bool IsIndoorCellId(uint cellId) { diff --git a/src/AcDream.App/Rendering/NdcScissorRect.cs b/src/AcDream.App/Rendering/NdcScissorRect.cs index f764335a..f26eb0c6 100644 --- a/src/AcDream.App/Rendering/NdcScissorRect.cs +++ b/src/AcDream.App/Rendering/NdcScissorRect.cs @@ -1,29 +1,29 @@ -// NdcScissorRect.cs +// NdcScissorRect.cs // -// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound). +// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound). // The scissor that brackets a landscape/doorway slice is a fallback BOUND on // the slice's view region (AD-17 in the divergence register): it must CONTAIN // every fragment the per-fragment plane clip would keep. Under-inclusion is -// the bug class — the #130 doorway top-edge background strip was this box +// the bug class — the #130 doorway top-edge background strip was this box // computed as Floor(origin) + Ceiling(size), whose far edge -// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge +// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge // at unlucky fractional alignments, scissoring away the aperture's top/right // pixel row for the whole slice (sky, terrain, statics, weather) while the -// seal still stamps it — a strip of clear color no later pass can fill. +// seal still stamps it — a strip of clear color no later pass can fill. // // Correct outer bound: floor both mins, ceil both maxes, width = difference. // A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in -// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒ -// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So +// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒ +// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So // [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by -// at most one pixel per edge — safe per AD-17's doctrine (the wall shell / +// at most one pixel per edge — safe per AD-17's doctrine (the wall shell / // plane clip repaints or kills the surplus). using System; using System.Numerics; namespace AcDream.App.Rendering; -internal static class NdcScissorRect +public static class NdcScissorRect { /// Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a /// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to diff --git a/src/AcDream.App/Rendering/OrbitCamera.cs b/src/AcDream.App/Rendering/OrbitCamera.cs index 1316222b..358dc9c2 100644 --- a/src/AcDream.App/Rendering/OrbitCamera.cs +++ b/src/AcDream.App/Rendering/OrbitCamera.cs @@ -2,7 +2,7 @@ namespace AcDream.App.Rendering; -internal sealed class OrbitCamera : ICamera +public sealed class OrbitCamera : ICamera { public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock public float Distance { get; set; } = 300f; diff --git a/src/AcDream.App/Rendering/OutdoorCellNode.cs b/src/AcDream.App/Rendering/OutdoorCellNode.cs index 7bbec56c..13ae4d0d 100644 --- a/src/AcDream.App/Rendering/OutdoorCellNode.cs +++ b/src/AcDream.App/Rendering/OutdoorCellNode.cs @@ -1,24 +1,24 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; /// -/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors. -/// Retail roots every in-world frame at viewer_cell (SmartBox::RenderNormalMode → +/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors. +/// Retail roots every in-world frame at viewer_cell (SmartBox::RenderNormalMode → /// DrawInside(viewer_cell), decomp:92635); when outdoors that is a CLandCell. acdream models it /// as a portal-less carrying only (so -/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw +/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw /// as the root's shell) and . /// /// R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail -/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the -/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827). +/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the +/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827). /// acdream issues those via per nearby /// building inside . The pre-R-A2 design flooded all /// buildings from one root through reverse portals, coupling their interior membership to a single -/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap. +/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap. /// -internal static class OutdoorCellNode +public static class OutdoorCellNode { public static LoadedCell Build(uint outdoorCellId) => new LoadedCell { diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs index 4ddfcb0e..33883803 100644 --- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs +++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs @@ -191,16 +191,13 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView { private readonly UiViewport _viewport; private readonly IPaperdollInventoryVisibility _inventory; - private readonly ExternalViewportTextureBridge _textureBridge; public RetailPaperdollFrameView( UiViewport viewport, - IPaperdollInventoryVisibility inventory, - IGpuDevice gpuDevice) + IPaperdollInventoryVisibility inventory) { _viewport = viewport ?? throw new ArgumentNullException(nameof(viewport)); _inventory = inventory ?? throw new ArgumentNullException(nameof(inventory)); - _textureBridge = new ExternalViewportTextureBridge(gpuDevice); } public bool TryGetVisibleSize(out int width, out int height) @@ -218,7 +215,7 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView } public void SetTextureHandle(uint textureHandle) => - _viewport.TextureHandle = _textureBridge.Resolve(textureHandle); + _viewport.TextureHandle = textureHandle; } /// Narrow visibility adapter for the paperdoll's inventory host. diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index f0f168e3..b01395d3 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// Paperdoll-specific facade over the shared private creature viewport. The /// fixed camera remains the verbatim retail gmPaperDollUI camera. /// -internal sealed class PaperdollViewportRenderer : +public sealed class PaperdollViewportRenderer : IUiViewportRenderer, IPaperdollDollRenderer, IDisposable diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index ecfa0ca8..81657d9a 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering; /// compositing order is shared with ordinary translucent GfxObj parts. Sky and /// sealed off-screen passes retain their independent immediate path. /// -internal sealed unsafe class ParticleRenderer : IDisposable +public sealed unsafe class ParticleRenderer : IDisposable { // The texture is per instance through GL_ARB_bindless_texture. Only blend // state remains a draw-call boundary, so stable retail distance order no @@ -69,8 +69,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable /// /// Vertex-instance ABI shared with particle.vert. Campaign V slice V2c /// (2026-07-27): TextureHandleLow/High (the split halves of a raw 64-bit - /// ARB_bindless_texture handle) became one TextureIndex — a slot into the - /// binding=9 handle table — so ordered particles using different textures + /// ARB_bindless_texture handle) became one TextureIndex — a slot into the + /// binding=9 handle table — so ordered particles using different textures /// still remain one instanced draw when their blend mode matches. /// [StructLayout(LayoutKind.Sequential)] @@ -121,8 +121,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable // Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, - // GpuBindingModel.StorageTextureTable). Owns its own table — see - // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why + // GpuBindingModel.StorageTextureTable). Owns its own table — see + // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why // particles don't share WbDrawDispatcher's/EnvCellRenderer's/ // TerrainModernRenderer's tables. There is no automated pixel-gate // coverage for particles (the offline gate's fixed outdoor view has none @@ -1157,7 +1157,7 @@ internal sealed unsafe class ParticleRenderer : IDisposable _gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float))); _gl.VertexAttribDivisor(5, 1); // Campaign V slice V2c: one uint table slot (was uvec2 low/high - // handle halves) — BillboardGpuInstance shrank by 4 bytes. + // handle halves) — BillboardGpuInstance shrank by 4 bytes. _gl.EnableVertexAttribArray(6); _gl.VertexAttribIPointer( 6, diff --git a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs index c2fb411c..23662168 100644 --- a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs +++ b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Physics; namespace AcDream.App.Rendering; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// cell walls (FindEnvCollisions) AND outdoor/baked GfxObj shells /// (FindObjCollisions) in one faithful path. /// -internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe +public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe { /// Retail viewer_sphere radius (acclient :93314). public const float ViewerSphereRadius = 0.3f; @@ -23,13 +23,13 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos) { - // update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null - // (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player. + // update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null + // (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player. if (cellId == 0) return new CameraSweepResult(playerPos, 0u); // === Start cell (pc:92824-92844) === // Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via - // CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than + // CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than // the feet (the cellar lip: feet in the low connector, head up at floor level). On // failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition). uint startCell = cellId; @@ -39,10 +39,10 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe if (found) startCell = pivotCell; } - // === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) === + // === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) === // SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player // foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the - // path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back. + // path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back. Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius); Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius); @@ -59,7 +59,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe // Retail SmartBox::update_viewer calls init_object(player, 0x5c) = // IsViewer | PathClipped | FreeRotate | PerfectClip (acclient // pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes - // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the + // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the // spring-arm pull-in, not the player's edge-slide. IsViewer lets the // eye pass through creatures, colliding only with world geometry // (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in @@ -95,7 +95,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe // === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye === // The sweep found no valid position; try to seat the eye at its own cell. - // (Seed with the player cell — acdream's camera doesn't track the sought-eye's + // (Seed with the player cell — acdream's camera doesn't track the sought-eye's // cell separately; the eye is near the player so its stab-list is the right one.) var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye); if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell); @@ -104,11 +104,11 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe return new CameraSweepResult(playerPos, 0u); } - /// Eye/pivot point → InitPath path point (subtract the sphere-center offset). + /// Eye/pivot point → InitPath path point (subtract the sphere-center offset). internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius) => spherePoint - new Vector3(0f, 0f, radius); - /// InitPath path point → eye point (add the sphere-center offset back). + /// InitPath path point → eye point (add the sphere-center offset back). internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius) => pathPoint + new Vector3(0f, 0f, radius); } diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs index 9565044e..8de3bdaa 100644 --- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs +++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Numerics; using AcDream.App.Rendering.Wb; @@ -8,15 +8,15 @@ namespace AcDream.App.Rendering; /// /// BR-2 (holistic building-render port): retail's invisible portal depth -/// writes — the port of D3DPolyRender::DrawPortalPolyInternal +/// writes — the port of D3DPolyRender::DrawPortalPolyInternal /// (Ghidra 0x0059bc90, pc:424490). /// /// Wired by T1 (BR-3, `579c8b0`): seal on interior roots, punch /// on outdoor / look-in roots, via RetailPViewPassExecutor.DrawPortalDepthWrite -/// (the DrawExitPortalMasks slice callback) — safe alongside the +/// (the DrawExitPortalMasks slice callback) — safe alongside the /// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after /// dynamics and erased the player; reverted 88be519). #117 (2026-06-11) -/// added the two-pass stencil depth gate on the punch side — see +/// added the two-pass stencil depth gate on the punch side — see /// . /// /// Retail projects a portal polygon, software-clips it against the @@ -25,12 +25,12 @@ namespace AcDream.App.Rendering; /// /// Seal (retail maxZ2=6, bit0 clear, data 0x00820e14): /// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE -/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen +/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen /// through a doorway keeps its pixels because farther interior geometry /// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840, /// pc:432783-432786). /// Punch (retail maxZ1=7, bit0 set, data 0x00820e18): -/// z forced to the far plane (0.99999988) — erases depth inside a building +/// z forced to the far plane (0.99999988) — erases depth inside a building /// aperture so the interior cells drawn next land cleanly /// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this /// side. @@ -38,7 +38,7 @@ namespace AcDream.App.Rendering; /// /// Where retail clips the polygon on the CPU against the view, we apply /// the SAME view region via gl_ClipDistance from the slice's clip-space -/// half-planes (≤8, the validated output) — the +/// half-planes (≤8, the validated output) — the /// depth write lands only inside the slice region, matching retail's clipped /// fan. /// @@ -46,7 +46,7 @@ namespace AcDream.App.Rendering; /// sets everything it depends on, restores the frame-global convention on /// exit, no early-outs between set and restore. /// -internal sealed class PortalDepthMaskRenderer : IDisposable +public sealed class PortalDepthMaskRenderer : IDisposable { private const string VertSrc = @"#version 430 core layout(location = 0) in vec3 aPos; @@ -215,28 +215,28 @@ void main() { } // depth-only: color writes are masked off by the caller state /// /// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the /// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because - /// retail's outdoor pass is painter's-ordered far→near (anything nearer + /// retail's outdoor pass is painter's-ordered far→near (anything nearer /// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame /// has no such order, so an unconditional far-Z punch erased the depth /// of NEARER occluders (terrain hills, closer buildings) at aperture - /// pixels — doors/interiors painted through them (the T5 #117 report). + /// pixels — doors/interiors painted through them (the T5 #117 report). /// The z-buffer-correct equivalent: punch ONLY where the aperture /// polygon itself wins a depth test at its true depth (two-pass - /// stencil below). The bias keeps the #108 case covered — terrain + /// stencil below). The bias keeps the #108 case covered — terrain /// hugging the door plane (centimeters in front of the aperture) must /// still be punched; a hill or another house meters nearer must not. /// private const float PunchMarkDepthBias = 0.0005f; /// - /// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b - /// spans ≈ b·d²/near meters of eye depth at eye distance d. With + /// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b + /// spans ≈ b·d²/near meters of eye depth at eye distance d. With /// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but /// ~190 m at a landblock away: every hill/house in front of a distant - /// aperture passed the mark and got far-Z punched — door-shaped leaks + /// aperture passed the mark and got far-Z punched — door-shaped leaks /// through occluders. Fix: cap the bias's EYE-SPACE span at /// . Below the ~10 m crossover - /// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins — + /// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins — /// bit-identical to the T5-validated close-range behavior (#108 grass /// coverage untouched); beyond it the punch can never reach an occluder /// more than the cap in front of the aperture plane. @@ -245,7 +245,7 @@ void main() { } // depth-only: color writes are masked off by the caller state /// Retail Render::znear = 0.1 (decomp :342173, re-landed /// d4b5c71). The cap conversion below assumes the production camera near - /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored. + /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored. public const float CameraNearPlaneMeters = 0.1f; /// CPU mirror of the vertex-shader mark-bias expression (keep in @@ -261,13 +261,13 @@ void main() { } // depth-only: color writes are masked off by the caller state /// slice's clip-space half-planes. selects /// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth). /// - /// Seal (interior root): one pass, retail-verbatim — + /// Seal (interior root): one pass, retail-verbatim — /// depth ALWAYS + true projected depth. It runs immediately after the /// gated full depth clear, so there is no nearer content to stomp. /// /// Punch (outdoor root / look-in): two passes (#117). /// Pass A marks stencil where the aperture fan passes a LEQUAL depth - /// test at its (biased) true depth — i.e. where the aperture is + /// test at its (biased) true depth — i.e. where the aperture is /// actually visible against everything drawn so far. Pass B writes the /// far-Z punch with depth ALWAYS but stencil-gated to the marked /// pixels, and zeroes the stencil as it goes (self-cleaning). This is @@ -301,7 +301,7 @@ void main() { } // depth-only: color writes are masked off by the caller state _gl.Disable(EnableCap.CullFace); // portal fans face either way _gl.Disable(EnableCap.ScissorTest); _gl.Enable(EnableCap.DepthTest); - _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color + _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color for (int i = 0; i < planeCount; i++) _gl.Enable(EnableCap.ClipDistance0 + i); @@ -351,7 +351,7 @@ void main() { } // depth-only: color writes are masked off by the caller state if (!forceFarZ) { - // ── SEAL: retail-verbatim single pass ── + // ── SEAL: retail-verbatim single pass ── _gl.DepthFunc(DepthFunction.Always); _gl.DepthMask(true); _gl.Uniform1(_locForceFarZ, 0); @@ -360,7 +360,7 @@ void main() { } // depth-only: color writes are masked off by the caller state } else { - // ── PUNCH pass A: stencil-mark visible aperture pixels ── + // ── PUNCH pass A: stencil-mark visible aperture pixels ── _gl.Enable(EnableCap.StencilTest); _gl.StencilFunc(StencilFunction.Always, 1, 0xFF); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); @@ -373,8 +373,8 @@ void main() { } // depth-only: color writes are masked off by the caller state PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters); _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n); - // ── PUNCH pass B: far-Z write on marked pixels only; - // zero the stencil as we go (self-cleaning) ── + // ── PUNCH pass B: far-Z write on marked pixels only; + // zero the stencil as we go (self-cleaning) ── _gl.StencilFunc(StencilFunction.Equal, 1, 0xFF); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero); _gl.DepthFunc(DepthFunction.Always); diff --git a/src/AcDream.App/Rendering/PortalProjection.cs b/src/AcDream.App/Rendering/PortalProjection.cs index b9163def..d6092ded 100644 --- a/src/AcDream.App/Rendering/PortalProjection.cs +++ b/src/AcDream.App/Rendering/PortalProjection.cs @@ -1,11 +1,11 @@ -// PortalProjection.cs +// PortalProjection.cs // // Phase A8.F: project a cell-local portal polygon to NDC screen space. Homogeneous frustum clip // in CLIP SPACE (before the perspective divide): first the IN-FRONT-OF-EYE half-space (keep where // w > MinW) so a portal straddling the camera does not invert under the divide and the divide -// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every +// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every // surviving vertex lands on the screen [-1,1] by construction. The side-plane clip is the R1 -// void-flap fix (2026-06-05) — see ProjectToNdc. +// void-flap fix (2026-06-05) — see ProjectToNdc. // // The clip is NEAR-INDEPENDENT on purpose. We only use the projected x/y for the visibility clip // REGION, so a vertex in front of the eye is meaningful even if it is closer than the projection's @@ -23,7 +23,7 @@ using System.Numerics; namespace AcDream.App.Rendering; -internal static class PortalProjection +public static class PortalProjection { internal ref struct ClipPolygonLease { @@ -118,17 +118,17 @@ internal static class PortalProjection Vector4[]? second = null; // Homogeneous frustum clip in CLIP SPACE, before the perspective divide. First the - // in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is - // standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The + // in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is + // standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The // side clip is the R1 void-flap fix (2026-06-05): without it, a portal WITHIN the near // plane projected small-w verts to wildly off-screen NDC (the probe saw (10.2,-67.4)), // which corrupted the downstream 2D ScreenPolygonClip into an EMPTY region -> OutsideView // empty -> terrain Skip -> the bluish doorway "void". Clipping the side planes here bounds // every surviving vertex to the screen [-1,1] by construction, so a screen-covering doorway // clips to the screen (non-empty) instead of collapsing. The eye plane is clipped FIRST so - // all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined. + // all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined. // Near/far are intentionally NOT clipped (near-independence). Retail PView::GetClip - // (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5). + // (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5). try { second = vectorPool.Rent(capacity); @@ -155,7 +155,7 @@ internal static class PortalProjection (current, output) = (output, current); } - // Perspective divide → NDC xy. This is the only result allocation. + // Perspective divide → NDC xy. This is the only result allocation. var ndc = new Vector2[currentCount]; for (int i = 0; i < currentCount; i++) { @@ -174,21 +174,21 @@ internal static class PortalProjection /// Faithful homogeneous projection (retail PrimD3DRender::xformStart + the W=0 clip of /// ACRender::polyClipFinish, decomp 424310 / 702749): transform the portal to clip space and clip - /// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO + /// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO /// frustum side-plane clamp. The screen bound is applied later by /// against the view region (the root region is the full screen), exactly as retail clips the portal /// against the accumulated portal_view rather than fixed side planes. /// /// The W=0 clip is exact on purpose (the knife-edge port, 2026-06-11; pseudocode at /// docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md): boundary intersections land - /// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks) + /// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks) /// yields the correct UNBOUNDED half-region, which the bounded view-region clip then cuts to the /// screen. The previous EyePlaneW = 1e-4 produced finite ~1e4-NDC boundary verts whose region - /// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0 + /// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0 /// vertex can never survive ClipToRegion into its divide (a nonzero direction fails at least one /// edge test of any BOUNDED convex region), so no divide-by-zero path exists; the measure-zero /// corner case is guarded in ClipToRegion. Matches polyClipFinish part 1: clip pass runs only - /// when some vertex has w < 0; <3 survivors → reject (empty). + /// when some vertex has w < 0; <3 survivors → reject (empty). public static Vector4[] ProjectToClip(IReadOnlyList localPoly, Matrix4x4 cellToWorld, Matrix4x4 viewProj) { using ClipPolygonLease lease = ProjectToClipLease(localPoly, cellToWorld, viewProj); @@ -274,7 +274,7 @@ internal static class PortalProjection /// (CCW convex) with w-aware Sutherland-Hodgman edge tests, then divide the survivors to NDC and /// normalize to CCW. Ports retail ACRender::polyClipFinish's view-region clip (decomp 702749): the /// edge test multiplies through w (which is > 0 after the eye-plane clip) so it never divides a - /// near-eye vertex, and the final divide runs only on survivors already bounded to the region — + /// near-eye vertex, and the final divide runs only on survivors already bounded to the region — /// stable by construction. Returns <3 verts when the portal does not intersect the region. public static Vector2[] ClipToRegion(IReadOnlyList subjectClip, IReadOnlyList regionCcwNdc) { @@ -339,7 +339,7 @@ internal static class PortalProjection // Homogeneous Sutherland-Hodgman: clip the (w > 0) subject against each CCW edge of the NDC // region. f(P) below is the NDC inside test cross(edge, P_ndc - a) multiplied through P.W, - // which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex + // which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex // is ever divided (retail polyClipFinish, decomp 702749). int regionCount = regionCcwNdc.Count; int capacity = checked(subjectClip.Length + regionCount); @@ -370,7 +370,7 @@ internal static class PortalProjection if (currentCount < 3) return System.Array.Empty(); - // Divide survivors → NDC. They are already inside the bounded region. A w=0 + // Divide survivors → NDC. They are already inside the bounded region. A w=0 // measure-zero corner remains the same empty knife-edge result as the prior path. ndcScratch = vector2Pool.Rent(currentCount); Span ndc = ndcScratch.AsSpan(0, currentCount); @@ -408,8 +408,8 @@ internal static class PortalProjection // Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses // runs of consecutive near-identical vertices, including across the // wrap-around. A polygon that collapses below 3 distinct vertices is - // degenerate (sub-pixel sliver) and returns empty — exactly retail's - // "<3 surviving verts → output count 0". + // degenerate (sub-pixel sliver) and returns empty — exactly retail's + // "<3 surviving verts → output count 0". private const float VertexMergeEpsilonNdc = 2f / 1080f; private static int MergeSubPixelVertices(Span poly) @@ -428,7 +428,7 @@ internal static class PortalProjection } poly[kept++] = vertex; } - // Wrap-around: last ≈ first. + // Wrap-around: last ≈ first. while (kept >= 2) { Vector2 first = poly[0]; @@ -442,9 +442,9 @@ internal static class PortalProjection return kept; } - // One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside + // One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside // (left) part of a HOMOGENEOUS polygon. Inside test for vertex P (clip space): the NDC cross - // product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0. + // product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0. // Crossings interpolate in homogeneous coords (perspective-correct), via the shared Lerp. private static int ClipHomogeneousEdge( ReadOnlySpan polygon, @@ -489,7 +489,7 @@ internal static class PortalProjection if (area2 < 0f) System.Array.Reverse(poly); } - // Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye + // Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye // (w=0) singularity and the ~5 cm right at it (bounding the perspective divide), but is // INTENTIONALLY far closer than the projection's 1.0 m near plane so a doorway the camera is // standing in still projects and the cell behind it stays visible. See the file header. diff --git a/src/AcDream.App/Rendering/PortalTunnelCamera.cs b/src/AcDream.App/Rendering/PortalTunnelCamera.cs index 07aa34ab..7126b4a9 100644 --- a/src/AcDream.App/Rendering/PortalTunnelCamera.cs +++ b/src/AcDream.App/Rendering/PortalTunnelCamera.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// (0x00453760). Identity looks along AC +Y with +Z up; the animated /// angle rolls that view around its own +Y forward axis. /// -internal sealed class PortalTunnelCamera : ICamera +public sealed class PortalTunnelCamera : ICamera { public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f); diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs index e9781784..8b10eb24 100644 --- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs +++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Content.Vfx; @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering; /// 40 frames/second and drawn as a replacement 3-D viewport beneath the /// retained gameplay UI. /// -internal sealed class PortalTunnelPresentation : IDisposable +public sealed class PortalTunnelPresentation : IDisposable { public const uint SetupClientEnum = 0x10000001u; public const uint AnimationClientEnum = 0x10000002u; diff --git a/src/AcDream.App/Rendering/PortalView.cs b/src/AcDream.App/Rendering/PortalView.cs index 8915296e..9dd082cd 100644 --- a/src/AcDream.App/Rendering/PortalView.cs +++ b/src/AcDream.App/Rendering/PortalView.cs @@ -1,4 +1,4 @@ -// PortalView.cs +// PortalView.cs // // Phase A8.F: GL-free 2D screen-space (NDC) clip-region data model. // Mirrors retail view_poly (acclient.h:32465) and view_type (acclient.h:32338): @@ -10,7 +10,7 @@ using System.Numerics; namespace AcDream.App.Rendering; /// One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect. -internal readonly struct ViewPolygon +public readonly struct ViewPolygon { public readonly Vector2[] Vertices; public readonly float MinX, MinY, MaxX, MaxY; @@ -101,7 +101,7 @@ internal sealed class PortalPolygonVertexStore } /// A cell's accumulated clip region: a set of convex view polygons + the union bounding rect. -internal sealed class CellView +public sealed class CellView { // ViewPolygon exposes its vertex array for the renderer, so this seed must // be owned by the CellView rather than shared globally. Pooling the @@ -170,7 +170,7 @@ internal sealed class CellView MaxY = float.MinValue; } - /// A region covering the entire NDC viewport — the camera cell's seed region + /// A region covering the entire NDC viewport — the camera cell's seed region /// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814). public static CellView FullScreen() { @@ -192,7 +192,7 @@ internal sealed class CellView // Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build // re-queues a cell every time its CellView GROWS, so the flood only terminates when Add // recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns - // float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman + + // float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman + // EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region // grew without bound -> O(n^2) CPU-spin hang in this method. We instead key each polygon by its // vertices SNAPPED to a small NDC grid, consecutive snap-duplicates removed, rotated to a @@ -206,14 +206,14 @@ internal sealed class CellView // #120 convergence (2026-06-11): reject a polygon CONTAINED in one already // stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a - // portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each - // lap, a region that is — in exact arithmetic — a SUBSET of the polygon + // portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each + // lap, a region that is — in exact arithmetic — a SUBSET of the polygon // that originated it; near-edge-on apertures make the re-clip wobble by // more than the 1e-3 key grid, so every lap keyed as "new" and the // in-place growth recursed to the depth-128 tripwire (chain dumps: - // 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests + // 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests // reproduces deterministically). Containment rejection makes growth - // strictly area-increasing — no new visible area, no propagation. The + // strictly area-increasing — no new visible area, no propagation. The // key stays recorded so the exact emission also short-circuits later. // Bonus: back-emission into a full-screen view (the root cell) is now // always rejected outright. @@ -228,7 +228,7 @@ internal sealed class CellView } // #120: is polygon p entirely inside ONE stored polygon (with DedupGridNdc - // slack)? Single-polygon containment is sufficient for the ping-pong class — + // slack)? Single-polygon containment is sufficient for the ping-pong class — // a round-trip re-emission descends from exactly one originator. Stored // polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the // edge test adapts to either winding via the polygon's signed area. @@ -252,7 +252,7 @@ internal sealed class CellView { if (convex.Length < 3) return false; - // signed area → winding (CCW positive); inside = left of every CCW edge. + // signed area → winding (CCW positive); inside = left of every CCW edge. float area2 = 0f; for (int i = 0; i < convex.Length; i++) { @@ -268,10 +268,10 @@ internal sealed class CellView var b = convex[(i + 1) % convex.Length]; var ab = b - a; float len = ab.Length(); - if (len < 1e-9f) continue; // degenerate edge — no constraint + if (len < 1e-9f) continue; // degenerate edge — no constraint foreach (var pt in pts) { - // signed perpendicular distance of pt from edge a→b (positive = inside for CCW) + // signed perpendicular distance of pt from edge a→b (positive = inside for CCW) float cross = sign * (ab.X * (pt.Y - a.Y) - ab.Y * (pt.X - a.X)); if (cross < -eps * len) return false; // a vertex lies outside this edge by more than eps @@ -280,7 +280,7 @@ internal sealed class CellView return true; } - // NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings + // NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings // (so real regions stay distinct) yet far coarser than the per-round float drift of a re-clipped // region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth. private const float DedupGridNdc = 1e-3f; @@ -293,24 +293,24 @@ internal sealed class CellView // // W=0 port (2026-06-11): an ALL-COLLINEAR polygon (zero area) keys as its snapped segment // ("L:" + extreme points) instead of null. A portal whose plane contains the eye projects to - // exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards + // exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards // any GetClip output with count != 0 to copy_view/OtherPortalClip with no area gate anywhere, // so the neighbour cell stays in the draw list (cells draw whole; onward floods die naturally // against the zero-area region). Rejecting these views dropped the whole chain behind an - // exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral + // exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral // landings). The segment key space is finite like the area-key space, so dedup + the strict // growth convergence invariant are unchanged. Degenerate is returned only when fewer than 2 - // distinct snapped points survive (a true sub-grid point — not a real region OR segment). + // distinct snapped points survive (a true sub-grid point — not a real region OR segment). // - // §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper - // (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop) + // §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper + // (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop) // legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so // BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices. // Without collinear canonicalization those re-emissions key as distinct, defeating the dedup and // accumulating duplicate polygons (the pre-2026-06-06 unbounded-growth hang in miniature, and the // exact reason the reciprocal clip was previously parked on the unstable divide-first path). // Dropping collinear snapped points makes the key purely a function of the region's CORNERS, so - // any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates. + // any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates. private CanonicalKeyResult TryAddCanonicalKey(Vector2[]? verts) { if (verts is null || verts.Length < 3) diff --git a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs index 2f37a238..2edaaf2a 100644 --- a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs +++ b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs @@ -1,4 +1,4 @@ -// PortalVisibilityBuilder.cs +// PortalVisibilityBuilder.cs // // Phase A8.F: recursive portal-clip visibility (the builder). Port of retail // PView::ConstructView (decomp:433750) -> ClipPortals (433572) -> AddViewToPortals @@ -12,7 +12,7 @@ using System.Numerics; namespace AcDream.App.Rendering; /// Per-frame output of the portal-frame BFS. -internal sealed class PortalVisibilityFrame +public sealed class PortalVisibilityFrame { private const int MaxRetainedCellViews = 512; internal const int MaxRetainedBuildCollectionCapacity = 512; @@ -31,7 +31,7 @@ internal sealed class PortalVisibilityFrame internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount; internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount; - /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals + /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals /// recursively clipped to their portal chain. The cellar-flap fix. public CellView OutsideView { get; private set; } = new(); @@ -39,7 +39,7 @@ internal sealed class PortalVisibilityFrame public Dictionary CellViews { get; } = new(); /// Visible interior cells in the exact order they were first dequeued from the - /// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's + /// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's /// PView::cell_draw_list, appended in PView::ConstructView (decomp:433783) as each cell pops /// off the nearest-vertex-sorted cell_todo_list (InsCellTodoList 433183). Deduplicated: a cell /// appears exactly once, on its first dequeue. The camera cell is always first. @@ -243,7 +243,7 @@ internal sealed class PortalVisibilityFrame } } -internal static class PortalVisibilityBuilder +public static class PortalVisibilityBuilder { // Side-classification epsilon. Retail's is F_EPSILON = 0.000199999995 // (const @0x007c8c70; PView::InitCell Ghidra 0x005a4b70). T2 (BR-4) @@ -253,20 +253,20 @@ internal static class PortalVisibilityBuilder // side test never sees a stale root more than F_EPSILON past a plane. Our // root can lag the eye by up to ~1 cm at pressed corners (the harness's // fixed-root sweep models this), and 0.01 is that documented root-lag - // tolerance — NOT a retail constant. Tighten to F_EPSILON only together + // tolerance — NOT a retail constant. Tighten to F_EPSILON only together // with eye-exact viewer-cell tracking verification (the #108-membership // family) + the cdstW near-clip pin. private const float PortalSideEpsilon = 0.01f; - // Retail F_EPSILON proper — used where the semantic is knife-edge - // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0, + // Retail F_EPSILON proper — used where the semantic is knife-edge + // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0, // Ghidra 0x005a59a0), which must NOT inherit the root-lag tolerance above // (a 1 cm-wide in-plane band would reject look-in seeds whenever the eye // stands near a doorway plane). private const float SeedInPlaneEpsilon = 0.0002f; // TEMP diagnostic (Phase A8.F visual-gate triage; strip after): ACDREAM_A8_DUMP_PV=1 dumps the - // local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell. + // local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell. private static readonly bool s_pvDump = Environment.GetEnvironmentVariable("ACDREAM_A8_DUMP_PV") == "1"; private static readonly Dictionary s_pvDumpCount = new(); @@ -275,14 +275,14 @@ internal static class PortalVisibilityBuilder /// #120 observable: total convergence-tripwire firings across both the /// interior and the exterior look-in propagation. /// The tripwire firing means the in-place growth's fixpoint invariant - /// broke (T2/BR-4) — tests reset this and assert it stays 0. + /// broke (T2/BR-4) — tests reset this and assert it stays 0. /// public static int ConvergenceTripwireCount; /// /// #120 self-attribution dump: the growth-recursion path that exceeded - /// the tripwire, as a per-cell frequency summary plus the chain tail — - /// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap) + /// the tripwire, as a per-cell frequency summary plus the chain tail — + /// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap) /// reads directly off the output. /// private static void DumpPropagationChain(uint[] chain, int depth, uint rootCellId, Vector3 eye) @@ -309,11 +309,11 @@ internal static class PortalVisibilityBuilder /// The +Z world lift applied to DRAWN cell shells (z-fighting vs /// terrain; applied in GameWindow's cell registration). The visibility - /// graph stays in PHYSICS (unlifted) space — feeding the lift into portal + /// graph stays in PHYSICS (unlifted) space — feeding the lift into portal /// planes broke horizontal-portal side tests (#119-residual, f35cb8b). /// Draw-space consumers of portal polygons (the OutsideView color gate /// here, the seal/punch depth fans in GameWindow) must apply this lift so - /// they meet the drawn shell's aperture edge — the unlifted gate left a + /// they meet the drawn shell's aperture edge — the unlifted gate left a /// 2 cm background strip under the drawn lintel (#130). public const float ShellDrawLiftZ = 0.02f; @@ -347,7 +347,7 @@ internal static class PortalVisibilityBuilder // Render unification (outdoor-as-cell, 2026-06-07): when the root IS the synthetic outdoor // node, the landscape is visible FULL-SCREEN, so seed OutsideView with the full-screen NDC // quad. ClipFrameAssembler turns that into a full-screen OutsideView slice, so DrawInside's - // DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" — + // DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" — // the very same callback that already draws the doorway slice when an INTERIOR root reaches // outdoors. Keyed on the explicit IsOutdoorNode flag (set by OutdoorCellNode.Build), NOT a // cell-id heuristic: production EnvCell ids are >= 0x100 but test fixtures use low interior @@ -357,7 +357,7 @@ internal static class PortalVisibilityBuilder frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad)); // Distance-priority work list (retail PView::cell_todo_list). Cells pop closest-first; - // each cell carries the camera→nearest-portal-vertex distance that put it on the list + // each cell carries the camera→nearest-portal-vertex distance that put it on the list // (retail keys on InitCell's per-portal min-vertex distance, decomp 432988-433004). The // camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it // always pops first. @@ -367,10 +367,10 @@ internal static class PortalVisibilityBuilder // Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the // retail portal_view slice offset 0x44 (last-incorporated view-poly watermark) vs 0x38 // (current view_count) decision in AddViewToPortals (433446): a cell is INSERTED into the - // todo list exactly once — on first discovery (retail's ecx_5==0 branch calls + // todo list exactly once — on first discovery (retail's ecx_5==0 branch calls // InsCellTodoList; the ecx_5!=eax_2 growth branch calls AddToCell IN PLACE and never // re-enqueues). Later growth into an already-discovered cell is unioned into its CellView but - // does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784 + // does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784 // the instant a cell is popped). Enqueue-once across the cell set is the hard termination // guarantee for cyclic / hub / diamond graphs: at most N cells are ever processed. The // camera cell is pre-marked so a portal looping back to it can never re-enqueue it. @@ -398,11 +398,11 @@ internal static class PortalVisibilityBuilder { Console.WriteLine($"[pv-dump] camCell=0x{cameraCell.CellId:X8} portals={cameraCell.Portals.Count} polyLists={cameraCell.PortalPolygons.Count} vp[M11={viewProj.M11:F3} M22={viewProj.M22:F3} M33={viewProj.M33:F3} M34={viewProj.M34:F3} M43={viewProj.M43:F3} M44={viewProj.M44:F3}]"); // Camera-cell portal census (A8.F triage 2026-05-29): report, for EVERY - // portal, the exact inputs the BFS guards read — BEFORE the guards run, so + // portal, the exact inputs the BFS guards read — BEFORE the guards run, so // a portal the loop silently `continue`s past is still visible here. An // empty OUTSIDEVIEW can then be traced to the precise gate: polyLen<3 (empty // polygon from EnvCellLandblockBuildBuilder), interiorSide=false (camera back-facing the - // portal — a legitimately-empty result, not a bug), or (if both OK) a + // portal — a legitimately-empty result, not a bug), or (if both OK) a // downstream projection/clip failure shown by the EXIT-PROJ/EXIT-CLIP lines. for (int ci = 0; ci < cameraCell.Portals.Count; ci++) { @@ -417,7 +417,7 @@ internal static class PortalVisibilityBuilder } // T2 (BR-4): retail's growth propagation is IN PLACE, never by re-enqueue - // — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first + // — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first // discovery enqueues via InsCellTodoList; growth into a cell whose // cell_view_done is set calls AdjustCellView (pc:433741-433745), which // re-clips ONLY the new views (the update_count watermark) through that @@ -426,7 +426,7 @@ internal static class PortalVisibilityBuilder // neighbour; it processes exactly the new tail and recurses further // growth. Termination is physical: recursion fires only when AddRegion // added a DISTINCT polygon (CanonicalKey dedup) that survived the 1-px - // vertex merge — the finite fixpoint floor that replaced the old + // vertex merge — the finite fixpoint floor that replaced the old // MaxReprocessPerCell=16 drift cap (deleted). The depth tripwire below // is a loud failsafe, not control flow: it firing means the convergence // invariant broke and must be fixed, not tuned. @@ -434,7 +434,7 @@ internal static class PortalVisibilityBuilder // #120 self-attribution: the recursion path (cell id per depth), so a // tripwire firing names the growth CYCLE instead of just the tip. // Harness sweeps (CornerFloodReplayTests *Converges tests) could not - // reproduce the T5 firing — production-only ingredients (full lookup + // reproduce the T5 firing — production-only ingredients (full lookup // graph / real camera path) are suspected; this dump pins them on the // next natural occurrence. uint[] propagationChain = frame.PropagationChainScratch; @@ -444,7 +444,7 @@ internal static class PortalVisibilityBuilder if (depth >= RecursionTripwire) { System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount); - Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); + Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); DumpPropagationChain(propagationChain, depth, cameraCell.CellId, cameraPos); return; } @@ -497,7 +497,7 @@ internal static class PortalVisibilityBuilder // Portal-side test (retail PView::InitCell side test, decomp:432962): only traverse a portal // the camera is on the INTERIOR side of. Retail culls the back-facing portal (the doorway just - // flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old + // flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old // `&& !eyeInsideOpening` bypass let a back portal within 1.75 m through, forming the // 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap (pinned in flap-sidechk.log: // back portals show camInterior=False eyeIn=True). @@ -529,8 +529,8 @@ internal static class PortalVisibilityBuilder if (dx) Console.WriteLine($"[pv-dump] EXIT-PROJ cell=0x{cell.CellId:X8} p{i} localN={poly.Length} clipN={clipVerts} local0=({poly[0].X:F2},{poly[0].Y:F2},{poly[0].Z:F2})"); if (dx) Console.WriteLine($"[pv-dump] EXIT-CLIP cell=0x{cell.CellId:X8} p{i} currentViewPolys={currentView.Polygons.Count} clipResult={clippedRegion.Count}"); - // Empty clip = no flood through this portal, period — retail's empty-GetClip rule - // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The + // Empty clip = no flood through this portal, period — retail's empty-GetClip rule + // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The // EyeInsidePortalOpening rescue that used to substitute the current view here was // the documented compensation for ProjectToClip's old EyePlaneW=1e-4 divergence // from polyClipFinish's exact W=0 clip; with the W=0 port (2026-06-11, pseudocode @@ -554,7 +554,7 @@ internal static class PortalVisibilityBuilder // Exit portal -> outdoors visible through this (clipped) opening. // OutsideView gates DRAWN color (terrain/sky/scissor), and the // shell that rasterizes this aperture draws +drawLiftZ above - // the physics transform — project the region in the SAME + // the physics transform — project the region in the SAME // lifted space or terrain stops a lift-height short of the // drawn lintel (#130 strip). Flood semantics keep the // unlifted clippedRegion path above. @@ -607,20 +607,20 @@ internal static class PortalVisibilityBuilder continue; } - // Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip + // Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip // decomp:433524). The portal opening seen from THIS cell may be wider than the // SAME opening seen from the neighbour (skewed/oblique apertures), so retail // re-clips the already-near-side-clipped region against the neighbour's matching - // (reciprocal) portal polygon — the propagated region is the intersection of the + // (reciprocal) portal polygon — the propagated region is the intersection of the // opening "seen from A" AND "seen from B". This can only TIGHTEN, never widen, and // degrades to the prior near-side-only region when the reciprocal is unresolvable // (over-include is the safe default). The reciprocal is the portal at index - // `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link + // `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link // (arg2->other_portal_id, 433557), NOT a scan for the first OtherCellId match. The // direct index is what lets a cell with TWO portals to the same neighbour clip each // opening against its OWN reciprocal instead of the first one. Mutates clippedRegion // in place before the union below. - // T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip + // T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip // returning nothing means the opening is invisible from the // neighbour's side; the old eye-in-opening restore was part of // the deleted rescue. @@ -645,8 +645,8 @@ internal static class PortalVisibilityBuilder if (grew) { - // First discovery → enqueue once (retail InsCellTodoList in - // the ecx_5==0 branch). Distance = camera→nearest portal- + // First discovery → enqueue once (retail InsCellTodoList in + // the ecx_5==0 branch). Distance = camera→nearest portal- // opening vertex (retail InitCell min-vertex distance, // pc:432988-433004). if (queued.Add(neighbourId)) @@ -655,10 +655,10 @@ internal static class PortalVisibilityBuilder InsertTodo(todo, neighbour, dist); inserted = true; } - // Growth into an already-POPPED cell → retail AdjustCellView: + // Growth into an already-POPPED cell → retail AdjustCellView: // process only the new views, immediately, in place. A cell // discovered but still pending in the todo list needs nothing - // — its pop processes everything to date via the watermark. + // — its pop processes everything to date via the watermark. else if (drawListed.Contains(neighbourId)) { inPlace = true; @@ -678,8 +678,8 @@ internal static class PortalVisibilityBuilder // draw position (retail appends to cell_draw_list once per pop, // pc:433783). Note: retail also RE-SORTS the draw list when a // late-grown cell's dependency order changes (AdjustCellPlace, - // pc:433247); we keep first-pop order — under T1's whole-cell - // far→near draws + depth testing, order affects only transparent- + // pc:433247); we keep first-pop order — under T1's whole-cell + // far→near draws + depth testing, order affects only transparent- // pass compositing in exotic chains (documented residual for T5). if (drawListed.Add(cell.CellId)) frame.OrderedVisibleCells.Add(cell.CellId); @@ -691,7 +691,7 @@ internal static class PortalVisibilityBuilder if (pvDump) Console.WriteLine($"[pv-dump] OUTSIDEVIEW polys={frame.OutsideView.Polygons.Count} bfsCellViews={frame.CellViews.Count} crossBldg={frame.CrossBuildingViews.Count}"); - // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the + // Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the // root cell's per-portal side-test + projection + the frame's exit/visible counts. if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) EmitFlapProbe(cameraCell, cameraPos, viewProj, frame); @@ -715,7 +715,7 @@ internal static class PortalVisibilityBuilder /// camera cell. It keeps the same retail distance-priority traversal and /// neighbour reciprocal clipping once inside the building. /// - /// Optional NDC region the seed apertures clip against — + /// Optional NDC region the seed apertures clip against — /// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip /// 0x005a4320): full screen when the viewer is outdoors, the accumulated /// outside (doorway) view when a building is looked into from an interior @@ -756,7 +756,7 @@ internal static class PortalVisibilityBuilder // If the camera is on the cell-interior side, the normal indoor // DrawInside path owns this portal instead. T2 (BR-4): a seed // portal the eye is IN-PLANE with (|dist| <= F_EPSILON) rejects - // OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on + // OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on // Sidedness IN_PLANE (Ghidra 0x005a59a0); no degenerate view is // ever built from a knife-edge aperture. if (i < cell.ClipPlanes.Count) @@ -788,7 +788,7 @@ internal static class PortalVisibilityBuilder // T2 (BR-4): empty clip = no seed, no exceptions (retail's // empty-GetClip rule; the full-screen substitute rescue is - // deleted — see Build()). + // deleted — see Build()). if (clippedRegion.Count == 0) continue; @@ -800,19 +800,19 @@ internal static class PortalVisibilityBuilder } } - // T2 (BR-4): in-place growth propagation — mirrors Build()'s + // T2 (BR-4): in-place growth propagation — mirrors Build()'s // ProcessCellPortals (retail AdjustCellView via the watermark); the // re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues // are deleted (empty clip culls, period). const int RecursionTripwire = 128; - uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build() + uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build() void ProcessCellPortals(LoadedCell cell, int depth) { if (depth >= RecursionTripwire) { System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount); - Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); + Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate"); DumpPropagationChain(propagationChain, depth, 0u, cameraPos); return; } @@ -841,7 +841,7 @@ internal static class PortalVisibilityBuilder if (portal.OtherCellId == 0xFFFF) continue; // already outdoors; exterior terrain was drawn by the caller. - // R-A2b: cull back portals by the side test alone — see Build(). + // R-A2b: cull back portals by the side test alone — see Build(). if (i < cell.ClipPlanes.Count && !CameraOnInteriorSide(cell, i, cameraPos)) continue; @@ -905,15 +905,15 @@ internal static class PortalVisibilityBuilder } /// - /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827), - /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal + /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827), + /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal /// (0x5a5ab0) during the terrain BSP walk. Floods ONE building's cells from its outside-facing /// entrance portal(s). Identical machinery to , but the CONTRACT is /// per-building: the caller passes exactly one building's cells, so the seed is that building's - /// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible - /// building, measured live §3.4). This differs from the synthetic outdoor node's single unified - /// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the - /// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed. + /// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible + /// building, measured live §3.4). This differs from the synthetic outdoor node's single unified + /// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the + /// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed. /// public static PortalVisibilityFrame ConstructViewBuilding( IEnumerable buildingCells, @@ -1053,12 +1053,12 @@ internal static class PortalVisibilityBuilder } // Phase U.4c flap probe. One [flap] line per Build: the root cell's per-portal - // signed distance D (eye→portal plane), traverse/cull decision, and NDC projection + // signed distance D (eye→portal plane), traverse/cull decision, and NDC projection // vertex count, plus the frame's OutsideView polygon count + visible-cell count. - // `localEye` is the eye in root-local space — its component along an interior portal + // `localEye` is the eye in root-local space — its component along an interior portal // plane reveals when the eye has crossed past that plane (the stale-root region that // makes the side test cull a still-needed portal). Read-only recompute; no effect on - // the returned frame. Throwaway apparatus — strip with the probe. + // the returned frame. Throwaway apparatus — strip with the probe. private static void EmitFlapProbe( LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame) { @@ -1080,9 +1080,9 @@ internal static class PortalVisibilityBuilder d = Vector3.Dot(pl.Normal, localEye) + pl.D; side = CameraOnInteriorSide(cameraCell, i, cameraPos); } - // Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so + // Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so // proj/clip mean the same as production: proj = clip-space verts in front of the eye, - // clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is + // clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is // genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands. int projN = -1, clipN = -1; string ndcText = ""; @@ -1115,21 +1115,21 @@ internal static class PortalVisibilityBuilder } // Mirrors CellVisibility's portal-side test (InsideSide convention). - // In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail + // In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail // InitCell leaves the in-plane case a CANDIDATE for cell portals (Ghidra // 0x005a4b70); building/exterior SEED portals additionally reject in-plane - // via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0). + // via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0). private static bool CameraOnInteriorSide(LoadedCell cell, int portalIndex, Vector3 cameraPos) { var plane = cell.ClipPlanes[portalIndex]; - if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow + if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform); float dot = Vector3.Dot(plane.Normal, localCam) + plane.D; return plane.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon; } // T2 (BR-4): retail ConstructView(CBldPortal)'s Sidedness IN_PLANE reject - // (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior + // (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior // portal contributes nothing this frame (knife-edge aperture). Uses the // true retail epsilon, NOT the side test's root-lag tolerance. private static bool EyeInPlaneOfPortal(LoadedCell cell, int portalIndex, Vector3 cameraPos) @@ -1153,22 +1153,22 @@ internal static class PortalVisibilityBuilder if (area2 < 0f) Array.Reverse(poly); } - // Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524). + // Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524). // Resolves the neighbour's reciprocal back-portal by DIRECT INDEX (`otherPortalId`), projects // that reciprocal polygon through the NEIGHBOUR's world transform to NDC, and intersects it into // every polygon of `clippedRegion` (already clipped against the near-side opening + current - // view). The net region is "opening seen from the near cell" ∩ "opening seen from the - // neighbour" — a strict tightening that prevents over-inclusion through skewed apertures. + // view). The net region is "opening seen from the near cell" ∩ "opening seen from the + // neighbour" — a strict tightening that prevents over-inclusion through skewed apertures. // // `otherPortalId` is the near-side portal's reciprocal back-link, straight from the dat's - // CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly — - // `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for + // CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly — + // `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for // the first OtherCellId match. A scan picks the FIRST back-portal for EVERY near-side portal to // the same neighbour, so a cell with two openings into one neighbour clips both against the same - // (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion + // (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion // bug #102 M-4). The direct index gives each opening its own reciprocal. // - // GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a + // GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a // guessed polygon: the index is out of range, OR the indexed polygon is missing/degenerate // (< 3 verts), OR it projects entirely behind the camera. Over-inclusion is the safe default; // mis-resolution is the bug this method exists to remove. PortalPolygons is in lockstep with @@ -1184,38 +1184,38 @@ internal static class PortalVisibilityBuilder { if (clippedRegion.Count == 0) return; - // Retail skips OtherPortalClip entirely for exact-match portals — both cells share + // Retail skips OtherPortalClip entirely for exact-match portals — both cells share // the SAME opening polygon, so re-clipping against the reciprocal can only re-derive // the near-side clip: PView::ClipPortals decomp:433689 // `if (exact_match != 0 || other_portal_id < 0) goto propagate-without-reciprocal`. if ((portalFlags & PortalFlagExactMatch) != 0) return; - // Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include. + // Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include. if (otherPortalId >= neighbour.PortalPolygons.Count) return; Vector3[]? reciprocalPoly = neighbour.PortalPolygons[otherPortalId]; - if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include + if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include - // §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous - // pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes - // the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish + // §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous + // pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes + // the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish // homogeneous clipper as the near-side portal; there is no divide-first special case. // // HISTORY: this used to be ProjectToNdc + 2D ScreenPolygonClip.Intersect, justified by "the - // reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE + // reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE // exactly at doorways/corners: the reciprocal IS the same opening whose plane the eye presses // against (2-60 cm). ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge - // there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex - // hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the - // flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests + // there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex + // hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the + // flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests // pins this deterministically; the glitch steps die with this change). The old path's other - // rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete: + // rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete: // CanonicalKey's 1e-3-grid snap dedup (2026-06-06) absorbs re-clip drift by construction. using PortalProjection.ClipPolygonLease reciprocalClip = PortalProjection.ProjectToClipLease( reciprocalPoly, neighbour.WorldTransform, viewProj); - if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include) + if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include) // Intersect the reciprocal opening into each near-side polygon; drop any that fall away. // ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same @@ -1252,7 +1252,7 @@ internal static class PortalVisibilityBuilder return grew; } - // Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal + // Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal // min-distance loop retail runs in PView::InitCell (decomp:432988-433004) to key the todo list: // it walks the portal's vertices, transforms each to world space, and keeps the smallest // straight-line distance to the camera viewpoint. Keying on the portal opening (not the cell @@ -1272,10 +1272,10 @@ internal static class PortalVisibilityBuilder /// /// Distance-sorted work list for the portal BFS, ported from retail PView::cell_todo_list + /// InsCellTodoList (decomp:433183). Insertion keeps the list ordered so the NEAREST cell sits at - /// the tail; removes the tail — giving closest-first traversal exactly + /// the tail; removes the tail — giving closest-first traversal exactly /// as ConstructView's pop-from-(cell_todo_num-1) does (433767-433769). The insertion only shifts /// entries strictly farther than the newcomer (retail's flag test breaks on the first - /// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST — + /// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST — /// LIFO on ties, matching retail's break-on-first-not-greater + pop-from-tail. /// private static void InsertTodo( diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs index 39817ab0..1b3949b2 100644 --- a/src/AcDream.App/Rendering/RenderBootstrap.cs +++ b/src/AcDream.App/Rendering/RenderBootstrap.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using AcDream.Content; using DatReaderWriter; using Microsoft.Extensions.Logging.Abstractions; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// classes and the same order as , minus /// terrain / sky / physics / streaming. /// -internal sealed record RenderStack( +public sealed record RenderStack( GL Gl, IDatReaderWriter Dats, string ShaderDir, @@ -28,13 +28,15 @@ internal sealed record RenderStack( AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable { internal GpuFrameFlightController FrameFlights { get; init; } = null!; - internal IGpuDevice GpuDevice { get; init; } = null!; - internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!; private ResourceShutdownTransaction? _shutdown; - internal void BeginFrame() => FrameLifetime.BeginFrame(); + internal void BeginFrame() + { + FrameFlights.BeginFrame(); + UiHost.TextRenderer.BeginFrame(FrameFlights.CurrentSlot); + } - internal void EndFrame() => FrameLifetime.EndFrame(); + internal void EndFrame() => FrameFlights.EndFrame(); /// Dispose the GL pieces this stack OWNS (everything created in /// ). + are caller-owned @@ -61,10 +63,6 @@ internal sealed record RenderStack( new("lighting UBO", LightingUbo.Dispose), new("UI host", UiHost.Dispose), ]), - new ResourceShutdownStage("GPU device (RHI)", - [ - new("GPU device", GpuDevice.Dispose), - ]), new ResourceShutdownStage("frame flight owner", [ new("frame flights", FrameFlights.Dispose), @@ -73,17 +71,17 @@ internal sealed record RenderStack( } /// - /// Resolves a sprite id (0x06xxxxxx) to a (texture-table slot, width, height) triple. - /// Copied verbatim from GameWindow's ResolveChrome closure — it calls + /// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple. + /// Copied verbatim from GameWindow's ResolveChrome closure — it calls /// TextureCache.GetOrUploadRenderSurface(id, out w, out h). /// - public (GpuTextureSlot handle, int width, int height) ResolveChrome(uint spriteId) + public (uint handle, int width, int height) ResolveChrome(uint spriteId) { - GpuTextureSlot t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h); + uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h); return (t, w, h); } - // ── Font cache (per-stack, keyed by FontDid) ───────────────────────────── + // ── Font cache (per-stack, keyed by FontDid) ───────────────────────────── /// /// Cache of loaded dat fonts keyed by FontDid (0x40000000-range). @@ -95,7 +93,7 @@ internal sealed record RenderStack( /// /// Lazily load and cache a dat font by its FontDid. Returns null (and - /// caches null) when the Font DBObj is absent or has no foreground surface — + /// caches null) when the Font DBObj is absent or has no foreground surface — /// callers fall back to the global font in that case. /// /// Pre-seeds (0x40000000) and @@ -125,7 +123,7 @@ internal sealed record RenderStack( } /// Options for . -internal sealed record RenderBootstrapOptions( +public sealed record RenderBootstrapOptions( AcDream.UI.Abstractions.Settings.QualitySettings Quality, string DiagnosticsDirectory); @@ -133,12 +131,12 @@ internal sealed record RenderBootstrapOptions( /// Constructs the UI Studio's render stack from the production classes, /// in the same order as . /// -internal static class RenderBootstrap +public static class RenderBootstrap { /// /// Build the studio's render stack. Throws /// (same message as GameWindow) if GL_ARB_bindless_texture or - /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory. + /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory. /// public static RenderStack Create( GL gl, @@ -167,15 +165,8 @@ internal static class RenderBootstrap // --- TextureCache (GameWindow ~1774) --- var frameFlights = new GpuFrameFlightController(gl); - // Campaign V slice V4a: the RHI device UI Studio's TextureCache/UiHost - // now need for their retained-UI texture/pipeline path, constructed - // the same way HostInputCameraCompositionPhase does for the main - // GameWindow (V1). - var gpuDevice = new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir); - var frameLifetime = new GpuDeviceFrameLifetime(gpuDevice); var textureCache = new TextureCache( gl, - gpuDevice, dats, bindless, frameFlights, @@ -210,7 +201,7 @@ internal static class RenderBootstrap return new AcDream.Core.Physics.AnimationSequencer( setup, mtable, capturedAnimLoader); } - // Setup exists but no motion table — no-op sequencer. + // Setup exists but no motion table — no-op sequencer. return new AcDream.Core.Physics.AnimationSequencer( setup, new DatReaderWriter.DBObjs.MotionTable(), @@ -228,10 +219,10 @@ internal static class RenderBootstrap var entitySpawnAdapter = new Wb.EntitySpawnAdapter( textureCache, SequencerFactory, meshAdapter); - // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) --- + // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) --- var classificationCache = new Wb.EntityClassificationCache(); - // --- TranslucencyFadeManager (GameWindow — field initializer, new()) --- + // --- TranslucencyFadeManager (GameWindow — field initializer, new()) --- var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager(); // --- WbDrawDispatcher (GameWindow ~2377-2381) --- @@ -246,13 +237,13 @@ internal static class RenderBootstrap // --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text. // The default font (0x40000000, 16px) renders the row names too small; the 18px // variant (confirmed in client_portal.dat 2026-06-26) matches the retail character - // window list more closely (≈ icon height ≈ 24px target, 18px is best available). + // window list more closely (≈ icon height ≈ 24px target, 18px is best available). var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u); // --- UiHost (GameWindow ~1790); pass null for debugFont (only used as - // a fallback BitmapFont for the world-space HUD — not needed for the + // a fallback BitmapFont for the world-space HUD — not needed for the // UI Studio, and BitmapFont requires a system font byte array) --- - var uiHost = new AcDream.App.UI.UiHost(gpuDevice, () => frameLifetime.Current, defaultFont: null); + var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null); var stack = new RenderStack( Gl: gl, @@ -270,8 +261,6 @@ internal static class RenderBootstrap LargeDatFont: largeDatFont) { FrameFlights = frameFlights, - GpuDevice = gpuDevice, - FrameLifetime = frameLifetime, }; // Pre-seed the font cache with the two already-uploaded atlas instances diff --git a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs index a3531334..df5f0e0a 100644 --- a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs +++ b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs @@ -38,52 +38,6 @@ internal interface IRenderFrameLifetime void EndFrame(); } -/// -/// Campaign V slice V4a: the current frame's , readable -/// by any ported renderer that needs to allocate a ring or open a pass — the -/// structural piece this slice adds so TextRenderer/DebugLineRenderer -/// have somewhere to reach the frame lifecycle already bracketing every render -/// callback ('s ). -/// -internal interface ICurrentGpuFrameSource -{ - /// The frame opened by the most recent . Throws if none is open. - IGpuFrame Current { get; } -} - -/// -/// Wires / into -/// the same bracket -/// occupied before this slice — additive, not a frame-graph restructuring: -/// already calls the frame-flight -/// controller's BeginFrame internally, so this type OWNS the -/// slot rather than running alongside the -/// controller (which would double-begin the same flight fence). Clears, -/// framebuffer management, and every non-ported renderer's own per-frame -/// bracket are untouched — slice V4h formalizes the rest of the frame spine. -/// -internal sealed class GpuDeviceFrameLifetime : IRenderFrameLifetime, ICurrentGpuFrameSource -{ - private readonly IGpuDevice _device; - private IGpuFrame? _current; - - public GpuDeviceFrameLifetime(IGpuDevice device) => - _device = device ?? throw new ArgumentNullException(nameof(device)); - - public IGpuFrame Current => _current - ?? throw new InvalidOperationException( - "No GPU frame is open — BeginFrame must run before Current is read."); - - public void BeginFrame() => _current = _device.BeginFrame(); - - public void EndFrame() - { - IGpuFrame frame = Current; - _current = null; - frame.End(); - } -} - internal interface IRenderFrameResourcePhase { void Prepare(RenderFrameInput input); diff --git a/src/AcDream.App/Rendering/RenderFrameResourceController.cs b/src/AcDream.App/Rendering/RenderFrameResourceController.cs index db714e99..03d4792d 100644 --- a/src/AcDream.App/Rendering/RenderFrameResourceController.cs +++ b/src/AcDream.App/Rendering/RenderFrameResourceController.cs @@ -88,6 +88,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour private readonly WbDrawDispatcher? _dispatcher; private readonly EnvCellRenderer? _environmentCells; private readonly PortalDepthMaskRenderer? _portalDepth; + private readonly TextRenderer? _worldText; + private readonly TextRenderer? _uiText; private readonly ClipFrame? _clip; private readonly TerrainModernRenderer? _terrain; private readonly SceneLightingUboBinding? _lighting; @@ -96,6 +98,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour WbDrawDispatcher? dispatcher, EnvCellRenderer? environmentCells, PortalDepthMaskRenderer? portalDepth, + TextRenderer? worldText, + TextRenderer? uiText, ClipFrame? clip, TerrainModernRenderer? terrain, SceneLightingUboBinding? lighting) @@ -104,6 +108,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour _dispatcher = dispatcher; _environmentCells = environmentCells; _portalDepth = portalDepth; + _worldText = worldText; + _uiText = uiText; _clip = clip; _terrain = terrain; _lighting = lighting; @@ -116,11 +122,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour _dispatcher?.BeginFrame(gpuSlot); _environmentCells?.BeginFrame(gpuSlot); _portalDepth?.BeginFrame(gpuSlot); - // TextRenderer (world-hud + retained UI) is off this per-slot int - // pattern as of Campaign V slice V4a — it allocates rings directly - // from the current IGpuFrame at Flush time, with no separate begin - // step (GpuDeviceFrameLifetime resets the device's ring watermark - // when IGpuDevice.BeginFrame runs). + _worldText?.BeginFrame(gpuSlot); + _uiText?.BeginFrame(gpuSlot); _clip?.BeginFrame(gpuSlot); _terrain?.BeginFrame(gpuSlot); _lighting?.BeginFrame(gpuSlot); diff --git a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs index 4acb4474..121802d6 100644 --- a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs +++ b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.Rendering.Residency; @@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Residency; /// Startup-time residency ceilings. Values preserve the pre-Slice-D cache /// behavior by default and are changed atomically as one profile. /// -internal sealed record ResidencyBudgetOptions( +public sealed record ResidencyBudgetOptions( long ObjectMeshGpuBytes, int ObjectMeshUnownedEntries, long PreparedMeshCpuBytes, diff --git a/src/AcDream.App/Rendering/RetailChaseCamera.cs b/src/AcDream.App/Rendering/RetailChaseCamera.cs index af2115f5..c2eefc8b 100644 --- a/src/AcDream.App/Rendering/RetailChaseCamera.cs +++ b/src/AcDream.App/Rendering/RetailChaseCamera.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.Core.Rendering; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// docs/research/named-retail/acclient_2013_pseudo_c.txt:95505): /// a STATEFUL sought position that converges from the current swept /// viewer toward the desired boom pose (CameraManager::UpdateCamera -/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame +/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame /// velocity-averaged slope-aligned heading frame, mouse-input low-pass /// filter. Pseudocode: /// docs/research/2026-07-06-camera-sought-position-pseudocode.md. @@ -27,7 +27,7 @@ namespace AcDream.App.Rendering; /// Spec: docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md. /// /// -internal sealed class RetailChaseCamera : ICamera +public sealed class RetailChaseCamera : ICamera { // ICamera surface. public Vector3 Position { get; private set; } @@ -35,19 +35,19 @@ internal sealed class RetailChaseCamera : ICamera /// /// The cell the collided viewer-sphere ended in (retail viewer_cell = /// sphere_path.curr_cell). Roots the render mode + indoor visibility + the portal - /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint. + /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint. /// Equals the passed player cell when camera collision is off / the probe is null. /// public uint ViewerCellId { get; private set; } public float Aspect { get; set; } = 16f / 9f; public float FovY { get; set; } = MathF.PI / 3f; public Matrix4x4 View { get; private set; } = Matrix4x4.Identity; - // Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 — - // Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)). + // Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 — + // Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)). // MUST be smaller than the 0.3 m camera-collision sphere (PhysicsCameraCollisionProbe. // ViewerSphereRadius): with a 1.0 m near, a wall the collided eye sits 0.3 m from - // falls INSIDE the near plane and is clipped away — pressing the camera into a corner - // let you see straight through the wall (§4 corner residual). History: 0.1 landed + // falls INSIDE the near plane and is clipped away — pressing the camera into a corner + // let you see straight through the wall (§4 corner residual). History: 0.1 landed // (137b4f2), was reverted (8bd3492) after correlating with missing indoor textures, // and re-landed once #110 resolved: the textures were the pre-existing #105 // staged-texture-flush drop (WbMeshAdapter.Tick), and 0.1 merely raised its trigger @@ -55,12 +55,12 @@ internal sealed class RetailChaseCamera : ICamera public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f); - // ── Public tunables (per-instance) ────────────────────────────── + // ── Public tunables (per-instance) ────────────────────────────── - /// Length of the viewer_offset vector. Retail default ≈ 2.61. + /// Length of the viewer_offset vector. Retail default ≈ 2.61. public float Distance { get; set; } = 2.61f; - /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°). + /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°). public float Pitch { get; set; } = 0.291f; /// @@ -92,27 +92,27 @@ internal sealed class RetailChaseCamera : ICamera public const float PitchMax = 1.4f; // Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp - // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 × - // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail + // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 × + // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail // freezes the boom at an exact fixed point (0x00456fe1). RotCloseEpsilon = - // 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the + // 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the // snap, Vector3.Lerp asymptotes forever and the boom drifts at rest, walking the eye - // across a portal plane and flipping the viewer cell → the indoor flicker. + // across a portal plane and flipping the viewer cell → the indoor flicker. private const float SnapEpsilon = 0.000199999995f * 2f; private const float RotCloseEpsilon = 0.000199999995f; - // ── Stateful camera state (retail SmartBox's two Positions) ───── + // ── Stateful camera state (retail SmartBox's two Positions) ───── // - // _soughtEye = retail viewer_sought_position — the persisted sweep + // _soughtEye = retail viewer_sought_position — the persisted sweep // TARGET, re-derived each frame from the current swept // viewer (NOT from itself). - // _publishedEye = retail viewer — the swept, published eye; the base + // _publishedEye = retail viewer — the swept, published eye; the base // of next frame's interpolation (SmartBox:: // PlayerPhysicsUpdatedCallback passes &this->viewer // into UpdateCamera, 0x00452d75). // _dampedForward = the sought's look direction. Sweeps translate but // never rotate, so the viewer's rotation is always the - // previous sought rotation — one field serves both. + // previous sought rotation — one field serves both. private readonly Vector3[] _velocityRing = new Vector3[5]; private int _velocityCount; @@ -121,12 +121,12 @@ internal sealed class RetailChaseCamera : ICamera private Vector3 _dampedForward = new(1f, 0f, 0f); private bool _initialised; - // Mouse-filter state — shared by FilterMouseDelta entrypoint. + // Mouse-filter state — shared by FilterMouseDelta entrypoint. private float _lastMouseDeltaX; private float _lastMouseDeltaY; private float _lastFilterTimeSec; - // ── Per-frame entry point ──────────────────────────────────────── + // ── Per-frame entry point ──────────────────────────────────────── /// /// Advance the camera one frame. Caller passes the player's current @@ -177,7 +177,7 @@ internal sealed class RetailChaseCamera : ICamera // 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera // (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the // desired pose and assigns the result to viewer_sought_position - // (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep + // (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep // target converges onto whatever the collision produced last frame // and re-extends gradually. The full-length ideal boom is never swept // directly. Pseudocode: @@ -193,15 +193,15 @@ internal sealed class RetailChaseCamera : ICamera { float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, dt); float rAlpha = ComputeDampingAlpha(CameraDiagnostics.RotationStiffness, dt); - // interpolate_origin(viewer.frame → desired, t) — the lerp base is the + // interpolate_origin(viewer.frame → desired, t) — the lerp base is the // VIEWER (0x00456fae), not the previous sought. The forward base is the - // viewer's rotation ≡ the previous sought forward (sweeps never rotate). + // viewer's rotation ≡ the previous sought forward (sweeps never rotate). Vector3 candidateEye = Vector3.Lerp(_publishedEye, targetEye, tAlpha); Vector3 candidateForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha)); - // Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step + // Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step // off the viewer is sub-epsilon in translation AND rotation, the sought - // parks EXACTLY ON the viewer — an exact fixed point instead of an + // parks EXACTLY ON the viewer — an exact fixed point instead of an // asymptote. Kills the at-rest drift AND the residual micro-jitter when // pressed against a wall. See ApplyConvergenceSnap + SnapEpsilon. (_soughtEye, _dampedForward, _) = @@ -209,13 +209,13 @@ internal sealed class RetailChaseCamera : ICamera } // 5b. Spring-arm collision (A8.F / #180). Retail SmartBox::update_viewer - // (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position + // (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position // and publishes the swept result as the viewer (set_viewer(curr_pos, 0) - // — the sought is NOT reset on success). Pressed against a wall, the + // — the sought is NOT reset on success). Pressed against a wall, the // sweep ray extends only one interpolation step past the contact, so a - // knife-edge r±ε graze can move the eye by at most that step (sub-mm at + // knife-edge r±ε graze can move the eye by at most that step (sub-mm at // high fps) instead of re-solving the full-length boom with its 0.27 m - // bistable contact pair — the #180 strobe fix. + // bistable contact pair — the #180 strobe fix. Vector3 publishedEye = _soughtEye; // The viewer cell defaults to the player cell (collision off / null probe); the sweep // overwrites it with the swept cell (retail viewer_cell). Always set so GameWindow has a @@ -227,13 +227,13 @@ internal sealed class RetailChaseCamera : ICamera publishedEye = swept.Eye; ViewerCellId = swept.ViewerCellId; // Total-failure fallback = retail set_viewer(player_pos, reset_sought=1) - // (update_viewer :92886 and the cell==0 bail :92775 — both surface here as + // (update_viewer :92886 and the cell==0 bail :92775 — both surface here as // ViewerCellId == 0): the sought resets to the returned position and // re-extends from there. if (swept.ViewerCellId == 0) _soughtEye = swept.Eye; } - // Retail viewer — the base of next frame's interpolation (step 5). + // Retail viewer — the base of next frame's interpolation (step 5). _publishedEye = publishedEye; // 6. Publish renderer surface (from the collided eye; rotation stays the @@ -241,7 +241,7 @@ internal sealed class RetailChaseCamera : ICamera Position = publishedEye; View = Matrix4x4.CreateLookAt(publishedEye, publishedEye + _dampedForward, new Vector3(0f, 0f, 1f)); - // 7. Auto-fade translucency — uses the published (collided) eye so the + // 7. Auto-fade translucency — uses the published (collided) eye so the // player fades once the eye is pulled in close. float d = Vector3.Distance(publishedEye, pivotWorld); PlayerTranslucency = ComputeTranslucency(d); @@ -295,12 +295,12 @@ internal sealed class RetailChaseCamera : ICamera /// public (float outX, float outY) FilterMouseDelta(float rawX, float rawY, float weight, float nowSec) { - // X first — advances the shared timestamp. + // X first — advances the shared timestamp. float x = FilterMouseAxis(rawX, weight, nowSec, ref _lastMouseDeltaX, ref _lastFilterTimeSec, CameraDiagnostics.MouseLowPassWindowSec); // Y uses a throwaway timestamp so the within-window check still uses the original delta // (X already advanced _lastFilterTimeSec to nowSec; if Y reused it, the within-window - // check would be 0 < windowSec which is always true — which is what we want here, since + // check would be 0 < windowSec which is always true — which is what we want here, since // both axes are sampled simultaneously and should both blend.). float yTimeShadow = _lastFilterTimeSec - 1f; // force within-window path for the Y axis float y = FilterMouseAxis(rawY, weight, nowSec, @@ -308,7 +308,7 @@ internal sealed class RetailChaseCamera : ICamera return (x, y); } - // Math primitives — pure, internal-static for unit-testability. + // Math primitives — pure, internal-static for unit-testability. /// /// Pick the heading vector that drives the camera basis. Mirrors @@ -316,8 +316,8 @@ internal sealed class RetailChaseCamera : ICamera /// path (decomp acclient_2013_pseudo_c.txt:95644-95795): /// /// Base heading is the player's facing - /// direction in world space — (cos yaw, sin yaw, 0) - /// — not the velocity vector. Velocity only gates whether + /// direction in world space — (cos yaw, sin yaw, 0) + /// — not the velocity vector. Velocity only gates whether /// slope-alignment fires. /// If is off /// OR the player's horizontal velocity is below epsilon (i.e. @@ -337,7 +337,7 @@ internal sealed class RetailChaseCamera : ICamera /// /// 5-frame averaged player velocity in world space. /// Player facing yaw + any orbit offset, radians. - /// Player's transient_state & 1 — does describe a valid contact plane? + /// Player's transient_state & 1 — does describe a valid contact plane? /// Player's current contact plane normal in world space; ignored when is false. /// User-tunable; when false skips the projection and returns the flat facing direction. internal static Vector3 ComputeHeading( @@ -356,15 +356,15 @@ internal sealed class RetailChaseCamera : ICamera // |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The // horizontal-magnitude-squared form is a cleaner equivalent. // Without this, the airborne path would still project against - // world up (no-op) which is fine — but the standing-jump case + // world up (no-op) which is fine — but the standing-jump case // wants the historical `direction` fallback that retail uses. float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y; if (hMagSq < 1e-4f) return baseHeading; // Pick the projection plane normal: - // grounded → contact_plane.N (slope-aligned camera basis) - // airborne → world up (projection becomes a no-op because - // baseHeading is already in the XY plane — but + // grounded → contact_plane.N (slope-aligned camera basis) + // airborne → world up (projection becomes a no-op because + // baseHeading is already in the XY plane — but // keeping the code path uniform makes the airborne // case impossible to swing vertically). Vector3 normal; @@ -375,13 +375,13 @@ internal sealed class RetailChaseCamera : ICamera // Project baseHeading onto plane perpendicular to normal: // projected = forward - normal * dot(forward, normal) - // On flat ground this is a no-op (dot ≈ 0). On a slope the + // On flat ground this is a no-op (dot ≈ 0). On a slope the // projected vector gains a Z component matching the slope angle, // which tilts the camera basis with the terrain. float dot = Vector3.Dot(baseHeading, normal); Vector3 projected = baseHeading - normal * dot; - // Degenerate: facing nearly parallel to normal (rare — would + // Degenerate: facing nearly parallel to normal (rare — would // require player rotated to face into the ground). Fall back to // the unprojected base heading. if (projected.LengthSquared() < 1e-4f) return baseHeading; @@ -449,7 +449,7 @@ internal sealed class RetailChaseCamera : ICamera Vector3 right; if (MathF.Abs(forward.Z) > 0.99f) { - // Near-vertical forward — use world +X as the secondary axis. + // Near-vertical forward — use world +X as the secondary axis. right = Vector3.Normalize(Vector3.Cross(forward, new Vector3(1f, 0f, 0f))); } else @@ -495,7 +495,7 @@ internal sealed class RetailChaseCamera : ICamera /// /// Exponential-damping rate per frame. /// alpha = clamp(stiffness * dt * 10, 0, 1). At - /// stiffness=0.45, dt=1/60 → ~0.075 + /// stiffness=0.45, dt=1/60~0.075 /// (~150 ms half-life). Matches retail's /// x_1 = stiffness * dt * 10 formulation. /// @@ -508,11 +508,11 @@ internal sealed class RetailChaseCamera : ICamera } /// - /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035). + /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035). /// After the per-frame lerp, if the translation step from /// (the interpolation base = the current swept viewer) to /// is below AND the rotation step is below - /// , retail returns the VIEWER unchanged — the sought + /// , retail returns the VIEWER unchanged — the sought /// parks exactly on it (return viewer, 0x00457025). Returns frozen=true /// with the viewer state in that case; otherwise frozen=false with the candidate. /// Both conditions are required (retail couples origin + rotation in the test), @@ -562,7 +562,7 @@ internal sealed class RetailChaseCamera : ICamera /// distance. 0 = fully opaque, 1 = fully transparent. /// Opaque at and beyond 0.45 m; fully transparent at and within /// 0.20 m; linear ramp between. Matches retail's CameraSet:: - /// UpdateCamera distance check (decomp :97703–97725). + /// UpdateCamera distance check (decomp :97703–97725). /// internal static float ComputeTranslucency(float distance) { diff --git a/src/AcDream.App/Rendering/RetailCursorManager.cs b/src/AcDream.App/Rendering/RetailCursorManager.cs index 1c49d6f9..1a73b39d 100644 --- a/src/AcDream.App/Rendering/RetailCursorManager.cs +++ b/src/AcDream.App/Rendering/RetailCursorManager.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI; +using AcDream.App.UI; using AcDream.Core.Textures; using DatReaderWriter; using AcDream.Content; @@ -9,7 +9,7 @@ using Silk.NET.Input; namespace AcDream.App.Rendering; /// Applies retail cursor feedback to Silk using dat MediaDescCursor art when available. -internal sealed class RetailCursorManager +public sealed class RetailCursorManager { private readonly IDatReaderWriter _dats; private readonly object _datLock; diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index e0d37eab..9b515852 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Scene; @@ -11,7 +11,7 @@ namespace AcDream.App.Rendering; /// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside -> /// PView::DrawInside -> ConstructView -> DrawCells. /// -internal sealed class RetailPViewRenderer +public sealed class RetailPViewRenderer { private readonly InteriorEntityPartition.IObserver? _partitionObserver; private readonly ICurrentRenderPViewObserver? _candidateObserver; @@ -44,7 +44,7 @@ internal sealed class RetailPViewRenderer private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new(); // #124: per-building look-in frames under an INTERIOR root, drawn as a - // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the + // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the // main frame (see DrawInside). Rebuilt each interior-root frame. private readonly List _lookInFrames = new(); private readonly Stack _lookInFramePool = new(); @@ -59,7 +59,7 @@ internal sealed class RetailPViewRenderer // MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/ // Dynamics), reused across frames instead of `new`ing a Result (a Dictionary // + 2 Lists, plus one List per visible cell) every DrawInside - // call. See InteriorEntityPartition.Partition(Result, ...) — clears in + // call. See InteriorEntityPartition.Partition(Result, ...) — clears in // place and reuses each cell's list across frames when the cell stays // visible. // Slice G4: this is now a diagnostic/fallback oracle only. Normal @@ -88,7 +88,7 @@ internal sealed class RetailPViewRenderer } // T2 (BR-4): retail has NO distance constant on the flood-admission chain - // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + + // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + // GetClip + GetVisible only). The old 48 m seed cap is replaced by the // caller's per-building frustum pre-gate on aperture bounds (GameWindow's // gather); seeds themselves are unbounded. @@ -113,28 +113,28 @@ internal sealed class RetailPViewRenderer drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ, reuseFrame: _mainPortalFrameScratch); - // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge + // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge // the small (~2-cell) per-building views into the frame. Retail reaches building interiors via // the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the // land root itself has no portals (it floods nothing into buildings). Per-building seeding is - // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood + // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood // oscillated as the chase eye grazed a doorway (the indoor flap). if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null) MergeNearbyBuildingFloods(ctx, pvFrame); // #124: interior-root building look-ins. Retail runs the look-in INSIDE - // the landscape stage for ANY root — LScape::draw is the FIRST call of + // the landscape stage for ANY root — LScape::draw is the FIRST call of // DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth // clear (pc:432732) and the exit-portal seals (pc:432785); a far // building seen through our doorway floods clipped to the INSTALLED // outside view (GetClip vs current view, ConstructView(CBldPortal) // 0x005a59a0). These frames therefore draw in DrawBuildingLookIns - // (inside the landscape stage), NEVER merged into the main frame — a + // (inside the landscape stage), NEVER merged into the main frame — a // merged cell would draw post-clear and z-fail against the root's seal // (its geometry is beyond the door plane). The eye-side seed test // self-excludes the root's own building (the eye is on its interior // side). Outdoor roots keep the MergeNearbyBuildingFloods path above - // (no depth clear under outdoor roots — the merged form is equivalent + // (no depth clear under outdoor roots — the merged form is equivalent // there). if (!ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null @@ -149,7 +149,7 @@ internal sealed class RetailPViewRenderer // R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the // assembler handed a clip-slot. This feeds the Prepare filter + entity partition, - // so every visible cell's shell has a prepared batch and seals — killing the grey + // so every visible cell's shell has a prepared batch and seals — killing the grey // (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells). // Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained). _drawableCellsScratch.Clear(); @@ -158,7 +158,7 @@ internal sealed class RetailPViewRenderer passes.UseIndoorMembershipOnlyRouting(); // #124: look-in cells need prepared shell batches + their statics routed - // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main + // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main // cell-object pass iterates pvFrame.OrderedVisibleCells, which never // contains them). drawableCells itself stays the MAIN flood: it feeds the // seals, the outside-stage predicate, and the frame result. @@ -174,22 +174,22 @@ internal sealed class RetailPViewRenderer } // (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran - // here was the seam-floor flicker mechanism — retail's visible_cell_table is - // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool + // here was the seam-floor flicker mechanism — retail's visible_cell_table is + // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool // is built once per frame in GameWindow, player-anchored.) passes.PrepareCellBatches(ctx, prepareCells); - // T1 (fused BR-2/3): retail's frame order — static world, then the - // aperture depth writes, then interior cells WHOLE far→near, then + // T1 (fused BR-2/3): retail's frame order — static world, then the + // aperture depth writes, then interior cells WHOLE far→near, then // per-cell statics, then ALL dynamics last (retail draws objects after // cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0). // The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is - // DELETED — retail never clips cell geometry; aperture exactness comes + // DELETED — retail never clips cell geometry; aperture exactness comes // from the punch/seal depth writes + the z-buffer, and the dynamics- // last order is what makes the punch safe (the first BR-2 attempt // punched after dynamics and erased the player, reverted 88be519). - // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view, + // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view, // never clipped (Ghidra 0x0054c250). Built once per frame from the // assembled slices + this frame's view-projection. var viewcone = ViewconeCuller.Build( @@ -256,19 +256,19 @@ internal sealed class RetailPViewRenderer passes.EmitDiagnostics(ctx, result); // #118: stage assignment for dynamics under an INTERIOR root. Retail - // draws the OUTSIDE world's objects inside the landscape stage — + // draws the OUTSIDE world's objects inside the landscape stage — // PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated // full depth clear (pc:432731-432732) and the exit-portal SEALS // (pc:432785-432786); DrawBlock draws every landcell's objects via // DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our // single last pass instead z-fails against the seal's true-depth stamp - // the moment it stands beyond the door plane — the house-exit + // the moment it stands beyond the door plane — the house-exit // clip+vanish (pinned by HouseExitWalkReplayTests). So under an // interior root: outdoor-classified dynamics draw in the outside // stage; an indoor dynamic whose sphere STRADDLES an exit portal // draws in BOTH stages (retail's per-overlapped-cell shadow-part // draw, DrawBlock pc:430056-430064) so neither body half clips at the - // plane. Outdoor roots keep ALL dynamics in the last pass — our + // plane. Outdoor roots keep ALL dynamics in the last pass — our // z-buffered equivalent of retail's painter-ordered outdoor pass (the // BR-2 punch-after-dynamics lesson, reverted 88be519). _outsideStageDynamics.Clear(); @@ -362,11 +362,11 @@ internal sealed class RetailPViewRenderer // on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0; // a cell visible through two apertures holds two views, all consumed // downstream). The old first-wins (`ContainsKey -> continue`) dropped the - // second building flood's views whenever a cell was already in the frame — + // second building flood's views whenever a cell was already in the frame — // the multiview-loss-first-wins divergence (a named #109 suspect: per-frame // winner flips between apertures). CellView.Add dedups exact/collinear // re-emissions (the dac8f6a CanonicalKey), so unioning is convergent. - // OutsideView is NOT merged — the outdoor root already seeds full-screen + // OutsideView is NOT merged — the outdoor root already seeds full-screen // terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView // empty (it stops at exit portals once inside the building). private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src) @@ -393,8 +393,8 @@ internal sealed class RetailPViewRenderer } // #124: per-building look-in floods for an INTERIOR root, seeded clipped - // against the OutsideView (retail: GetClip runs under the INSTALLED view — - // the accumulated doorway region — so a far building floods only within the + // against the OutsideView (retail: GetClip runs under the INSTALLED view — + // the accumulated doorway region — so a far building floods only within the // doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip // 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own // building self-excludes via the seed eye-side test. @@ -445,15 +445,15 @@ internal sealed class RetailPViewRenderer private void ResetBuildingGroups() => _buildingGroups.Reset(); - // #124: draw the interior-root look-ins INSIDE the landscape stage — - // retail's placement (LScape::draw → DrawBlock → DrawSortCell → + // #124: draw the interior-root look-ins INSIDE the landscape stage — + // retail's placement (LScape::draw → DrawBlock → DrawSortCell → // DrawBuilding runs as the FIRST call of DrawCells' outside-view branch, // pc:432719, before the depth clear + seals). Per building: punch ALL - // apertures first (retail finishes build_draw_portals_only pass 1 — the - // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods), - // then draw the flooded cells' shells + statics far→near (the nested + // apertures first (retail finishes build_draw_portals_only pass 1 — the + // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods), + // then draw the flooded cells' shells + statics far→near (the nested // DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is - // empty by construction — PView ctor draw_landscape=0 — so no recursive + // empty by construction — PView ctor draw_landscape=0 — so no recursive // landscape/clear/seal). Anything rasterized outside an aperture is // repainted by the root's own shells after the depth clear, so over-draw // here is color-safe; statics draw whole (the main viewcone has no entry @@ -491,12 +491,12 @@ internal sealed class RetailPViewRenderer } } - // Pass 2: shells + statics, far→near. + // Pass 2: shells + statics, far→near. passes.UseIndoorMembershipOnlyRouting(); // Opaque shells batched per building into ONE Render (this building's // aperture punches above already ran; z-buffer handles order and - // lighting is per-instance CellId-keyed) — was one heavy per-frame + // lighting is per-instance CellId-keyed) — was one heavy per-frame // Render per cell. Per-cell entity/particle work stays in the loop. _shellBatch.Clear(); foreach (uint cid in frame.OrderedVisibleCells) @@ -509,7 +509,7 @@ internal sealed class RetailPViewRenderer uint cellId = frame.OrderedVisibleCells[i]; _oneCell.Clear(); _oneCell.Add(cellId); - // Opaque shell batched above. Transparent stays per-cell (far→near) + // Opaque shell batched above. Transparent stays per-cell (far→near) // for correct compositing; skipped for opaque-only cells. if (passes.CellHasTransparentShell(cellId)) passes.DrawTransparentCellShells(_oneCell); @@ -523,7 +523,7 @@ internal sealed class RetailPViewRenderer // #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the // Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE - // under an interior root — DrawDynamicsLast viewcone-culls + // under an interior root — DrawDynamicsLast viewcone-culls // them (the main cone has no entries for look-in cells), and // post-clear they would z-fail against the root's seal anyway // (the #118 lesson). Retail draws a look-in cell's objects @@ -576,7 +576,7 @@ internal sealed class RetailPViewRenderer _cellStaticScratch, _oneCell); - // The cell-particles pass for look-in cells — retail's + // The cell-particles pass for look-in cells — retail's // nested DrawCells draws objects WITH their emitters. foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext( @@ -600,7 +600,7 @@ internal sealed class RetailPViewRenderer // #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha // draws of the landscape stage and flushes them ONCE after LScape::draw - // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent + // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent // landscape content (portal swirl meshes, flame particles) composites // AFTER the building look-ins. Our dispatcher draws translucency inside // each Draw call, so the stage is split in TWO phases instead: EARLY = @@ -609,12 +609,12 @@ internal sealed class RetailPViewRenderer // LATE = outside-stage dynamics' meshes + ALL scene particles + // weather. Content drawn early and overlapped by a look-in aperture // was otherwise overpainted by the far interior (translucents write no - // depth to protect themselves) — the portal-swirl/candle-flame class. + // depth to protect themselves) — the portal-swirl/candle-flame class. int probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { passes.SetTerrainClip(slice.Planes); - // T3 (BR-5): entities are never hard-clipped — retail viewcone- + // T3 (BR-5): entities are never hard-clipped — retail viewcone- // CHECKS each mesh's sphere against the view (Ghidra 0x0054c250) // and draws it whole. The old per-slice entity clip routing // (gl_ClipDistance via SetClipRouting) is replaced by the sphere @@ -663,7 +663,7 @@ internal sealed class RetailPViewRenderer }); } - // #124: far-building look-ins draw HERE — still inside the landscape + // #124: far-building look-ins draw HERE — still inside the landscape // stage (their punches mark against the terrain/exterior depth just // drawn), strictly BEFORE the depth clear + seals below, matching // retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785). @@ -675,11 +675,11 @@ internal sealed class RetailPViewRenderer frameEntityPasses, in frameView); - // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn + // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn // pre-clear so the seal protects their aperture pixels; AFTER the // look-ins so a translucent portal mesh blends over a far interior // instead of being overpainted) + the scene-particle owners (statics + - // dynamics cone survivors — flames ride here for the same reason). + // dynamics cone survivors — flames ride here for the same reason). probeSliceIndex = 0; foreach (var slice in clipAssembly.OutsideViewSlices) { @@ -700,7 +700,7 @@ internal sealed class RetailPViewRenderer if (ownerPass) _lateParticleOwnerScratch.Add(e.Id); // #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids - // double as an ENTITY-id watchlist here — one line per watched + // double as an ENTITY-id watchlist here — one line per watched // outdoor-static owner per CHANGE of its cone verdict. passes.EmitOutStageOwner( e, @@ -764,11 +764,11 @@ internal sealed class RetailPViewRenderer }); } - // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, + // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, // campfires, ground effects anchored at a position) have no owner id // to ride any of the id-filtered particle passes. The outdoor root // has the dedicated T3 pass for them; an INTERIOR root had NO pass - // at all. Draw them ONCE per frame (not per slice — alpha particles + // at all. Draw them ONCE per frame (not per slice — alpha particles // must not double-draw, the #121 lesson), at the END of the landscape // stage: after the clear they would z-fail against the doorway seal. if (!ctx.RootCell.IsOutdoorNode) @@ -780,7 +780,7 @@ internal sealed class RetailPViewRenderer passes.FlushLandscapeAlpha(); // T1: retail clears the FULL depth buffer ONCE between the outside - // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — + // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 — // Clear gated on portalsDrawnCount; exact gate semantics is a plan // open question, staged as "any outside slice drawn"), then re-stamps // every outside-leading portal's TRUE depth (the seals, @@ -819,20 +819,20 @@ internal sealed class RetailPViewRenderer IRetailPViewPassExecutor passes, PortalVisibilityFrame pvFrame) { - // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's - // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once. + // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's + // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once. // Retail NEVER clips cell geometry: the production path is the // prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the // planeMask=0xffffffff legacy submit means skip-all-edges), and // aperture exactness comes from the punch/seal depth writes + the // z-buffer + this order. The former gl_ClipDistance chop // (927fd8f/9ce335e, #114) is deleted with this rewrite. - // Per-cell opaque+transparent keeps the far→near transparent + // Per-cell opaque+transparent keeps the far→near transparent // compositing the per-cell loop already provided. passes.UseIndoorMembershipOnlyRouting(); // Opaque: ONE batched Render for all shell cells (was one heavy per-frame - // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at + // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at // Arwic). Opaque needs no draw order (z-buffer), and lighting is // per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI- // Internal), so cross-cell batching is visually identical. The filtered @@ -856,16 +856,16 @@ internal sealed class RetailPViewRenderer passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells); } - // T1: the frame's single LAST entity pass — ALL server-spawned dynamics + // T1: the frame's single LAST entity pass — ALL server-spawned dynamics // (player, NPCs, doors, items), indoor or out, drawn after the static // world + punches + interior cells. Depth-tested, never hard-clipped - // (retail draws objects per cell AFTER cells and viewcone-culls them — + // (retail draws objects per cell AFTER cells and viewcone-culls them — // PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is // T3). Drawing dynamics last is what makes the aperture punch safe. - // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its + // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its // cell's views; outdoor/unresolved vs the outside views (pass-all under // the outdoor root's full-screen outside view). A dynamic in a NON-flooded - // room culls HERE — retail never reaches an object whose cell is not in + // room culls HERE — retail never reaches an object whose cell is not in // the draw list; the partition keeps routing it so the CULL (not the // visibility set) drops it, exactly retail's shape. private void DrawDynamicsLast( @@ -928,12 +928,12 @@ internal sealed class RetailPViewRenderer && AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0 && e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid; // #118: under an interior root, outdoor-classified dynamics drew in - // the outside stage (pre-clear, seal-protected) — retail draws them + // the outside stage (pre-clear, seal-protected) — retail draws them // via LScape::draw's per-landcell DrawSortCell, never in the // post-seal cell-object epilogue (PView::DrawCells pc:432719 vs // pc:432878). Drawing them here instead z-fails them against the // seal. Indoor dynamics (incl. exit-portal straddlers, which drew - // in BOTH stages) stay — this pass is retail's loop C. + // in BOTH stages) stay — this pass is retail's loop C. if (!rootIsOutdoor && !indoor) { if (isProbePlayer) @@ -976,12 +976,12 @@ internal sealed class RetailPViewRenderer visibleCellIds: null); // #121: dynamics' attached emitters (portal swirls, creature effects) - // gate through the SAME cone-surviving owner set as their meshes — + // gate through the SAME cone-surviving owner set as their meshes — // retail draws emitters with the owner object. Before this callback, // dynamics' emitters fell through EVERY particle filter under the pview // path (the landscape slice carries outdoor statics + #118 outside- // stage dynamics; the cell callback carries cell statics; T4 deleted - // the old clipRoot==null global pass from normal frames) — all world + // the old clipRoot==null global pass from normal frames) — all world // portals went invisible. Outside-stage dynamics are excluded here: // their emitters already drew in the landscape slice (alpha-blended // particles must not double-draw, unlike the depth-idempotent meshes). @@ -1049,29 +1049,29 @@ internal sealed class RetailPViewRenderer return; } - // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — - // dynamics moved to DrawDynamicsLast. Far→near with the cells, after - // the shells (retail DrawCells epilogue: PortalList = cell's views → + // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — + // dynamics moved to DrawDynamicsLast. Far→near with the cells, after + // the shells (retail DrawCells epilogue: PortalList = cell's views → // DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is - // tested against ITS CELL's views (retail viewconeCheck) — the + // tested against ITS CELL's views (retail viewconeCheck) — the // statics-through-walls fix: a static whose sphere is outside every // view of its cell no longer paints through the wall (the cottage // phantom staircase's draw path). // Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching): // the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic // (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs + - // full state setup). Collapse them into ONE cross-cell batched draw — the + // full state setup). Collapse them into ONE cross-cell batched draw — the // shipped cells-shell batching pattern applied to cell OBJECTS. Two loops // preserve the statics-before-particles depth order: loop 1 culls + // accumulates every cell's survivors and draws them once; loop 2 runs the // per-cell particle passes AFTER the statics own the depth buffer (particles // depth-test but write no depth). The dispatcher sorts opaque front-to-back // and transparent back-to-front by group distance (WbDrawDispatcher.cs: - // 1469-1470), so cross-cell batching composites correctly — equal-or-better + // 1469-1470), so cross-cell batching composites correctly — equal-or-better // than the old per-cell-bucketed order. visibleCellIds = the union of cells, // so the dispatcher admits exactly the same survivor set. - // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells. + // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells. _allCellStatics.Clear(); _cellObjCells.Clear(); for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) @@ -1099,7 +1099,7 @@ internal sealed class RetailPViewRenderer } // ONE batched static-object draw for every visible cell (was N per-cell - // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics + // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics // draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against // ITS cell's views above (the statics-through-walls fix is preserved by the // cull; only the draw is batched). @@ -1124,20 +1124,20 @@ internal sealed class RetailPViewRenderer _cellObjCells); } - // Cell-particle pass — consolidated across ALL visible cells into ONE + // Cell-particle pass — consolidated across ALL visible cells into ONE // draw. Was per-cell, and each call re-walked the ENTIRE live particle set - // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every - // live emitter), i.e. O(cells × particles) — the dense-town cellobjects + // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every + // live emitter), i.e. O(cells × particles) — the dense-town cellobjects // sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION // (= _allCellStatics, already accumulated above for the batched draw) draws // EXACTLY the same emitters: the callback gates on owner id (the cone- // surviving set), the renderer sorts globally back-to-front, and the per- // cell slice was never used for clipping (the scissor gate was deleted in - // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after + // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after // the batched static draw so emitters depth-test against the statics now in // the buffer (the statics-before-particles order). cellId/slice are unused - // by the particle pass — pass NoClipSlice + the union owner list. This also - // drops the per-cell BuildDrawList allocations (N → 1). + // by the particle pass — pass NoClipSlice + the union owner list. This also + // drops the per-cell BuildDrawList allocations (N → 1). if (frameEntityPasses is not null || _allCellStatics.Count > 0) { @@ -1199,7 +1199,7 @@ internal sealed class RetailPViewRenderer private readonly List _cellStaticScratch = new(); private readonly List _dynamicsScratch = new(); // #118: dynamics assigned to the OUTSIDE stage this frame (interior roots - // only) — outdoor-classified + exit-portal straddlers. Cleared per frame. + // only) — outdoor-classified + exit-portal straddlers. Cleared per frame. private readonly List _outsideStageDynamics = new(); // Dense-town FPS iteration-1 (cellobject batching): all visible cells' // viewcone-surviving statics accumulated for ONE batched DrawEntityBucket, @@ -1274,17 +1274,17 @@ internal sealed class RetailPViewRenderer /// /// #118 stage assignment for a dynamic under an INTERIOR root: does it draw - /// in the OUTSIDE (landscape) stage — before the gated depth clear and the - /// exit-portal seals — like retail's per-landcell object draw - /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at + /// in the OUTSIDE (landscape) stage — before the gated depth clear and the + /// exit-portal seals — like retail's per-landcell object draw + /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at /// the top of PView::DrawCells pc:432719)? /// /// True for outdoor-classified dynamics (their fragments lie beyond the /// door plane and would z-fail the seal in the last pass), and for INDOOR /// dynamics whose sphere straddles an exit-portal plane of their flood- - /// visible cell — retail draws an object once per overlapped shadow cell + /// visible cell — retail draws an object once per overlapped shadow cell /// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in - /// both stages and neither half clips at the plane. Pure — also driven + /// both stages and neither half clips at the plane. Pure — also driven /// headlessly by HouseExitWalkReplayTests as the ordering contract. /// public static bool DynamicDrawsInOutsideStage( @@ -1299,7 +1299,7 @@ internal sealed class RetailPViewRenderer uint cellId = parentCellId!.Value; if (!drawableCells.Contains(cellId)) - return false; // not in the flood — the last-pass cone cull owns it + return false; // not in the flood — the last-pass cone cull owns it var cell = cells.Find(cellId); if (cell is null) return false; @@ -1320,7 +1320,7 @@ internal sealed class RetailPViewRenderer return false; } - // Conservative bounding sphere from the entity's cached AABB — the same + // Conservative bounding sphere from the entity's cached AABB — the same // bounds source the dispatcher's frustum cull uses. private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius) { @@ -1343,7 +1343,7 @@ internal sealed class RetailPViewRenderer } -internal interface IRetailPViewCellSource +public interface IRetailPViewCellSource { LoadedCell? Find(uint cellId); } @@ -1354,7 +1354,7 @@ internal interface IRetailPViewCellSource /// pass only; visibility construction and draw ordering remain renderer-owned. /// All frame inputs and results are borrowed for the duration of the call. /// -internal interface IRetailPViewPassExecutor +public interface IRetailPViewPassExecutor { void AbortFrame(); void BeginFrame(); @@ -1565,7 +1565,7 @@ internal sealed class BuildingGroupScratch } } -internal sealed class RetailPViewFrameInput +public sealed class RetailPViewFrameInput { public LoadedCell RootCell { get; private set; } = null!; @@ -1669,7 +1669,7 @@ internal sealed class RetailPViewFrameInput /// frame objects are deliberately reused to keep the render loop allocation /// free; consumers must copy any state they need to retain asynchronously. /// -internal sealed class RetailPViewFrameResult +public sealed class RetailPViewFrameResult { public PortalVisibilityFrame PortalFrame { get; private set; } = null!; public ClipFrameAssembly ClipAssembly { get; private set; } = null!; @@ -1712,17 +1712,17 @@ internal sealed class RetailPViewFrameResult diagnosticPartition); } -internal readonly record struct RetailPViewLandscapeSliceContext( +public readonly record struct RetailPViewLandscapeSliceContext( ClipViewSlice Slice, IReadOnlyList OutdoorEntities) { internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } -/// #131/#132: the late landscape phase's per-slice payload — +/// #131/#132: the late landscape phase's per-slice payload — /// outside-stage dynamics to mesh-draw, plus the full scene-particle owner /// set (statics + dynamics cone survivors) the attached-emitter filter keys on. -internal readonly record struct RetailPViewLandscapeLateSliceContext( +public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, IReadOnlyList Dynamics, IReadOnlySet ParticleOwnerIds) @@ -1730,7 +1730,7 @@ internal readonly record struct RetailPViewLandscapeLateSliceContext( internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } } -internal readonly record struct RetailPViewCellSliceContext( +public readonly record struct RetailPViewCellSliceContext( uint CellId, ClipViewSlice Slice, IReadOnlySet ParticleOwnerIds); diff --git a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs index 484fef9e..2d221c27 100644 --- a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs +++ b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Meshing; using DatReaderWriter.DBObjs; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering; /// degrade has mode 1, is drawn as its authored 3D mesh. Every other first /// degrade mode uses the camera-facing 2D presentation. /// -internal static class RetailParticleGeometryClassifier +public static class RetailParticleGeometryClassifier { public static RetailParticleGeometryKind Classify(uint? firstDegradeMode) => firstDegradeMode is uint mode && mode != 1u diff --git a/src/AcDream.App/Rendering/SamplerCache.cs b/src/AcDream.App/Rendering/SamplerCache.cs index fc874764..91647bab 100644 --- a/src/AcDream.App/Rendering/SamplerCache.cs +++ b/src/AcDream.App/Rendering/SamplerCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering; /// Two persistent GL sampler objects (Repeat + ClampToEdge) created once /// per GL context. Renderers the appropriate /// one to a texture unit instead of mutating per-texture -/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the +/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the /// texture's own wrap parameters, so two renderers can share the same /// texture handle but sample it with different wrap modes safely. /// @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering; /// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132. /// Filter modes match 's upload defaults /// (Linear / Linear, no mipmaps) so binding either sampler doesn't -/// change the visual filtering behavior — only the wrap behavior at +/// change the visual filtering behavior — only the wrap behavior at /// UVs outside [0, 1]. /// /// @@ -28,7 +28,7 @@ namespace AcDream.App.Rendering; /// per-texture wrap state. /// /// -internal sealed class SamplerCache : IDisposable +public sealed class SamplerCache : IDisposable { private readonly GL _gl; private readonly ResourceCleanupGroup _resources; diff --git a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs index d5ae01e6..72b02277 100644 --- a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs +++ b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.InteropServices; using AcDream.App.Rendering.Wb; using AcDream.Core.Lighting; @@ -13,7 +13,7 @@ namespace AcDream.App.Rendering; /// consistent data without per-shader re-upload. /// /// -/// Usage (r12 §13.2 + r13 §12.3): +/// Usage (r12 §13.2 + r13 §12.3): /// /// Instantiate once at startup, after the GL context exists. /// Each frame, after , call with a freshly-built . @@ -21,7 +21,7 @@ namespace AcDream.App.Rendering; /// /// /// -internal sealed unsafe class SceneLightingUboBinding : IDisposable +public sealed unsafe class SceneLightingUboBinding : IDisposable { private readonly GL _gl; private uint _ubo; diff --git a/src/AcDream.App/Rendering/ScreenPolygonClip.cs b/src/AcDream.App/Rendering/ScreenPolygonClip.cs index 16617645..666b5fff 100644 --- a/src/AcDream.App/Rendering/ScreenPolygonClip.cs +++ b/src/AcDream.App/Rendering/ScreenPolygonClip.cs @@ -1,4 +1,4 @@ -// ScreenPolygonClip.cs +// ScreenPolygonClip.cs // // Phase A8.F: 2D convex-polygon intersection (Sutherland-Hodgman). // Ports the BEHAVIOR of retail ACRender::polyClipFinish (the screen-space @@ -11,7 +11,7 @@ using System.Numerics; namespace AcDream.App.Rendering; -internal static class ScreenPolygonClip +public static class ScreenPolygonClip { private const float Eps = 1e-7f; diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs index 1a431ce4..b74960ae 100644 --- a/src/AcDream.App/Rendering/Shader.cs +++ b/src/AcDream.App/Rendering/Shader.cs @@ -1,9 +1,9 @@ -using System.Numerics; +using System.Numerics; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; -internal sealed class Shader : IDisposable +public sealed class Shader : IDisposable { private readonly GL _gl; private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal); @@ -15,10 +15,10 @@ internal sealed class Shader : IDisposable } /// - /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when + /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when /// is true, the text of - /// Shaders/common.glsl — sitting alongside - /// — is spliced into both sources right after their leading + /// Shaders/common.glsl — sitting alongside + /// — is spliced into both sources right after their leading /// #version/#extension block. GL has no #include, so this /// is plain string concatenation at load time rather than a GLSL-level /// mechanism. Every existing two-argument-path caller is unaffected: the @@ -49,14 +49,10 @@ internal sealed class Shader : IDisposable /// Inserts right after the shader's leading /// #version/#extension/blank-line block. GLSL requires /// #version to be the very first statement in the source, so the - /// preamble cannot simply be prepended — it has to land after that block, + /// preamble cannot simply be prepended — it has to land after that block, /// before the first real declaration. - /// - /// Internal (not private) so - /// can reuse the exact same splice for RHI-created pipelines rather than - /// a second copy of this parsing. /// - internal static string InjectPreamble(string source, string preamble) + private static string InjectPreamble(string source, string preamble) { int insertAt = 0; int lineStart = 0; diff --git a/src/AcDream.App/Rendering/Shaders/debug_line.vert b/src/AcDream.App/Rendering/Shaders/debug_line.vert index 6c7eb398..f6340133 100644 --- a/src/AcDream.App/Rendering/Shaders/debug_line.vert +++ b/src/AcDream.App/Rendering/Shaders/debug_line.vert @@ -2,16 +2,12 @@ layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aColor; -// Campaign V slice V4a: the shared GpuPushConstants block carries ONE -// combined view-projection matrix (uViewProjection), the same convention -// every other ported shader uses (mesh_modern.vert, terrain_modern.vert, -// particle.vert) rather than the separate uView/uProjection this shader used -// pre-migration. -uniform mat4 uViewProjection; +uniform mat4 uView; +uniform mat4 uProjection; out vec3 vColor; void main() { vColor = aColor; - gl_Position = uViewProjection * vec4(aPos, 1.0); + gl_Position = uProjection * uView * vec4(aPos, 1.0); } diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.frag b/src/AcDream.App/Rendering/Shaders/ui_text.frag index 09a94ec1..75c9cd3d 100644 --- a/src/AcDream.App/Rendering/Shaders/ui_text.frag +++ b/src/AcDream.App/Rendering/Shaders/ui_text.frag @@ -1,36 +1,19 @@ #version 430 core -#extension GL_ARB_bindless_texture : require in vec2 vUv; in vec4 vColor; out vec4 FragColor; -// Campaign V slice V4a: the bound sprite/glyph texture arrives as a -// texture-table slot index (GpuTextureSlot.Index) via the shared -// push-constant block, rather than a raw sampler2D bound to a fixed texture -// unit. common.glsl's ACDREAM_TEXTURE_HANDLE macro (spliced in below this -// shader's leading #version/#extension block) resolves the slot to the GL -// bindless handle; the Vulkan backend will index its set-2 sampled-texture -// descriptor array with the same integer. -uniform uint uTextureIndexA; - -// uUseTexture reuses the shared block's uRenderPass scalar: the block has no -// other spare per-draw int, and "shaders declare only the fields they read, -// interpreted as they need" is the documented design -// (docs/plans/2026-07-27-vulkan-campaign.md §3.4) — mesh_modern's -// "0=opaque,1=translucent" meaning for this field does not apply here. -uniform int uRenderPass; -#define uUseTexture uRenderPass +uniform sampler2D uTex; +uniform int uUseTexture; void main() { if (uUseTexture == 1) { // Font atlas is a single-channel R8 texture; red = coverage alpha. - sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)); - float coverage = texture(tex, vUv).r; + float coverage = texture(uTex, vUv).r; FragColor = vec4(vColor.rgb, vColor.a * coverage); } else if (uUseTexture == 2) { // RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha. - sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)); - FragColor = texture(tex, vUv) * vColor; + FragColor = texture(uTex, vUv) * vColor; } else { FragColor = vColor; } diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.vert b/src/AcDream.App/Rendering/Shaders/ui_text.vert index 037b2809..0cc6c932 100644 --- a/src/AcDream.App/Rendering/Shaders/ui_text.vert +++ b/src/AcDream.App/Rendering/Shaders/ui_text.vert @@ -3,19 +3,12 @@ layout(location = 0) in vec2 aPos; // screen pixels, origin top-left layout(location = 1) in vec2 aUv; layout(location = 2) in vec4 aColor; -// Campaign V slice V4a: this vertex shader has no projection matrix to carry -// (V3 audit, docs/plans/2026-07-27-vulkan-campaign.md §4.10: ui_text converts -// pixel coordinates straight to NDC with a constant z=0), so the screen size -// travels through the shared push-constant block's two spare scalars instead -// of a dedicated uScreenSize uniform. -uniform float uParamA; // screen width in pixels -uniform float uParamB; // screen height in pixels +uniform vec2 uScreenSize; out vec2 vUv; out vec4 vColor; void main() { - vec2 uScreenSize = vec2(uParamA, uParamB); // Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up). vec2 ndc = vec2( aPos.x / uScreenSize.x * 2.0 - 1.0, diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 5173ad04..26520bb8 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Sky; /// /// Port of references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs. /// Draws the retail sky as a stack of independent celestial meshes (the -/// "it's not a dome" insight from r12 §2) rather than a cube/sphere +/// "it's not a dome" insight from r12 §2) rather than a cube/sphere /// with a gradient texture. Each is /// visible in a window of day-fraction space, sweeps from /// BeginAngle to EndAngle across the sky, and samples its @@ -25,11 +25,11 @@ namespace AcDream.App.Rendering.Sky; /// /// GL state delta per frame: /// -/// Depth mask OFF, depth test OFF, cull OFF — the sky +/// Depth mask OFF, depth test OFF, cull OFF — the sky /// should never occlude scene geometry. -/// Separate projection matrix with a 0.1–1e6 near/far +/// Separate projection matrix with a 0.1–1e6 near/far /// so mesh vertices at large distance don't clip. -/// View matrix with translation zeroed — sky is +/// View matrix with translation zeroed — sky is /// always camera-centred; moving doesn't get you closer to the /// sun. /// @@ -38,12 +38,12 @@ namespace AcDream.App.Rendering.Sky; /// /// Meshes are built lazily per GfxObj id on first reference. The /// per-object arc transform matches WorldBuilder's composition: -/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs +/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs /// come from AC's Z-up right-handed convention where heading is /// measured clockwise from north. /// /// -internal sealed unsafe class SkyRenderer : IDisposable +public sealed unsafe class SkyRenderer : IDisposable { private readonly GL _gl; private readonly IDatReaderWriter _dats; @@ -54,11 +54,11 @@ internal sealed unsafe class SkyRenderer : IDisposable // Lazily-built GPU resources per sky-GfxObj. private readonly Dictionary> _gpuByGfxObj = new(); - // When did we start running — used to accumulate TexVelocityX/Y over + // When did we start running — used to accumulate TexVelocityX/Y over // real time (independent of the day-fraction clock). private readonly DateTime _startedAt = DateTime.UtcNow; - // Configurable render distance — retail uses ~1e6; anything larger + // Configurable render distance — retail uses ~1e6; anything larger // than the scene far plane works. public float Near { get; set; } = 0.1f; public float Far { get; set; } = 1_000_000f; @@ -73,7 +73,7 @@ internal sealed unsafe class SkyRenderer : IDisposable } /// - /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — + /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — /// every SkyObject with Properties & 0x04 == 0). /// Called BEFORE the scene; terrain / meshes / debug lines / overlay /// land on top via depth-test. @@ -82,7 +82,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Mirrors the first half of retail's LScape::draw at /// 0x00506330: that function calls GameSky::Draw(0) /// (sky pass) before the landblock loop, then GameSky::Draw(1) - /// (weather pass) after. acdream splits the same way — see + /// (weather pass) after. acdream splits the same way — see /// for the post-scene companion. /// /// @@ -90,17 +90,17 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Each submesh renders with retail's per-vertex lighting formula: /// tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1) /// where emissive is the submesh's Surface.Luminosity - /// float (1.0 for dome + sun + moon → texture passthrough via - /// saturation; 0.0 for clouds → get the full time-of-day tint). + /// float (1.0 for dome + sun + moon → texture passthrough via + /// saturation; 0.0 for clouds → get the full time-of-day tint). /// supplies the AmbientColor and SunColor /// already pre-multiplied by AmbBright / DirBright (loader-side). /// /// - /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for + /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for /// the full decompile citation. The empirical Dereth dump ( /// ACDREAM_DUMP_SKY=1, logged 2026-04-23) confirmed the /// SurfaceType.Luminous flag bit is NOT set on any Dereth sky - /// mesh — the differentiator is the Surface.Luminosity FLOAT + /// mesh — the differentiator is the Surface.Luminosity FLOAT /// field. /// /// @@ -118,13 +118,13 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Draw the POST-SCENE sky objects (the foreground rain mesh /// 0x01004C44 on Rainy DayGroups, plus any other SkyObject with /// Properties & 0x01 != 0). Called AFTER the scene so these - /// meshes paint on top of terrain and entities — retail-faithful order + /// meshes paint on top of terrain and entities — retail-faithful order /// from LScape::draw at 0x00506330, where /// GameSky::Draw(1) fires after the DrawBlock loop and /// renders the after_sky_cell contents. With depth-test /// disabled and additive blend (the rain Surface flag includes /// Additive), the 815m-tall rain cylinder's bright streak texels add - /// over the scene — making rain appear in the air between camera and + /// over the scene — making rain appear in the air between camera and /// character instead of only at the horizon. /// /// Method name kept as RenderWeather for API stability; the @@ -149,7 +149,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Sets up the same GL state for both (depth-test off, additive + /// alpha-blend per submesh, camera-anchored translation) and iterates /// only the SkyObjects matching the requested partition by - /// — bit 0x01 per the + /// — bit 0x01 per the /// retail decomp at GameSky::MakeObject (0x00506ee0). /// private void RenderPass( @@ -171,7 +171,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // that FOV here, including the near-180-degree teleport transition. var skyProj = SkyProjection.WithDepthRange(camera.Projection, Near, Far); - // View with translation zeroed — keeps the sky at camera origin + // View with translation zeroed — keeps the sky at camera origin // regardless of camera position in the world. var skyView = camera.View; skyView.M41 = 0f; @@ -183,7 +183,7 @@ internal sealed unsafe class SkyRenderer : IDisposable _shader.SetMatrix4("uSkyProjection", skyProj); // Retail per-vertex lighting inputs (AdjustPlanes formula). - // AmbColor/SunColor are already × AmbBright/DirBright from + // AmbColor/SunColor are already × AmbBright/DirBright from // SkyDescLoader. SunDir is the unit vector FROM surface TO sun // derived from the keyframe's DirHeading/DirPitch. _shader.SetVec3("uAmbientColor", keyframe.AmbientColor); @@ -204,14 +204,14 @@ internal sealed unsafe class SkyRenderer : IDisposable bool wasCullFace = _gl.IsEnabled(EnableCap.CullFace); _gl.Disable(EnableCap.CullFace); _gl.Enable(EnableCap.Blend); - // Default blend — overridden per-submesh inside the inner loop. + // Default blend — overridden per-submesh inside the inner loop. // Additive surfaces (sun/moon/stars via SurfaceType.Additive = // 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome // with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA. _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // Look up the keyframe's override list so we can apply - // SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation + // SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation // override + transparency fade + luminosity cap. var replaces = PickReplaces(group, dayFraction); @@ -220,19 +220,19 @@ internal sealed unsafe class SkyRenderer : IDisposable for (int i = 0; i < group.SkyObjects.Count; i++) { var obj = group.SkyObjects[i]; - // Partition by post-scene flag (Properties bit 0x01) — the + // Partition by post-scene flag (Properties bit 0x01) — the // caller chose either the pre-scene sky pass (bit clear) or // the post-scene pass (bit set). Mirrors retail // GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp // line 269036 which routes (Properties & 1) into // before_sky_cell vs after_sky_cell, and GameSky::Draw at // 0x00506ff0 which renders those cells in the two passes. - // NOTE: bit 0x04 (IsWeather) is independent — it gates whether + // NOTE: bit 0x04 (IsWeather) is independent — it gates whether // the object is instantiated when weather_enabled is false. // Earlier acdream incorrectly used IsWeather for this // partition, putting the outer rain cylinder 0x01004C42 // (Props=0x04, NO bit 0x01) into the post-scene pass with the - // foreground rain — double-thick rain not matching retail. + // foreground rain — double-thick rain not matching retail. if (obj.IsPostScene != postScenePass) continue; if (!obj.IsVisible(dayFraction)) continue; // Retail GameSky::Draw (0x00506ff0) skips Properties bit 0x02 @@ -254,7 +254,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // fallback at the inner loop never fired (1f is always > 0). // RainMeshProbe (committed b8e0857) confirmed empirically that // NO Dereth sky surface carries the SurfaceType.Luminous flag - // bit (0x40) — the differentiator is purely the float field. + // bit (0x40) — the differentiator is purely the float field. float replaceLuminosity = float.NaN; float replaceDiffuse = float.NaN; if (replaces.TryGetValue((uint)i, out var rep)) @@ -289,7 +289,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f // // Gate: bit 0x04 (weather) set AND bit 0x08 unset. NOT every - // post-scene SkyObject — bit 0x01 (post-scene) is independent + // post-scene SkyObject — bit 0x01 (post-scene) is independent // of bit 0x04 (weather). Today's Dereth ships every post-scene // entry as also weather-flagged so the previous unconditional // offset was a no-op divergence, but a future DayGroup with a @@ -302,15 +302,15 @@ internal sealed unsafe class SkyRenderer : IDisposable // cylinder bottom sits at z=0.11 ABOVE the camera (skyView // translation is zeroed so model-origin == camera); looking // horizontally shows nothing. With -120m the cylinder spans z - // = (camera-119.89)..(camera+694.90) — camera is inside, - // looking in any direction shows surrounding walls — the + // = (camera-119.89)..(camera+694.90) — camera is inside, + // looking in any direction shows surrounding walls — the // volumetric foreground-rain look retail has. if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u) model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f); _shader.SetMatrix4("uModel", model); - // UV scroll accumulates real-time × velocity. Wrap to [0, 1] + // UV scroll accumulates real-time × velocity. Wrap to [0, 1] // so long-running sessions don't accumulate float precision // loss in the fragment UV. float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f; @@ -328,7 +328,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // sky dome is Base1Image (Opaque, mapped to // SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1). // See FUN_00508010 (chunk_00500000.c:7535) for the retail - // pattern — retail routes sky meshes through the normal + // pattern — retail routes sky meshes through the normal // mesh pipeline where Surface flags dictate state. if (sub.IsAdditive) _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); @@ -338,24 +338,24 @@ internal sealed unsafe class SkyRenderer : IDisposable // Emissive source picks the surface's authored Luminosity by // default; the per-keyframe replace data can OVERRIDE // (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches - // retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive + // retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive // (via material cache +0x3c), with the keyframe replace // promoting bright-keyframe clouds when the keyframe asks. // // Empirical Dereth sky surfaces (RainMeshProbe, b8e0857): - // dome/sun/moon → Lum=1.0 → vTint saturates → texture + // dome/sun/moon → Lum=1.0 → vTint saturates → texture // passthrough (correct retail look); - // stars/clouds → Lum=0.0 → vTint = ambient + diffuse → + // stars/clouds → Lum=0.0 → vTint = ambient + diffuse → // picks up the time-of-day tint; - // rain → Lum=0.1484 → faint emissive baseline, + // rain → Lum=0.1484 → faint emissive baseline, // ambient+diffuse adds atmospheric tint. // // Pre-fix: the replace-override variable defaulted to 1f and // the fallback `(luminosity > 0) ? luminosity : sub.SurfLuminosity` - // never fired — every sky mesh got effEmissive=1.0, + // never fired — every sky mesh got effEmissive=1.0, // saturating vTint. That made stars/clouds look full-bright // instead of time-of-day-tinted, and made rain streaks - // 6.7× too bright (one of two factors compounding the + // 6.7× too bright (one of two factors compounding the // foreground-rim visibility bug). float effEmissive = float.IsNaN(replaceLuminosity) ? sub.SurfLuminosity @@ -374,7 +374,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // Retail D3DPolyRender::SetSurface at 0x59c882 calls // SetFFFogAlphaDisabled(1) when the Additive flag (0x10000) - // is set on the Surface — so the sun, moon, stars, and any + // is set on the Surface — so the sun, moon, stars, and any // additive cloud sheet are drawn WITHOUT fog. Skipping fog // on additive surfaces keeps the sun bright at horizon // dusk/dawn (where fog would otherwise dim it to fog color). @@ -477,13 +477,13 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Lazy mesh build for a sky object. Handles two cases: /// /// - /// 0x010xxxxx — direct . Reuses + /// 0x010xxxxx — direct . Reuses /// so the pos/neg polygon /// splitting logic stays consistent with the main static-mesh /// pipeline. Most sky meshes are single-surface. /// /// - /// 0x020xxxxx — . The agent at + /// 0x020xxxxx. The agent at /// 2026-04-27 found these Setup-backed sky objects (e.g. /// 0x02000588, 0x02000589, 0x02000714, /// 0x02000BA6) were silently dropped: every cache miss @@ -496,7 +496,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Setup.Parts at the default placement frame and /// produces submeshes for each /// part. Per-part transforms are baked into vertex positions - /// (sky setups are static — no animation needed for the static + /// (sky setups are static — no animation needed for the static /// mesh half of the visual). /// /// @@ -504,7 +504,7 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Even with this fix the visible aurora-style sheen most retail /// rainy/cloudy setups produce comes from the pes_id field /// on each (a Particle - /// Effect Schedule) — that's a separate Phase-level feature. + /// Effect Schedule) — that's a separate Phase-level feature. /// Rendering the Setup's static parts here is the geometry half; /// the dynamic particle half is deferred. /// @@ -550,8 +550,8 @@ internal sealed unsafe class SkyRenderer : IDisposable // Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj // once, so we can determine which submeshes carry Luminous (0x40) // vs plain-lit. This settles the retail "cloud tint = per-vertex - // lighting on non-Luminous meshes" hypothesis — see - // docs/research/2026-04-23-sky-retail-verbatim.md §6. + // lighting on non-Luminous meshes" hypothesis — see + // docs/research/2026-04-23-sky-retail-verbatim.md §6. if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1") DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes); @@ -565,14 +565,14 @@ internal sealed unsafe class SkyRenderer : IDisposable /// Setup-backed sky object loader. Walks at /// the default placement frame, builds submeshes via /// , and bakes the per-part transform - /// into the vertex positions before upload. Static-pose only — sky + /// into the vertex positions before upload. Static-pose only — sky /// setups don't animate in any meaningful way for the visual we care /// about (the dynamic look comes from pes_id particles, not /// the underlying mesh). /// /// Mirrors retail's at - /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup - /// → CSetup::SetSetupID, which loads the setup and instantiates + /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup + /// → CSetup::SetSetupID, which loads the setup and instantiates /// each part as a separate CPhysicsObj child. We collapse the /// children into a flat submesh list because the sky pass renders /// without per-part transforms anyway. @@ -654,13 +654,13 @@ internal sealed unsafe class SkyRenderer : IDisposable continue; } - // SurfaceType is a flag enum — `ToString()` gives the + // SurfaceType is a flag enum — `ToString()` gives the // comma-joined names (e.g. "Base1Image, Additive"). uint rawType = (uint)surface.Type; string names = surface.Type.ToString(); uint origTex = surface.OrigTextureId?.DataId ?? 0u; var trans = TranslucencyKindExtensions.FromSurfaceType(surface.Type); - // Surface's own Luminosity (0..1 fraction per test fixture — + // Surface's own Luminosity (0..1 fraction per test fixture — // different from SkyObjectReplace.Luminosity which lives in the keyframe). Console.WriteLine( $"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " + @@ -703,7 +703,7 @@ internal sealed unsafe class SkyRenderer : IDisposable // // NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40` // as additive, but that flag is present on the sky DOME itself and - // on cloud sheets — turning those additive blew the whole sky to + // on cloud sheets — turning those additive blew the whole sky to // white. `Luminous` means "self-illuminated / unshaded" in retail's // render pipeline, not "additive blend". Only the Additive bit // toggles the blend mode. @@ -753,18 +753,18 @@ internal sealed unsafe class SkyRenderer : IDisposable /// public bool IsAdditive; /// - /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous + /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous /// flag bit). Passed to the sky fragment shader as uEmissive; /// when 1.0 it saturates the lighting math so the mesh renders at /// full texture brightness (dome, sun). When 0.0 the mesh picks up /// the time-of-day ambient+diffuse tint (clouds). See - /// docs/research/2026-04-23-sky-retail-verbatim.md §6. + /// docs/research/2026-04-23-sky-retail-verbatim.md §6. /// public float SurfLuminosity; public float SurfDiffuse; /// /// True when the source mesh's authored UVs exceed [0,1] (e.g. - /// the inner sky/star layer 0x010015EF and the cloud meshes — + /// the inner sky/star layer 0x010015EF and the cloud meshes — /// they tile their texture across the geometry). The renderer /// must use GL_REPEAT for these or only the small region /// where UVs fall in [0,1] samples the actual texture; the rest diff --git a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs index 1af9fabc..01c50711 100644 --- a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs +++ b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.World; namespace AcDream.App.Rendering; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering; /// from portal space to the destination world at the transition projection; /// there is no black-alpha compositor between them. /// -internal sealed class TeleportViewPlaneController +public sealed class TeleportViewPlaneController { public const float TransitionViewPlaneDistance = 0.001f; diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs index 4d8860d8..e4c7ad59 100644 --- a/src/AcDream.App/Rendering/TerrainAtlas.cs +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Textures; +using AcDream.Core.Textures; using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,21 +13,21 @@ namespace AcDream.App.Rendering; /// Holds both texture arrays the terrain renderer samples from: /// /// -/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type +/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type /// (grass, dirt, sand, forest...), sourced from /// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc. /// /// -/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, +/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, /// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the /// same TexMerge. Used by the fragment shader to blend up to three /// terrain overlays and two roads on top of a base cell texture. /// /// -/// The alpha atlas is built but not yet sampled by any shader — that wiring +/// The alpha atlas is built but not yet sampled by any shader — that wiring /// lands in Phase 3c.4 along with the shader rewrite. /// -internal sealed unsafe class TerrainAtlas : IDisposable +public sealed unsafe class TerrainAtlas : IDisposable { private readonly GL _gl; @@ -276,7 +276,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable /// /// Load corner, side, and road alpha maps from the TexMerge into a second - /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; + /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; /// expands each alpha byte /// into all four RGBA channels so the shader can sample from any channel. /// @@ -368,7 +368,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable cornerTCodes, sideTCodes, roadRCodes); } - // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). + // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). // Fall back to the max observed so a stray mismatch doesn't crash us. int aMaxW = 1, aMaxH = 1; foreach (var d in decoded) @@ -433,7 +433,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable // Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha // format) or the more generic PFID_A8; terrain blending alpha masks - // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader + // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader // reads .r for the blend weight. Palette is not used. var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true); if (ReferenceEquals(d, DecodedTexture.Magenta)) @@ -515,7 +515,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable /// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at /// runtime (called by /// when - /// the user changes Quality preset mid-session). Idempotent — calling with + /// the user changes Quality preset mid-session). Idempotent — calling with /// the same level as the current setting is safe and produces no visual /// change. The texture must not be resident-bindless when its parameters /// are mutated; we temporarily make it non-resident if needed. diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs index d5e1f6e4..643a2d1a 100644 --- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering; /// /// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot -/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes +/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes /// per slot). Per-frame: build a DrawElementsIndirectCommand array from /// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas /// textures bound via bindless handles set per-frame as sampler uniforms. @@ -16,9 +16,9 @@ namespace AcDream.App.Rendering; /// Total ~6-8 GL calls per frame for terrain regardless of visible /// landblock count. /// -internal sealed unsafe class TerrainModernRenderer : IDisposable +public sealed unsafe class TerrainModernRenderer : IDisposable { - // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses + // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses // `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and // because we bake `slot * VertsPerLandblock` into indices CPU-side and // pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes @@ -83,7 +83,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable private uint _fallbackClipUbo; // Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2) - // became uTextureIndexA/uTextureIndexB (uint table slots) — cached + // became uTextureIndexA/uTextureIndexB (uint table slots) — cached // uniform locations (matrix uniforms are set by name via Shader.SetMatrix4). private int _uTextureIndexALoc; private int _uTextureIndexBLoc; @@ -91,8 +91,8 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable private bool _textureTilingUploaded; // GL-only emulation of the eventual Vulkan global texture descriptor array - // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table — - // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for + // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table — + // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for // why terrain doesn't share WbDrawDispatcher's/EnvCellRenderer's tables. private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; @@ -495,7 +495,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // Bind shader + uniforms + atlas handles. // Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj; // no separate landscape viewpoint to sync. Both uView and uProjection derive - // from the ICamera passed into this method — the same camera used for all other + // from the ICamera passed into this method — the same camera used for all other // renderers in the unified pipeline. Retail's LScape::update_viewpoint // pre-positions terrain to the outdoor landcell, but acdream uses the // unified camera matrix everywhere, so no separate viewpoint divergence can occur. @@ -508,7 +508,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // Campaign V slice V2b: pass each handle's binding=9 table slot // instead of the raw uvec2 handle. GLSL reconstructs // sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use - // site — see terrain_modern.frag. + // site — see terrain_modern.frag. uint terrainSlot = _textureTable.GetOrAdd(terrainHandle); uint alphaSlot = _textureTable.GetOrAdd(alphaHandle); FlushAndBindTextureTable(); @@ -519,7 +519,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // when wired, else the no-clip fallback (count 0 = ungated terrain). BindClipUboBinding2(); - // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw + // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw // (0x006b7040) draws each land triangle ONLY when the camera is on the // POSITIVE (upper) side of its plane (Plane::which_side2 vs // Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling @@ -527,13 +527,13 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable // LandblockMesh emits every triangle CCW in world XY seen from above // (LandblockMeshTests winding pin), which the unified camera chain // (CreateLookAt up=+Z + Numerics perspective) maps to CCW window - // winding from above / CW from below (TerrainCullOrientationTests) — + // winding from above / CW from below (TerrainCullOrientationTests) — // so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the // underside. WB drew the whole world with culling DISABLED - // frame-globally (WB GameScene.cs:841 — an editor camera goes + // frame-globally (WB GameScene.cs:841 — an editor camera goes // underground); inheriting that drew terrain DOUBLE-SIDED, and a // below-grade eye (cellar ascent) saw the UNDERSIDE of the grade - // sheet through the exit-door aperture — the #108 grass window. + // sheet through the exit-door aperture — the #108 grass window. // Self-contained state per feedback_render_self_contained_gl_state; // the frame-global CW + cull-off baseline is restored after the draw. _gl.Enable(EnableCap.CullFace); diff --git a/src/AcDream.App/Rendering/TextRenderGlStateScope.cs b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs new file mode 100644 index 00000000..b65123b7 --- /dev/null +++ b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs @@ -0,0 +1,156 @@ +using Silk.NET.OpenGL; + +namespace AcDream.App.Rendering; + +internal interface ITextRenderGlStateApi +{ + bool IsEnabled(EnableCap capability); + + int GetInteger(GetPName parameter); + + bool GetBoolean(GetPName parameter); + + void SetCapability(EnableCap capability, bool enabled); + + void DepthMask(bool enabled); + + void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha); + + void UseProgram(uint program); + + void BindVertexArray(uint vertexArray); + + void BindBuffer(BufferTargetARB target, uint buffer); + + void ActiveTexture(TextureUnit unit); + + void BindTexture(TextureTarget target, uint texture); +} + +internal sealed class SilkTextRenderGlStateApi : ITextRenderGlStateApi +{ + private readonly GL _gl; + + public SilkTextRenderGlStateApi(GL gl) => + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + + public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability); + + public int GetInteger(GetPName parameter) => _gl.GetInteger(parameter); + + public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter); + + public void SetCapability(EnableCap capability, bool enabled) + { + if (enabled) + _gl.Enable(capability); + else + _gl.Disable(capability); + } + + public void DepthMask(bool enabled) => _gl.DepthMask(enabled); + + public void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha) => + _gl.BlendFuncSeparate( + sourceRgb, + destinationRgb, + sourceAlpha, + destinationAlpha); + + public void UseProgram(uint program) => _gl.UseProgram(program); + + public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray); + + public void BindBuffer(BufferTargetARB target, uint buffer) => + _gl.BindBuffer(target, buffer); + + public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit); + + public void BindTexture(TextureTarget target, uint texture) => + _gl.BindTexture(target, texture); +} + +/// +/// Exact, focused state transaction for . It +/// captures every GL value that Flush or DrawLayer mutates, while avoiding the +/// dozens of unrelated synchronous reads made by the broad diagnostic scope. +/// +internal readonly struct TextRenderGlStateScope : IDisposable +{ + private readonly ITextRenderGlStateApi _gl; + private readonly bool _depthTest; + private readonly bool _blend; + private readonly bool _cullFace; + private readonly bool _alphaToCoverage; + private readonly bool _multisample; + private readonly bool _depthWrite; + private readonly int _blendSourceRgb; + private readonly int _blendDestinationRgb; + private readonly int _blendSourceAlpha; + private readonly int _blendDestinationAlpha; + private readonly int _program; + private readonly int _vertexArray; + private readonly int _arrayBuffer; + private readonly int _activeTexture; + private readonly int _texture0Binding2D; + + public TextRenderGlStateScope(ITextRenderGlStateApi gl) + { + _gl = gl ?? throw new ArgumentNullException(nameof(gl)); + _depthTest = gl.IsEnabled(EnableCap.DepthTest); + _blend = gl.IsEnabled(EnableCap.Blend); + _cullFace = gl.IsEnabled(EnableCap.CullFace); + _alphaToCoverage = gl.IsEnabled(EnableCap.SampleAlphaToCoverage); + _multisample = gl.IsEnabled(EnableCap.Multisample); + _depthWrite = gl.GetBoolean(GetPName.DepthWritemask); + _blendSourceRgb = gl.GetInteger(GetPName.BlendSrcRgb); + _blendDestinationRgb = gl.GetInteger(GetPName.BlendDstRgb); + _blendSourceAlpha = gl.GetInteger(GetPName.BlendSrcAlpha); + _blendDestinationAlpha = gl.GetInteger(GetPName.BlendDstAlpha); + _program = gl.GetInteger(GetPName.CurrentProgram); + _vertexArray = gl.GetInteger(GetPName.VertexArrayBinding); + _arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding); + _activeTexture = gl.GetInteger(GetPName.ActiveTexture); + + try + { + gl.ActiveTexture(TextureUnit.Texture0); + _texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D); + } + finally + { + gl.ActiveTexture((TextureUnit)_activeTexture); + } + } + + public void Dispose() + { + _gl.UseProgram((uint)_program); + _gl.BindVertexArray((uint)_vertexArray); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer); + + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D); + _gl.ActiveTexture((TextureUnit)_activeTexture); + + _gl.DepthMask(_depthWrite); + _gl.BlendFuncSeparate( + (BlendingFactor)_blendSourceRgb, + (BlendingFactor)_blendDestinationRgb, + (BlendingFactor)_blendSourceAlpha, + (BlendingFactor)_blendDestinationAlpha); + _gl.SetCapability(EnableCap.DepthTest, _depthTest); + _gl.SetCapability(EnableCap.Blend, _blend); + _gl.SetCapability(EnableCap.CullFace, _cullFace); + _gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage); + _gl.SetCapability(EnableCap.Multisample, _multisample); + } +} diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index 7358a76d..18f8862b 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -1,7 +1,11 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using AcDream.App.Rendering.Wb; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -11,39 +15,36 @@ namespace AcDream.App.Rendering; /// at the start of a HUD pass, queue geometry via /// / , then . /// -/// Campaign V slice V4a: ported onto . One pipeline -/// (blend, depth-disable, and the MSAA/alpha-to-coverage isolation the prior -/// TextRenderGlStateScope restored by hand are now baked into the -/// pipeline description) and one pass per , with each -/// bucket/segment getting its own per-frame ring allocation instead of a -/// shared, growable VBO. Rects are drawn first so text sits on top of -/// background panels — bucket ORDER is unchanged, only how each bucket -/// reaches the GPU. +/// Uses two internal vertex buffers (text and rect) flushed in two draw calls +/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text +/// sits on top of background panels. /// -internal sealed class TextRenderer : IDisposable +public sealed unsafe class TextRenderer : IDisposable { private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4) - private const int VertexStrideBytes = FloatsPerVertex * sizeof(float); - private static readonly GpuVertexLayout VertexLayout = new( - StrideBytes: VertexStrideBytes, - [ - new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0), - new GpuVertexAttribute(1, GpuVertexFormat.Float2, 8), - new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16), - ]); + private readonly GL _gl; + private readonly ITextRenderGlStateApi _glState; + private readonly Shader _shader; + private readonly ResourceCleanupGroup _resources; + private uint _vao; + private uint _vbo; + private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket + private int _vboCapacityBytes; - // uUseTexture values the ui_text.frag shader branches on (reusing the - // shared push-constant block's uRenderPass scalar — see the shader's own - // comment for why there is no dedicated field). - private const int UseTextureNone = 0; - private const int UseTextureFont = 1; - private const int UseTextureSprite = 2; + private sealed class FrameBufferSet + { + public uint Vao; + public uint Vbo; + public int CapacityBytes; + public int UsedBytes; + } - private readonly IGpuDevice _device; - private readonly IGpuPipeline _pipeline; + private readonly FrameBufferSet[] _frameBuffers; + private FrameBufferSet? _activeFrameBuffer; - private sealed class SpriteSeg { public GpuTextureSlot TextureSlot; public readonly List Verts = new(256); } + internal long DynamicBufferCapacityBytes => + _frameBuffers.Sum(set => (long)set.CapacityBytes); private readonly List _textBuf = new(8192); private readonly List _rectBuf = new(1024); @@ -52,14 +53,16 @@ internal sealed class TextRenderer : IDisposable // Drawing segments in submission order preserves painter z-order for // sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture // at its FIRST-insertion point, so later bar sprites covered glyphs emitted - // earlier via the shared dat-font atlas — the stamina/mana numbers vanished.) + // earlier via the shared dat-font atlas — the stamina/mana numbers vanished.) + private sealed class SpriteSeg { public uint Texture; public readonly List Verts = new(256); } + private readonly List _spriteSegs = new(); private int _segUsed; private int _textVerts; private int _rectVerts; private Vector2 _screenSize; - // Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text + // Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text // buckets, so open popups/menus composite on top of EVERYTHING, including translucent // rect panel backgrounds (which otherwise always win because rects flush after // sprites). Routed by OverlayMode; the UI root sets it for the popup traversal. @@ -74,38 +77,142 @@ internal sealed class TextRenderer : IDisposable /// of all normal-layer geometry). Set by the UI root around the popup/overlay pass. public bool OverlayMode { get; set; } - /// - /// No longer meaningful post-V4a: per-frame vertex data comes from the - /// device's shared ring rather than a VBO this class owns. Kept (returning - /// 0) so RenderFrameDiagnosticSources's telemetry read still compiles; - /// the dynamic-buffer dimension it reported is now a device-wide, not a - /// per-renderer, concern. - /// - internal long DynamicBufferCapacityBytes => 0; - - public TextRenderer(IGpuDevice device) + public TextRenderer(GL gl, string shaderDir) { - _device = device ?? throw new ArgumentNullException(nameof(device)); - _pipeline = _device.CreatePipeline(new GpuPipelineDescription + _gl = gl; + _glState = new SilkTextRenderGlStateApi(gl); + var resources = new ResourceCleanupGroup(); + Shader? shader = null; + var frameBuffers = new FrameBufferSet[3]; + uint whiteTexture = 0; + try { - Name = "ui-text", - Shaders = new GpuShaderSet("ui_text"), - VertexLayout = VertexLayout, - Topology = GpuPrimitiveTopology.TriangleList, - Blend = GpuBlendMode.StraightAlpha, - // The retained UI is a self-contained 2-D pass: depth is - // irrelevant (feedback_render_self_contained_gl_state) and the - // world pass's alpha-to-coverage/multisample state must not leak - // in — SampleCount=1 drives the GL backend's GL_MULTISAMPLE - // toggle off when this pipeline binds (GlGpuDevice.ApplyRenderState), - // which is what the deleted TextRenderGlStateScope used to restore - // by hand around every Flush. - Depth = GpuDepthState.Disabled, - Cull = GpuCullMode.None, - AlphaToCoverage = false, - ColorWrite = true, - SampleCount = 1, - }); + shader = new Shader(gl, + Path.Combine(shaderDir, "ui_text.vert"), + Path.Combine(shaderDir, "ui_text.frag")); + resources.Add("text shader", shader.Dispose); + + for (int i = 0; i < frameBuffers.Length; i++) + frameBuffers[i] = CreateFrameBufferSet(resources); + + // 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE + // bucket (the shader multiplies texel×color → white×color = color). Lets a panel + // background draw UNDER its text in painter order, which DrawRect's separate + // bucket cannot (it always composites after all sprites). + whiteTexture = GlResourceCommand.CreateTexture( + _gl, + "TextRenderer white texture"); + uint ownedWhiteTexture = whiteTexture; + resources.Add( + "white texture", + () => GlResourceCommand.DeleteTexture( + _gl, + ownedWhiteTexture, + $"delete TextRenderer white texture {ownedWhiteTexture}")); + GlResourceCommand.Execute( + _gl, + "initialize TextRenderer white texture", + () => + { + _gl.BindTexture(TextureTarget.Texture2D, whiteTexture); + Span whitePixel = stackalloc byte[] { 255, 255, 255, 255 }; + fixed (byte* wp = whitePixel) + _gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, wp); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest); + _gl.BindTexture(TextureTarget.Texture2D, 0); + }); + } + catch (Exception constructionFailure) + { + try + { + resources.RetryCleanup(); + } + catch (Exception cleanupFailure) + { + throw new GlResourceConstructionException( + "TextRenderer construction failed and its published GL resources did not cleanly roll back.", + resources, + [constructionFailure, cleanupFailure]); + } + + throw; + } + + _resources = resources; + _shader = shader; + _frameBuffers = frameBuffers; + _whiteTex = whiteTexture; + } + + /// + /// Selects the GPU-fenced frame slot and resets its append cursor. Every + /// UI segment rendered during the frame receives a distinct byte range; + /// later text or sprite batches cannot overwrite an earlier in-flight draw. + /// + public void BeginFrame(int frameSlot) + { + if ((uint)frameSlot >= (uint)_frameBuffers.Length) + throw new ArgumentOutOfRangeException(nameof(frameSlot)); + + FrameBufferSet set = _frameBuffers[frameSlot]; + set.UsedBytes = 0; + _activeFrameBuffer = set; + _vao = set.Vao; + _vbo = set.Vbo; + _vboCapacityBytes = set.CapacityBytes; + } + + private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources) + { + uint vao = TrackedGlResource.CreateVertexArray( + _gl, + "TextRenderer frame VAO creation"); + RetryableGpuResourceRelease vaoRelease = + TrackedGlResource.CreateRetryableVertexArrayDeletion( + _gl, + vao, + "TextRenderer frame VAO disposal"); + resources.Add("frame VAO", vaoRelease.Run); + var set = new FrameBufferSet { Vao = vao }; + + uint vbo = TrackedGlResource.CreateBuffer( + _gl, + "TextRenderer frame VBO creation"); + set.Vbo = vbo; + RetryableGpuResourceRelease? vboRelease = null; + resources.Add( + "frame VBO", + () => + { + vboRelease ??= TrackedGlResource.CreateRetryableBufferDeletion( + _gl, + vbo, + set.CapacityBytes, + "TextRenderer frame VBO disposal"); + vboRelease.Run(); + }); + + GlResourceCommand.Execute( + _gl, + "initialize TextRenderer frame VAO and VBO", + () => + { + _gl.BindVertexArray(set.Vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo); + uint stride = FloatsPerVertex * sizeof(float); + _gl.EnableVertexAttribArray(0); + _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0); + _gl.EnableVertexAttribArray(1); + _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float))); + _gl.EnableVertexAttribArray(2); + _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float))); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + _gl.BindVertexArray(0); + }); + return set; } /// Begin a HUD pass. Call once per frame before any Draw* calls. @@ -134,11 +241,11 @@ internal sealed class TextRenderer : IDisposable /// Draw a solid-colour quad through the SPRITE bucket (and the overlay layer /// when active), so it composites in painter order with sprites + dat-font text. Use - /// this — not — for a panel BACKGROUND that text draws on top of: + /// this — not — for a panel BACKGROUND that text draws on top of: /// DrawRect's bucket always flushes after all sprites, so a rect background would cover /// the text instead. public void DrawFill(float x, float y, float w, float h, Vector4 color) - => DrawSprite(_device.DefaultTextureSlot, x, y, w, h, 0f, 0f, 1f, 1f, color); + => DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color); /// Draw a 1-pixel-thick outline rect. public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f) @@ -206,7 +313,7 @@ internal sealed class TextRenderer : IDisposable } if (!font.TryGetGlyph(c, out var g)) { - // Unknown glyph — skip its advance width if '?' exists. + // Unknown glyph — skip its advance width if '?' exists. if (font.TryGetGlyph('?', out var q)) cursorX += q.Advance; continue; @@ -237,9 +344,9 @@ internal sealed class TextRenderer : IDisposable /// /// Draw a textured sprite quad in screen pixel space with an explicit /// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per - /// texture-table slot, flushed with uUseTexture=2 (RGBA modulate). + /// GL texture handle; flushed with uUseTexture=2 (RGBA modulate). /// - public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h, + public void DrawSprite(uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint) { SpriteSeg seg = OverlayMode @@ -251,18 +358,18 @@ internal sealed class TextRenderer : IDisposable /// Pick the sprite segment for : extend the current /// same-texture run, else reuse a pooled segment, else allocate. Submission order is /// preserved (painter z-order for sprite-on-sprite UI). - private static SpriteSeg NextSpriteSeg(List segs, ref int used, GpuTextureSlot texture) + private static SpriteSeg NextSpriteSeg(List segs, ref int used, uint texture) { - if (used > 0 && segs[used - 1].TextureSlot == texture) + if (used > 0 && segs[used - 1].Texture == texture) return segs[used - 1]; if (used < segs.Count) { var s = segs[used++]; - s.TextureSlot = texture; + s.Texture = texture; s.Verts.Clear(); return s; } - var ns = new SpriteSeg { TextureSlot = texture }; + var ns = new SpriteSeg { Texture = texture }; segs.Add(ns); used++; return ns; @@ -274,10 +381,10 @@ internal sealed class TextRenderer : IDisposable { // Two triangles (6 verts). CCW in pixel space is clockwise in NDC // because the vertex shader flips Y, so OpenGL's default front-face - // is GL_CCW — we rely on cull-face being disabled during HUD pass. - // (x, y) ─ (x+w, y) - // │ │ - // (x, y+h) ─ (x+w, y+h) + // is GL_CCW — we rely on cull-face being disabled during HUD pass. + // (x, y) ─ (x+w, y) + // │ │ + // (x, y+h) ─ (x+w, y+h) // // Triangle 1: (x,y) (x+w,y+h) (x+w,y) // Triangle 2: (x,y) (x,y+h) (x+w,y+h) @@ -295,105 +402,131 @@ internal sealed class TextRenderer : IDisposable V(x + w, y + h, u1, v1); } - /// Upload + draw accumulated rects + text against the current frame. font may - /// be null if only DrawRect was used. - public void Flush(BitmapFont? font, IGpuFrame frame) + /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used. + public void Flush(BitmapFont? font) { bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0; bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0; if (!anyNormal && !anyOverlay) return; - ArgumentNullException.ThrowIfNull(frame); - using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription - { - Name = "ui-text", - // GL's BeginPass deliberately does not touch viewport/scissor for - // a Target:null pass, and Load/Store against the backbuffer - // reproduces exactly what this pass did before the RHI existed — - // the frame spine still owns clears until slice V4h. - Color = new GpuColorAttachment( - Target: null, - Load: GpuLoadOp.Load, - Store: GpuStoreOp.Store, - ClearColor: default), - Depth = null, - SampleCount = 1, - }); - pass.BindPipeline(_pipeline); + // Retained UI is a private render pass: an upload or draw failure must + // not leak its depth/cull/blend/MSAA state into a later recoverable + // frame. The focused scope restores from Flush's generated finally, + // including when either DrawLayer call throws. + using var stateScope = new TextRenderGlStateScope(_glState); - GpuPushConstants baseConstants = GpuPushConstants.Default; - baseConstants.ParamA = _screenSize.X; - baseConstants.ParamB = _screenSize.Y; + _shader.Use(); + _shader.SetVec2("uScreenSize", _screenSize); - // LAYERED compositing for the UI (background → fill → text): - // 1. RGBA dat sprites — window chrome / panel backgrounds (behind) - // 2. Untextured rects — widget fills (e.g. vital bars) on the chrome - // 3. Text glyphs — on top + _gl.BindVertexArray(_vao); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + + // Establish the self-contained UI pass state. + // The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher, + // QualitySettings MSAA). If they bleed into the UI pass, each glyph's soft alpha + // EDGE is converted to dithered MSAA coverage instead of a clean alpha blend — + // the "text not sharp / fuzzy" artifact. The UI composites with straight alpha + // blending and must own this state (feedback_render_self_contained_gl_state). + _gl.Disable(EnableCap.SampleAlphaToCoverage); + _gl.Disable(EnableCap.Multisample); + _gl.Disable(EnableCap.DepthTest); + _gl.Disable(EnableCap.CullFace); + _gl.DepthMask(false); + _gl.Enable(EnableCap.Blend); + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + + // LAYERED compositing for the UI (background → fill → text): + // 1. RGBA dat sprites — window chrome / panel backgrounds (behind) + // 2. Untextured rects — widget fills (e.g. vital bars) on the chrome + // 3. Text glyphs — on top // Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs, // so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text) // composite on top, in that order. The OVERLAY layer repeats all three // AFTER the normal layer, so open popups beat even the rect backgrounds. - DrawLayer(pass, frame, in baseConstants, _spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font); - DrawLayer(pass, frame, in baseConstants, _overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font); + DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font); + DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font); + } - /// Draw one compositing layer: sprites (submission order, one draw per - /// segment) → untextured rects → debug-font text. Shared by the normal and overlay - /// layers; pipeline + pass are already bound by . + /// Draw one compositing layer: sprites (submission order, one call per + /// texture) → untextured rects → debug-font text. Shared by the normal and overlay + /// layers; GL state + shader are set up by . private void DrawLayer( - IGpuPassEncoder pass, - IGpuFrame frame, - in GpuPushConstants baseConstants, List spriteSegs, int segUsed, List rectBuf, int rectVerts, List textBuf, int textVerts, BitmapFont? font) { - // 1. RGBA dat sprites — one draw per distinct texture-table slot. - for (int i = 0; i < segUsed; i++) + // 1. RGBA dat sprites — one draw call per distinct GL texture. + if (segUsed > 0) { - SpriteSeg seg = spriteSegs[i]; - if (seg.Verts.Count == 0) continue; - DrawBucket(pass, frame, in baseConstants, seg.Verts, UseTextureSprite, seg.TextureSlot); + _shader.SetInt("uUseTexture", 2); + _gl.ActiveTexture(TextureUnit.Texture0); + _shader.SetInt("uTex", 0); + for (int i = 0; i < segUsed; i++) + { + var seg = spriteSegs[i]; + if (seg.Verts.Count == 0) continue; + _gl.BindTexture(TextureTarget.Texture2D, seg.Texture); + int firstVertex = UploadBuffer(seg.Verts); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)(seg.Verts.Count / FloatsPerVertex)); + } } - // 2. Untextured rects — widget fills on top of the chrome. + // 2. Untextured rects — widget fills on top of the chrome. if (rectVerts > 0) - DrawBucket(pass, frame, in baseConstants, rectBuf, UseTextureNone, GpuTextureSlot.Unassigned); + { + _shader.SetInt("uUseTexture", 0); + int firstVertex = UploadBuffer(rectBuf); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)rectVerts); + } // 3. Textured debug-font text glyphs on top. if (textVerts > 0 && font is not null) - DrawBucket(pass, frame, in baseConstants, textBuf, UseTextureFont, font.TextureId); + { + _shader.SetInt("uUseTexture", 1); + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, font.TextureId); + _shader.SetInt("uTex", 0); + int firstVertex = UploadBuffer(textBuf); + _gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)textVerts); + } } - private static void DrawBucket( - IGpuPassEncoder pass, - IGpuFrame frame, - in GpuPushConstants baseConstants, - List verts, - int useTexture, - GpuTextureSlot textureSlot) + private int UploadBuffer(List buf) { - int byteCount = verts.Count * sizeof(float); - if (byteCount == 0) return; + int bytes = buf.Count * sizeof(float); + if (bytes == 0) return 0; + FrameBufferSet set = _activeFrameBuffer + ?? throw new InvalidOperationException("BeginFrame must be called before rendering text."); + int byteOffset = set.UsedBytes; + int requiredBytes = checked(byteOffset + bytes); - GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex); - CollectionsMarshal.AsSpan(verts).CopyTo(allocation.AsSpan()); + if (requiredBytes > _vboCapacityBytes) + { + int newCapacity = DynamicBufferCapacity.Grow( + _vboCapacityBytes, + requiredBytes); + TrackedGlResource.AllocateBufferStorage( + _gl, + GLEnum.ArrayBuffer, + _vbo, + _vboCapacityBytes, + newCapacity, + GLEnum.DynamicDraw, + "TextRenderer frame VBO growth"); + _vboCapacityBytes = newCapacity; + } - GpuPushConstants constants = baseConstants; - constants.RenderPass = useTexture; - // Unassigned (the untextured-rect bucket) never reaches the shader as - // a slot index — uUseTexture==0 skips the sample entirely — but a - // defined value is still written so no stale slot lingers in the - // uniform between draws. - constants.TextureIndexA = textureSlot.IsAssigned ? textureSlot.Index : 0u; + fixed (float* p = CollectionsMarshal.AsSpan(buf)) + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p); - pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes); - pass.SetPushConstants(in constants); - pass.Draw((uint)(verts.Count / FloatsPerVertex), instanceCount: 1, firstVertex: 0, firstInstance: 0); + set.UsedBytes = requiredBytes; + set.CapacityBytes = _vboCapacityBytes; + return byteOffset / (FloatsPerVertex * sizeof(float)); } public void Dispose() { - _pipeline.Dispose(); + _resources.RetryCleanup(); } } diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index adab5810..c4bfb1fd 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -1,4 +1,4 @@ -// src/AcDream.App/Rendering/TextureCache.cs +// src/AcDream.App/Rendering/TextureCache.cs using AcDream.Core.Textures; using AcDream.Core.World; using AcDream.Content; @@ -12,12 +12,11 @@ using AcDream.App.Rendering.Residency; namespace AcDream.App.Rendering; -internal sealed unsafe class TextureCache +public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable { private readonly GL _gl; - private readonly IGpuDevice _device; private readonly IDatReaderWriter _dats; private readonly string _diagnosticsDirectory; // Handle and decoded dimensions are one atomic cache entry. Keeping them @@ -29,40 +28,17 @@ internal sealed unsafe class TextureCache _decodedDimensionsByTexture = new(); private uint _magentaHandle; - /// - /// Campaign V slice V4a: one registered plus its - /// device texture-table and decoded pixel - /// size. Direct-RenderSurface caches for UI sprites: 0x06xxxxxx - /// RenderSurface ids decoded directly (Portal/HighRes → - /// DecodeRenderSurface), bypassing the Surface→SurfaceTexture chain that - /// GetOrUpload uses for world materials — that world path stays on raw GL - /// () until its own campaign slice. - /// - private readonly record struct GpuUiTextureEntry( - GpuTextureSlot Slot, - IGpuTexture Texture, - int Width, - int Height); + // Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids + // decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the + // Surface→SurfaceTexture chain that GetOrUpload uses for world materials. + private readonly Dictionary _handlesByRenderSurfaceId = new(); + private readonly Dictionary _rsSizeById = new(); - private readonly Dictionary _renderSurfaceGpuTextures = new(); - - // Ad-hoc GPU textures produced by the public UploadRgba8(byte[],int,int,bool) - // wrapper (used by IconComposer for composited item icons). These are NOT - // stored in the keyed cache above, so Dispose must sweep this list to avoid - // leaking GPU texture-table slots until process exit. - private readonly List _adhocGpuTextures = new(); - - // Every UI-path upload uses REPEAT addressing (existing behaviour: panel - // fills and tiled chrome sample UVs > 1) with the caller-selected filter. - // Nearest+Repeat has no predefined GpuSamplerDescription (UiNearest clamps), - // so it is constructed once here; CreateSampler de-duplicates by value. - private static readonly GpuSamplerDescription UiSpriteNearestRepeat = new( - GpuFilter.Nearest, - GpuFilter.Nearest, - GpuMipFilter.None, - GpuAddressMode.Repeat, - GpuAddressMode.Repeat, - MaxAnisotropy: 1f); + // Ad-hoc handles produced by the public UploadRgba8(byte[],int,int,bool) wrapper + // (used by IconComposer for composited item icons). These are NOT stored in any + // of the keyed caches above, so Dispose must sweep this list to avoid leaking + // GL texture objects until process exit. + private readonly List _adhocHandles = new(); private readonly Wb.BindlessSupport? _bindless; private readonly CompositeTextureArrayCache? _compositeTextures; @@ -106,14 +82,13 @@ internal sealed unsafe class TextureCache // Frame counter for the one-shot ACDREAM_DUMP_SURFACES=1 trigger. // Increments per Tick call; fires the dump once at frame index 600 - // and never again for the session. See spec §5. + // and never again for the session. See spec §5. private int _dumpFrameCounter; private bool _surfaceHistogramAlreadyDumped; - public TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) + public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) : this( gl, - device, dats, bindless, ImmediateGpuResourceRetirementQueue.Instance, @@ -126,7 +101,6 @@ internal sealed unsafe class TextureCache internal TextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless, IGpuResourceRetirementQueue retirementQueue, @@ -135,7 +109,6 @@ internal sealed unsafe class TextureCache { budgets ??= ResidencyBudgetOptions.Default; _gl = gl; - _device = device ?? throw new ArgumentNullException(nameof(device)); _dats = dats; _bindless = bindless; ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory); @@ -246,22 +219,23 @@ internal sealed unsafe class TextureCache /// /// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded - /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the - /// Surface→SurfaceTexture chain that uses + /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the + /// Surface→SurfaceTexture chain that uses /// for world-geometry materials. This is the correct path for retail UI /// chrome + font glyph sheets, which reference RenderSurface directly. - /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object - /// health-bar track 0x0600193E — are decoded against the RenderSurface's own + /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object + /// health-bar track 0x0600193E — are decoded against the RenderSurface's own /// DefaultPaletteId (same starting palette - /// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns + /// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns /// a 1x1 magenta handle on miss. /// - public GpuTextureSlot GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false) + public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false) { - if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing)) + if (_handlesByRenderSurfaceId.TryGetValue(renderSurfaceId, out var existing) + && _rsSizeById.TryGetValue(renderSurfaceId, out var sz)) { - width = existing.Width; height = existing.Height; - return existing.Slot; + width = sz.w; height = sz.h; + return existing; } DecodedTexture decoded; @@ -282,43 +256,16 @@ internal sealed unsafe class TextureCache decoded = DecodedTexture.Magenta; } - GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}"); - _renderSurfaceGpuTextures[renderSurfaceId] = entry; + uint h = UploadRgba8(decoded, nearest); + _handlesByRenderSurfaceId[renderSurfaceId] = h; + _rsSizeById[renderSurfaceId] = (decoded.Width, decoded.Height); width = decoded.Width; height = decoded.Height; - return entry.Slot; - } - - /// - /// Campaign V slice V4a: creates an for one - /// decoded UI sprite/atlas and registers it into the device's global - /// texture table. Every UI-path texture uses REPEAT addressing (existing - /// behaviour — panel fills and tiled chrome sample UVs greater than 1) and - /// a single mip level (UI sprites never mip). - /// selects point sampling for pixel-crisp glyphs/icons versus bilinear for - /// everything else, matching the GL path's prior per-call choice. - /// - private GpuUiTextureEntry UploadUiTexture(DecodedTexture decoded, bool nearest, string debugName) - { - int width = Math.Max(1, decoded.Width); - int height = Math.Max(1, decoded.Height); - IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription( - debugName, - GpuTextureKind.Texture2D, - GpuTextureFormat.Rgba8Unorm, - Width: width, - Height: height, - LayerCount: 1, - MipLevelCount: 1)); - texture.Upload(0, 0, decoded.Rgba8); - IGpuSampler sampler = _device.CreateSampler( - nearest ? UiSpriteNearestRepeat : GpuSamplerDescription.WorldRepeat); - GpuTextureSlot slot = _device.RegisterTexture(texture, sampler); - return new GpuUiTextureEntry(slot, texture, decoded.Width, decoded.Height); + return h; } /// /// Alpha-channel histogram for one decoded texture. Used to diagnose - /// "why are clouds not transparent" — if cloud textures come out with + /// "why are clouds not transparent" — if cloud textures come out with /// alpha = 1.0 everywhere we know the decode path strips the alpha /// channel somewhere. Printed once per unique surfaceId under /// ACDREAM_DUMP_SKY=1. Adds ~2ms per texture upload, negligible. @@ -583,7 +530,7 @@ internal sealed unsafe class TextureCache { if (_bindless is null) throw new InvalidOperationException( - "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " + + "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " + "WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport)."); } @@ -638,7 +585,7 @@ internal sealed unsafe class TextureCache /// internal static ulong HashPaletteOverride(PaletteOverride p) { - // Not cryptographic — just needs to distinguish override setups + // Not cryptographic — just needs to distinguish override setups // for caching. Start with base palette id, fold in each entry. ulong h = 0xCBF29CE484222325UL; // FNV-1a offset basis const ulong prime = 0x100000001B3UL; @@ -659,17 +606,17 @@ internal sealed unsafe class TextureCache /// Phase N.6 slice 1: one-shot surface-format histogram dump for the /// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires /// once after BOTH gates pass: - /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks + /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks /// have elapsed (catches the "we're already past startup boilerplate" /// bound; ~10s at 60fps, ~3s at 200fps). - /// 2. _uploadMetadata.Count >= 100 — the cache contains at + /// 2. _uploadMetadata.Count >= 100 — the cache contains at /// least 100 uploaded textures, indicating streaming has actually /// pulled in world content (not just sky/UI/font). The original /// frame-only gate fired during the login/handshake phase where /// OnRender ticks at GUI rates but no world has streamed in. /// Output goes to the host-provided portable diagnostics directory. /// Zero cost - /// when off. See spec §5 in + /// when off. See spec §5 in /// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md. /// public void TickSurfaceHistogramDumpIfEnabled() @@ -694,7 +641,7 @@ internal sealed unsafe class TextureCache { // Diagnostic-only path. If the dump file can't be written // (disk full, permission denied, antivirus lock, path too - // long) we must NOT crash OnRender — that would invalidate + // long) we must NOT crash OnRender — that would invalidate // the very measurement pass this diagnostic is meant to // support. Log to stderr and let the caller mark the dump // as "already done" so it doesn't retry every frame. @@ -710,7 +657,7 @@ internal sealed unsafe class TextureCache "n6-surfaces.txt"); var sb = new System.Text.StringBuilder(); - sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"); + sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}"); sb.AppendLine("# Per-entry: surfaceId(hex), width, height, format, byteCount"); sb.AppendLine(); @@ -763,7 +710,7 @@ internal sealed unsafe class TextureCache foreach (var kv in bucketsByFormat.OrderByDescending(kv => kv.Value)) sb.AppendLine($"# {kv.Key}: {kv.Value}"); - sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:"); + sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:"); foreach (var kv in bucketsByTriple.OrderByDescending(kv => kv.Value).Take(10)) sb.AppendLine($"# {kv.Key.W}x{kv.Key.H} {kv.Key.F}: {kv.Value}"); @@ -782,8 +729,8 @@ internal sealed unsafe class TextureCache } // Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue - // instead of a texture chain. Overrides are irrelevant here — there's - // no texture chain to swap — so the override is ignored for solid-color + // instead of a texture chain. Overrides are irrelevant here — there's + // no texture chain to swap — so the override is ignored for solid-color // surfaces. Translucency is honored so Base1Solid|Translucent surfaces // with Translucency=1.0 become alpha=0, which the mesh shader's discard // cutout makes invisible. @@ -812,7 +759,7 @@ internal sealed unsafe class TextureCache // Start with the texture's default palette, then apply overlays. // ACViewer's Render/TextureCache.IndexToColor does the same and never - // consults ObjDesc.BasePaletteId for palette-indexed textures — the + // consults ObjDesc.BasePaletteId for palette-indexed textures — the // RenderSurface's own default palette is the starting point. Palette? basePalette = rs.DefaultPaletteId != 0 ? _dats.Get(rs.DefaultPaletteId) @@ -870,13 +817,12 @@ internal sealed unsafe class TextureCache /// to upload CPU-composited icon layers. /// The returned handle is tracked in and deleted by /// . Callers must NOT also store the handle in any of the - /// keyed caches — that would cause a double-delete on Dispose. - public GpuTextureSlot UploadRgba8(byte[] rgba, int width, int height, bool nearest = false) + /// keyed caches — that would cause a double-delete on Dispose. + public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false) { - GpuUiTextureEntry entry = UploadUiTexture( - new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-icon"); - _adhocGpuTextures.Add(entry); - return entry.Slot; + uint h = UploadRgba8(new DecodedTexture(rgba, width, height), nearest); + _adhocHandles.Add(h); + return h; } private uint UploadRgba8(DecodedTexture decoded, bool nearest = false) @@ -900,7 +846,7 @@ internal sealed unsafe class TextureCache PixelType.UnsignedByte, p); - // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat + // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat // font's small glyphs. Other surfaces use bilinear. int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear; _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter); @@ -1016,28 +962,16 @@ internal sealed unsafe class TextureCache _magentaHandle = 0; } - // RenderSurface (UI sprite) GPU textures — pre-existing gap: this dict was - // populated by GetOrUploadRenderSurface but was not swept here before that fix. - foreach (GpuUiTextureEntry entry in _renderSurfaceGpuTextures.Values) - DisposeUiTexture(entry); - _renderSurfaceGpuTextures.Clear(); + // RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated + // by GetOrUploadRenderSurface but was not swept here before this fix. + foreach (var h in _handlesByRenderSurfaceId.Values) + DeleteUploadedTexture(h); + _handlesByRenderSurfaceId.Clear(); - // Ad-hoc GPU textures from the public UploadRgba8(byte[],int,int,bool) wrapper + // Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper // (IconComposer composited icons). Not stored in any keyed cache. - foreach (GpuUiTextureEntry entry in _adhocGpuTextures) - DisposeUiTexture(entry); - _adhocGpuTextures.Clear(); - } - - /// - /// Releases a UI-path texture's table slot before disposing the backing - /// . Both route through the device's retirement - /// queue, so releasing the slot first is purely bookkeeping order, not a - /// use-after-free concern. - /// - private void DisposeUiTexture(GpuUiTextureEntry entry) - { - _device.ReleaseTextureSlot(entry.Slot); - entry.Texture.Dispose(); + foreach (var h in _adhocHandles) + DeleteUploadedTexture(h); + _adhocHandles.Clear(); } } diff --git a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs index 18f31c44..578a3b65 100644 --- a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs +++ b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Vfx; using DatReaderWriter.Types; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx; /// here preserves that observable order and avoids firing a hand or weapon /// effect against the previous animation frame. /// -internal sealed class AnimationHookFrameQueue +public sealed class AnimationHookFrameQueue { private readonly AnimationHookRouter _router; private readonly IEntityEffectPoseSource _poses; diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs index f11df828..1d42cb5d 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -23,7 +23,7 @@ namespace AcDream.App.Rendering.Vfx; /// (0x00513260) and both play_default_script overloads /// (0x005132B0, 0x00513300). /// -internal sealed class EntityEffectController : IAnimationHookSink, +public sealed class EntityEffectController : IAnimationHookSink, IEntityEffectAdvanceSource { private readonly LiveEntityRuntime _liveEntities; diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs index 88677b61..0e9a6362 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; using AcDream.Core.World; @@ -15,7 +15,7 @@ namespace AcDream.App.Rendering.Vfx; /// (0x0051D180). This registry is the modern, read-only seam exposing /// those same final frames without coupling Core effects to the renderer. /// -internal sealed class EntityEffectPoseRegistry : +public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityEffectCellSource, IEntityEffectPoseChangeSource, @@ -314,7 +314,7 @@ internal sealed class EntityEffectPoseRegistry : } } -internal interface IEntityEffectPoseLifetimeSource +public interface IEntityEffectPoseLifetimeSource { ulong GetPoseOwnerLifetimeVersion(uint localEntityId); } diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs index 4696558e..304b44bf 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs @@ -1,4 +1,4 @@ -using AcDream.App.World; +using AcDream.App.World; using AcDream.Core.Net.Messages; using AcDream.Core.Vfx; using DatReaderWriter.DBObjs; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx; /// a network PhysicsDesc then unconditionally replaces the typed table, even /// when PeTable was absent or zero. /// -internal sealed class EntityEffectProfile : ILiveEntityEffectProfile +public sealed class EntityEffectProfile : ILiveEntityEffectProfile { private EntityEffectProfile(Setup setup) { diff --git a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs index 97b23c27..d998be06 100644 --- a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs +++ b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; using AcDream.Core.World; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Vfx; /// Setup script/profile data resolved once when a logical effect owner is /// registered. Part transforms are indexed and root-local. /// -internal sealed record ScriptActivationInfo( +public sealed record ScriptActivationInfo( uint ScriptId, IReadOnlyList PartTransforms, EntityEffectProfile? EffectProfile = null, @@ -26,7 +26,7 @@ internal sealed record ScriptActivationInfo( /// initialization. Live registration invokes this class once per logical /// generation; spatial rebucketing never replays it. /// -internal sealed class EntityScriptActivator +public sealed class EntityScriptActivator { private sealed class StaticOwnerState { diff --git a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs index 65e6582f..97a74b7e 100644 --- a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs +++ b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs @@ -1,4 +1,4 @@ -using AcDream.App.World; +using AcDream.App.World; using AcDream.Core.Lighting; using AcDream.Core.Physics; using AcDream.Runtime.Entities; @@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Vfx; /// ; leaving the world removes only this /// cell-scoped presentation and re-entry registers it again. /// -internal sealed class LiveEntityLightController : IDisposable +public sealed class LiveEntityLightController : IDisposable { private readonly LiveEntityRuntime _liveEntities; private readonly EntityEffectPoseRegistry _poses; diff --git a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs index db95c41f..f922dd29 100644 --- a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs +++ b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Vfx; namespace AcDream.App.Rendering.Vfx; @@ -18,7 +18,7 @@ internal interface IWorldSceneParticleVisibility /// frame meaning: one completed viewer position plus the AC cells admitted by /// that completed view. It neither creates emitters nor performs rendering. /// -internal sealed class ParticleVisibilityController : IWorldSceneParticleVisibility +public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility { public const float ExtendedRangeMultiplier = 2f; diff --git a/src/AcDream.App/Rendering/ViewconeCuller.cs b/src/AcDream.App/Rendering/ViewconeCuller.cs index 2bbbbc2d..d686deeb 100644 --- a/src/AcDream.App/Rendering/ViewconeCuller.cs +++ b/src/AcDream.App/Rendering/ViewconeCuller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -6,31 +6,31 @@ namespace AcDream.App.Rendering; /// /// T3 (BR-5): the port of retail's Render::viewconeCheck (Ghidra -/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal +/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal /// view by a bounding-sphere test against the view's edge planes, never /// clipped. Retail stores each view vertex with its 3D eye-edge plane /// (view_vertex { Vec2D pt; Plane plane }, acclient.h:32483) and tests /// the object's drawing sphere against the installed view's plane set; -/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310, +/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310, /// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840). /// -/// Our views are clip-space half-planes (≤8 per slice, +/// Our views are clip-space half-planes (≤8 per slice, /// output: (nx,ny,0,d) satisfied when -/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space — -/// the view_vertex.plane analog, a plane through the EYE and the view edge — +/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space — +/// the view_vertex.plane analog, a plane through the EYE and the view edge — /// is one matrix fold: with row-vector convention (System.Numerics), -/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P) +/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P) /// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the -/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside). +/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside). /// /// A sphere is visible through a SLICE when it is not entirely outside /// any of the slice's planes (convex region); visible for a CELL when any of /// the cell's slices passes. A slice with zero planes is pass-all (the -/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in +/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in /// retail an object whose cell is not in the draw list is simply never /// reached. /// -internal sealed class ViewconeCuller +public sealed class ViewconeCuller { private const int MaxRetainedCellPlaneSets = 512; private const int MaxRetainedPlanesPerCell = 256; @@ -65,7 +65,7 @@ internal sealed class ViewconeCuller } /// True when the outside view is a full-screen pass-all (the - /// synthetic outdoor root) — every outside-test passes. + /// synthetic outdoor root) — every outside-test passes. public bool OutsideIsFullScreen { get; private set; } public static ViewconeCuller Build( @@ -158,7 +158,7 @@ internal sealed class ViewconeCuller Vector4 l = plane.Equation; float nLen = plane.NormalLength; if (nLen < 1e-12f) - continue; // degenerate plane — no constraint + continue; // degenerate plane — no constraint float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W; if (dist < -radius * nLen) return false; // entirely outside this edge plane @@ -167,7 +167,7 @@ internal sealed class ViewconeCuller } /// Sphere-vs-the-cell's-views: visible when any slice passes. - /// A cell with no views culls (not in the draw list ⇒ never reached in + /// A cell with no views culls (not in the draw list ⇒ never reached in /// retail). A zero-plane slice is pass-all. public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius) { diff --git a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs index 168c7612..913b7bf1 100644 --- a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs +++ b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.Physics; namespace AcDream.App.Rendering.Wb; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb; /// equipped items). Holds AC-specific per-instance customizations the WB /// atlas cache doesn't carry: AnimPartChange override map + /// HiddenParts bitmask. Also holds a reference to acdream's existing -/// — Phase N.4 explicitly does not touch +/// — Phase N.4 explicitly does not touch /// the sequencer; we just route through it at draw time. /// /// @@ -16,11 +16,11 @@ namespace AcDream.App.Rendering.Wb; /// a server CreateObject is processed; destroyed by /// EntitySpawnAdapter.OnRemove on RemoveObject. The mesh /// data backing each part is cached in WB's ObjectMeshManager; -/// per-instance customizations don't go through the atlas — they overlay +/// per-instance customizations don't go through the atlas — they overlay /// at draw time. /// /// -internal sealed class AnimatedEntityState +public sealed class AnimatedEntityState { private readonly Dictionary _partGfxObjOverrides = new(); private ulong _hiddenMask = 0; @@ -49,7 +49,7 @@ internal sealed class AnimatedEntityState } /// Override the GfxObj id for a Setup part. Used for - /// AnimPartChange — e.g. wielding a weapon swaps the hand-part's + /// AnimPartChange — e.g. wielding a weapon swaps the hand-part's /// GfxObj. public void SetPartOverride(int partIdx, ulong gfxObjId) => _partGfxObjOverrides[partIdx] = gfxObjId; diff --git a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs index 299e0350..cde447c2 100644 --- a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs +++ b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs @@ -1,4 +1,4 @@ -using Silk.NET.OpenGL; +using Silk.NET.OpenGL; using Silk.NET.OpenGL.Extensions.ARB; using AcDream.App.Rendering; @@ -9,7 +9,7 @@ namespace AcDream.App.Rendering.Wb; /// for the modern rendering path. Constructed once at startup via /// , which returns false if the extension isn't present. /// -internal sealed class BindlessSupport +public sealed class BindlessSupport { private readonly GL _gl; private readonly ArbBindlessTexture _ext; @@ -63,7 +63,7 @@ internal sealed class BindlessSupport /// make it resident. Idempotent per (texture, sampler) pair. /// /// Added for Campaign V slice V1's GlGpuDevice.RegisterTexture, - /// which registers a (texture, sampler) pair per the RHI contract — "the + /// which registers a (texture, sampler) pair per the RHI contract — "the /// same texture registered with two samplers occupies two slots." The /// texture-only above cannot express /// that; ManagedGLTextureArray already calls the equivalent @@ -117,7 +117,7 @@ internal sealed class BindlessSupport // and removed when terrain rendering surfaced GL_INVALID_OPERATION on // NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB // combination. The replacement pattern (uvec2 handle uniform + GLSL - // sampler-from-handle constructor — see terrain_modern.frag) lives at the + // sampler-from-handle constructor — see terrain_modern.frag) lives at the // call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If // you re-introduce a sampler-handle helper, restrict it to drivers known // to accept the direct sampler-uniform path. diff --git a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs index 47d08f38..19788818 100644 --- a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs +++ b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System; using System.Collections.Generic; @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; namespace AcDream.App.Rendering.Wb { - internal static class BufferUsageExtensions { + public static class BufferUsageExtensions { /// /// Converts a BufferUsage to a GL BufferUsageARB /// diff --git a/src/AcDream.App/Rendering/Wb/Building.cs b/src/AcDream.App/Rendering/Wb/Building.cs index e43d5134..aaff1b96 100644 --- a/src/AcDream.App/Rendering/Wb/Building.cs +++ b/src/AcDream.App/Rendering/Wb/Building.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.Rendering.Wb; /// -/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked +/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked /// via the dat-level LandBlockInfo.Buildings entry. Building shells (cottage -/// walls, inn walls — IsBuildingShell=true entities) are scoped to this +/// walls, inn walls — IsBuildingShell=true entities) are scoped to this /// building's cells via their dat-derived anchor. The exit portal polygons are /// stencil-marked so outdoor visibility leaks through portal silhouettes only. /// @@ -19,7 +19,7 @@ namespace AcDream.App.Rendering.Wb; /// Retail reference: docs/research/named-retail/acclient.h:32035 /// (BuildInfo) + 32094 (CBldPortal). /// -internal sealed class Building +public sealed class Building { /// Unique within a landblock; allocated sequentially by /// starting at 1 (0 is reserved for "no building" semantics on LoadedCell). diff --git a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs index b5e6c978..bcb6c586 100644 --- a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs +++ b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter.DBObjs; @@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb; /// Algorithm (mirrors WB's PortalService.GetPortalsByBuilding at /// WorldBuilder.Shared/Services/PortalService.cs:43-97): /// -/// Step A — seed the cell set from BuildingInfo.Portals entry portals. -/// Step B — BFS through to discover all +/// Step A — seed the cell set from BuildingInfo.Portals entry portals. +/// Step B — BFS through to discover all /// interior cells reachable from the entry portals (interior portals only; -/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch). -/// Step C — collect exit portal polygons in world space for the stencil +/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch). +/// Step C — collect exit portal polygons in world space for the stencil /// pipeline (Phase A8 Steps 1+2, RR7 scope). /// /// @@ -57,7 +57,7 @@ internal sealed class BuildingRegistryPublication internal bool PublicationCommitted { get; set; } } -internal static class BuildingLoader +public static class BuildingLoader { /// /// Builds a from the supplied landblock data. diff --git a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs index f2826008..998f64bd 100644 --- a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs +++ b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-26): per-landblock registry of s. -/// Two-way indexed for O(1) cell→building and building-id→building lookups. +/// Two-way indexed for O(1) cell→building and building-id→building lookups. /// Built once per landblock at load time by ; /// no mutations occur after initial population. /// -/// The cell→building index uses a List<Building> value type -/// to handle the (rare but valid) case where two buildings share an EnvCell — +/// The cell→building index uses a List<Building> value type +/// to handle the (rare but valid) case where two buildings share an EnvCell — /// each building performs its own BFS so a shared boundary cell ends up in both /// EnvCellIds sets. returns all /// owners so RR7's render path can pick the correct one. @@ -19,13 +19,13 @@ namespace AcDream.App.Rendering.Wb; /// (BuildingPortalGroup). Design: /// docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md. /// -internal sealed class BuildingRegistry +public sealed class BuildingRegistry { - // Index 1: cell-id → list of buildings containing that cell. + // Index 1: cell-id → list of buildings containing that cell. // Cells may belong to multiple buildings (rare; handled via List). private readonly Dictionary> _byCellId = new(); - // Index 2: building-id → Building. + // Index 2: building-id → Building. private readonly Dictionary _byBuildingId = new(); /// diff --git a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs index a8b28c68..66bdd06a 100644 --- a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs +++ b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs @@ -1,10 +1,10 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering.Wb { // Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs. // LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped; // default color values inlined from LandscapeColorsSettings field initializers. - internal class DebugRenderSettings { + public class DebugRenderSettings { public bool ShowBoundingBoxes { get; set; } = false; public bool SelectVertices { get; set; } = true; public bool SelectBuildings { get; set; } = true; diff --git a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs index ee4a4d4f..80d1119d 100644 --- a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs +++ b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb; /// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this). /// [StructLayout(LayoutKind.Sequential, Pack = 4)] -internal struct DrawElementsIndirectCommand +public struct DrawElementsIndirectCommand { public uint Count; // index count for this draw public uint InstanceCount; // number of instances diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs index 35d75023..1caaa343 100644 --- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Physics; using AcDream.Core.World; @@ -11,7 +11,7 @@ namespace AcDream.App.Rendering.Wb; /// logical removal has been queued and must be retried by the live-entity /// teardown owner after the transition unwinds. /// -internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid) +public sealed class EntityPresentationRemovalDeferredException(uint serverGuid) : InvalidOperationException( $"Live entity 0x{serverGuid:X8} presentation removal is deferred until its active reference transition completes."); @@ -43,7 +43,7 @@ internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid /// both to the created . /// /// -internal sealed class EntitySpawnAdapter +public sealed class EntitySpawnAdapter { private readonly IEntityTextureLifetime _textureLifetime; private readonly Func _sequencerFactory; @@ -161,14 +161,14 @@ internal sealed class EntitySpawnAdapter } // A.5 T18: populate cached AABB so WalkEntities reads from the cache - // rather than recomputing Position±5 per frame. Called here because + // rather than recomputing Position±5 per frame. Called here because // all entity-state initialization (position, rotation) is complete // by this point via the WorldEntity passed in. entity.RefreshAabb(); // Build the per-entity AnimatedEntityState. The sequencer factory // may return a stub (in tests) or a fully-constructed sequencer from - // the MotionTable (in production). Factory must not return null — + // the MotionTable (in production). Factory must not return null — // if the entity has no motion table the factory should construct a // no-op sequencer (Setup + empty MotionTable + NullAnimationLoader). var sequencer = _sequencerFactory(entity); @@ -185,7 +185,7 @@ internal sealed class EntitySpawnAdapter // Snapshot each unique GfxObj id for the shorter presentation lifetime. // Includes both the entity's natural MeshRefs AND any server-sent - // PartOverride GfxObjs (weapons, clothing, helmets) — those replace the + // PartOverride GfxObjs (weapons, clothing, helmets) — those replace the // Setup default and need their own mesh data uploaded. // Construct the replacement completely before displacing a live owner. // Sequencer/appearance construction is allowed to fail; in that case diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs index 9fbbfe81..9a1c02a0 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Numerics; using AcDream.Core.Rendering.Wb; using DatReaderWriter.DBObjs; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb; /// placement data; the render thread schedules mesh preparation when it commits /// the containing . /// -internal sealed record EnvCellShellPlacement( +public sealed record EnvCellShellPlacement( uint CellId, ulong GeometryId, uint EnvironmentId, @@ -29,7 +29,7 @@ internal sealed record EnvCellShellPlacement( /// both portal-visibility cells and drawable shell placements so neither can be /// drained by, or mixed with, another streaming completion. /// -internal sealed class EnvCellLandblockBuild +public sealed class EnvCellLandblockBuild { public EnvCellLandblockBuild( uint landblockId, @@ -57,7 +57,7 @@ internal sealed class EnvCellLandblockBuild /// global pending bags, instances of this class are never shared between jobs or /// observed by the render thread before returns. /// -internal sealed class EnvCellLandblockBuildBuilder +public sealed class EnvCellLandblockBuildBuilder { private readonly uint _landblockId; private readonly List _visibilityCells = new(); diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs index a40d625b..1b39656f 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.Rendering.Wb; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb; /// one private shell at a time; commit publishes the completed immutable /// landblock snapshot with one owner dictionary replacement. /// -internal interface IEnvCellLandblockPublisher +public interface IEnvCellLandblockPublisher { EnvCellLandblockPublication PreparePublication( EnvCellLandblockBuild build); @@ -17,7 +17,7 @@ internal interface IEnvCellLandblockPublisher void CommitPublication(EnvCellLandblockPublication publication); } -internal sealed class EnvCellLandblockPublication +public sealed class EnvCellLandblockPublication { internal EnvCellLandblockPublication( object owner, diff --git a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs index 9f8d2f6c..725c0137 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Starts CPU mesh extraction after the completed EnvCell build has been @@ -7,7 +7,7 @@ /// current streaming generation; stale portal destinations never keep decoder /// jobs or surface lists alive. /// -internal static class EnvCellMeshPreparationScheduler +public static class EnvCellMeshPreparationScheduler { public static void Schedule( EnvCellLandblockBuild build, diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index e4983b56..4c4054a1 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -1,4 +1,4 @@ -// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the +// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the // production cell-rendering pipeline for indoor visibility, replacing the // broken "cell as WorldEntity with MeshRef(envCellId)" approach that the // four reverted RR7 variants couldn't fix. @@ -12,7 +12,7 @@ // // Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base // class owns the landblock-streaming loop (Update, _pendingGeneration, -// _uploadQueue). acdream's StreamingController already does that work — +// _uploadQueue). acdream's StreamingController already does that work — // running a parallel loop would compete for dat I/O. Instead, streaming builds // a private EnvCellLandblockBuild and CommitLandblock publishes the completed // snapshot on the render thread. @@ -29,7 +29,7 @@ using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb; -internal sealed unsafe class EnvCellRenderer : +public sealed unsafe class EnvCellRenderer : IDisposable, IEnvCellLandblockPublisher { @@ -39,7 +39,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly WbFrustum _frustum; // Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF). - // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks — + // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks — // we use uint (full LB id) because acdream uses 32-bit landblock keys throughout. private readonly ConcurrentDictionary _landblocks = new(); @@ -64,7 +64,7 @@ internal sealed unsafe class EnvCellRenderer : private Matrix4x4 _lastViewProjection = Matrix4x4.Identity; private bool _initialized; - // List pool — copied from WB ObjectRenderManagerBase. + // List pool — copied from WB ObjectRenderManagerBase. // WB ObjectRenderManagerBase.cs:83-86: protected readonly List> _listPool = new(); protected int _poolIndex = 0; private readonly List> _listPool = new(); private int _poolIndex = 0; @@ -78,7 +78,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly ThreadLocal _prepareScratch = new(() => new PrepareScratch(), trackAllValues: true); - // Modern-MDI scratch buffers (single slot — we re-upload every frame). + // Modern-MDI scratch buffers (single slot — we re-upload every frame). // WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers // We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws. private uint _mdiCommandBuffer; @@ -95,7 +95,7 @@ internal sealed unsafe class EnvCellRenderer : // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to // _modernInstanceBuffer. One uint per instance selecting its CellClip slot, // indexed by the same BaseInstance + gl_InstanceID the shader uses for - // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots. + // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots. private uint _clipSlotBuffer; private int _clipSlotCapacity; private uint[] _clipSlotData = Array.Empty(); @@ -156,17 +156,17 @@ internal sealed unsafe class EnvCellRenderer : // Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, // GpuBindingModel.StorageTextureTable). Owns its own table rather than - // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache + // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache // dependency and nothing requires index agreement between renderers (each // rebinds its own buffer to binding=9 immediately before its own draw // call). See GlBindlessHandleTable's doc comment and the campaign doc's - // §5.2. Lazily created; grown/uploaded only when a genuinely new handle - // appears (rare — see FlushAndBindTextureTable). + // §5.2. Lazily created; grown/uploaded only when a genuinely new handle + // appears (rare — see FlushAndBindTextureTable). private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; private int _textureTableSsboCapacityBytes; - // Reusable scratch arrays — avoid per-frame allocation. + // Reusable scratch arrays — avoid per-frame allocation. // WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>() private DrawElementsIndirectCommand[] _commands = Array.Empty(); private ModernBatchData[] _modernBatches = Array.Empty(); @@ -192,7 +192,7 @@ internal sealed unsafe class EnvCellRenderer : private readonly Dictionary> _activeSnapshotGlobalGroups = new(); private readonly List _activeSnapshotGlobalGfxObjIds = new(); - // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28. + // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28. // Shared across all manager instances on the same GL context. private static uint _currentVao; private static CullMode? _currentCullMode; @@ -204,8 +204,8 @@ internal sealed unsafe class EnvCellRenderer : // inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell // filter, the trim window, mesh render-data availability (the snapshot bakes // per-cell transparency from TryGetRenderData), or the view-projection. - // NeedsPrepare existed since A8 but was never read — this wires it. The VP - // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer + // NeedsPrepare existed since A8 but was never read — this wires it. The VP + // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer // R-A2 note) while any real camera motion crosses it in the same frame. private Matrix4x4 _preparedViewProjection; private Vector3 _preparedCameraPosition; @@ -223,14 +223,14 @@ internal sealed unsafe class EnvCellRenderer : public bool IsDisposed { get; private set; } public LastFrameStats Stats => _lastFrameStats; - internal struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; } + public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; } private LastFrameStats _lastFrameStats; /// /// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28). /// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water). /// A divergence between expected and actual values would indicate a pool- - /// management regression — exactly the bug class the 2026-05-28 audit caught. + /// management regression — exactly the bug class the 2026-05-28 audit caught. /// public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics() { @@ -338,20 +338,20 @@ internal sealed unsafe class EnvCellRenderer : public void SetClipRegionSsbo(uint sharedClipRegionSsbo) => _sharedClipRegionSsbo = sharedClipRegionSsbo; - // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When + // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When // non-null, RenderModernMDIInternal writes instanceClipSlot[i] = // _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are // gated to that cell's portal-clip region. When null (U.3 path), every // instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0 - // (no-clip) — but the caller's Render filter already restricts the draw to the + // (no-clip) — but the caller's Render filter already restricts the draw to the // map's keys, so that fallback should not fire in practice. private IReadOnlyDictionary? _cellIdToSlot; /// - /// Phase U.4: install the per-frame cellId→slot map used to gate cell shells + /// Phase U.4: install the per-frame cellId→slot map used to gate cell shells /// to their portal-clip regions. Call once per frame BEFORE /// . Pass null to revert to - /// the U.3 no-clip behavior (every shell instance → slot 0). + /// the U.3 no-clip behavior (every shell instance → slot 0). /// public void SetClipRouting(IReadOnlyDictionary? cellIdToSlot) => _cellIdToSlot = cellIdToSlot; @@ -386,7 +386,7 @@ internal sealed unsafe class EnvCellRenderer : surfaces); // --------------------------------------------------------------------------- - // CommitLandblock — render-thread transaction boundary + // CommitLandblock — render-thread transaction boundary // --------------------------------------------------------------------------- /// @@ -586,7 +586,7 @@ internal sealed unsafe class EnvCellRenderer : int? renderRadius = null) { // Phase U.4 fix: stash the view-projection so Render() can upload it itself. - // Stashed even when the gate below skips the rebuild — Render must always + // Stashed even when the gate below skips the rebuild — Render must always // project with the CURRENT frame's matrix (the U.4 stale-matrix root cause). _lastViewProjection = viewProjection; @@ -614,7 +614,7 @@ internal sealed unsafe class EnvCellRenderer : return; } - // Prepare gate: every snapshot input unchanged → keep the active snapshot. + // Prepare gate: every snapshot input unchanged → keep the active snapshot. // (Same-thread discipline makes the version sample exact: publish, release // tickets, and this method all run on the render thread.) if (_hasPreparedSnapshot @@ -633,7 +633,7 @@ internal sealed unsafe class EnvCellRenderer : lock (_renderLock) { _poolIndex = 0; } // WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant - // because we don't need camera-LB tracking for the snapshot — just frustum tests. + // because we don't need camera-LB tracking for the snapshot — just frustum tests. // WB EnvCellRenderManager.cs:262: // Filter loaded landblocks by GpuReady + Instances non-empty. @@ -674,7 +674,7 @@ internal sealed unsafe class EnvCellRenderer : PrepareScratch scratch = _prepareScratch.Value!; - // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside. + // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside. if (testResult == FrustumTestResult.Inside) { foreach (var (gfxObjId, instances) in lb.BuildingPartGroups) @@ -692,7 +692,7 @@ internal sealed unsafe class EnvCellRenderer : return; } - // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test. + // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test. HashSet visibleCells = scratch.VisibleCells; visibleCells.Clear(); foreach (var kvp in lb.EnvCellBounds) @@ -804,13 +804,13 @@ internal sealed unsafe class EnvCellRenderer : /// /// Pure half of the prepare gate's camera test (regression-tested without a /// GL context, same pattern as ). - /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest + /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest /// jitter but dirties on any real movement (a slow walk moves 20+ mm/frame). - /// Position must not be tested through the matrix — the view-projection's + /// Position must not be tested through the matrix — the view-projection's /// translation row scales with world coordinates (~5e4 in AC), where a - /// relative tolerance would mask sub-meter motion. Rows 1–3 of - /// view × projection are position-independent (rotation × projection), so a - /// relative 1e-5 there dirties at ≈0.001° of rotation and on any + /// relative tolerance would mask sub-meter motion. Rows 1–3 of + /// view × projection are position-independent (rotation × projection), so a + /// relative 1e-5 there dirties at ≈0.001° of rotation and on any /// projection (FOV/aspect/near/far) change. /// internal static bool CameraApproximatelyEqual( @@ -926,7 +926,7 @@ internal sealed unsafe class EnvCellRenderer : // Verbatim port of WB EnvCellRenderManager.cs:395-511. // Deviations from WB (all documented): // - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5). - // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state. + // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state. // - Replace RenderModernMDI(base) with private RenderModernMDIInternal. // - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader // class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing @@ -947,7 +947,7 @@ internal sealed unsafe class EnvCellRenderer : /// filter (the drawable visible cells from the PView traversal; each cell's /// shell instances are clip-gated to its CellClip slot by the caller's /// binding=3 map). NOTE: this is NOT the old two-pipe RenderInsideOut approach - /// — that flat camera-inside-building stencil pass was deleted in Phase U.1. + /// — that flat camera-inside-building stencil pass was deleted in Phase U.1. /// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights). /// public void Render(WbRenderPass renderPass, HashSet? filter) @@ -979,7 +979,7 @@ internal sealed unsafe class EnvCellRenderer : // WB EnvCellRenderManager.cs:403-404: _shader.Use(); // FIX 2026-05-28 (pool aliasing root cause): mirror WB - // EnvCellRenderManager.cs:405 — restore the pool cursor to the + // EnvCellRenderManager.cs:405 — restore the pool cursor to the // high-water mark Prepare's merge phase reached, so any // GetPooledList calls below return lists past the snapshot's // owned region. Original code used `snapshot.BatchedByCell.Count` @@ -999,7 +999,7 @@ internal sealed unsafe class EnvCellRenderer : // RenderInsideOutAcdream stencil pipeline) change the actual GL // state without updating these caches. The cache then lies, and // the per-batch SetCullMode in RenderModernMDIInternal skips its - // glCullFace call — leaving stale cull state from the prior + // glCullFace call — leaving stale cull state from the prior // consumer. For a cottage with mixed CullMode batches, half the // walls end up culled and the user sees "missing walls". // @@ -1012,15 +1012,15 @@ internal sealed unsafe class EnvCellRenderer : _shader.SetInt("uRenderPass", (int)renderPass); _shader.SetInt("uFilterByCell", 0); _shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun) - // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. + // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); // Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when // moving"): upload uViewProjection HERE rather than inheriting it from // WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's // Draw (GameWindow ~7411 vs ~7418, the only other setter), so without - // this the opaque shells used the PREVIOUS frame's matrix — a stale - // gl_Position against this frame's clip planes → pose-dependent clipping, + // this the opaque shells used the PREVIOUS frame's matrix — a stale + // gl_Position against this frame's clip planes → pose-dependent clipping, // worst while moving. Same self-contained-GL-state precedent as the // 2026-05-28 cull-state cache fix above. _shader.SetMatrix4("uViewProjection", _lastViewProjection); @@ -1059,7 +1059,7 @@ internal sealed unsafe class EnvCellRenderer : else if (filter is null) { RebuildUnfilteredGroups(snapshot); - // WB EnvCellRenderManager.cs:418-429: optimized path — global groups. + // WB EnvCellRenderManager.cs:418-429: optimized path — global groups. foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds) { if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms)) @@ -1144,7 +1144,7 @@ internal sealed unsafe class EnvCellRenderer : renderPass); } - // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state). + // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state). // WB EnvCellRenderManager.cs:506-509: cleanup. _shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0)); @@ -1170,12 +1170,12 @@ internal sealed unsafe class EnvCellRenderer : ? dc.renderData.Batches[0].IndexCount / 3 : 0) * dc.count; - // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY. + // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY. // Per opaque-pass call: totals + per visible (filtered) cell whether it is // present in the prepared snapshot, and its geometry/flags. Answers why the - // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry - // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture - // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/ + // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry + // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture + // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/ // occlusion or the geometry isn't the wall). Opaque pass only (halves noise). if (renderPass == WbRenderPass.Opaque && AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled) @@ -1217,7 +1217,7 @@ internal sealed unsafe class EnvCellRenderer : /// /// True if the cell's prepared snapshot has any transparent render batch. /// The pview shell pass uses this to skip the (heavy per-frame) transparent - /// call for opaque-only cells — most cell geometry is + /// call for opaque-only cells — most cell geometry is /// opaque walls/floors/ceilings, so this removes the bulk of the per-cell /// transparent draws. Read-only; mirrors the [shell] probe's batch scan. /// @@ -1227,7 +1227,7 @@ internal sealed unsafe class EnvCellRenderer : // --------------------------------------------------------------------------- // GetCellLightSet (A7 Fix D D-2 helper) // Per-cell up-to-8 point lights, cached per frame. Camera-independent, like - // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds. + // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds. // --------------------------------------------------------------------------- // A7 Fix D (D-2): the up-to-8 point lights reaching a cell, by the cell's world @@ -1248,21 +1248,21 @@ internal sealed unsafe class EnvCellRenderer : var snap = _pointSnapshot; // Landblocks are keyed by the streaming landblock id 0xXXYYFFFF - // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock + // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock // key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key // (0xXXYY0000) NEVER matched a registered landblock, so this lookup always // missed: SelectForObject never ran and every EnvCell wall received ZERO // point lights (the entire "indoor torches/lanterns don't light the room" - // bug — confirmed by the [cell-light] probe: inBounds=False for every cell). + // bug — confirmed by the [cell-light] probe: inBounds=False for every cell). if (snap is { Count: > 0 } && _landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) && lb.EnvCellBounds.TryGetValue(cellId, out var b)) { Vector3 center = (b.Min + b.Max) * 0.5f; float radius = (b.Max - b.Min).Length() * 0.5f; - // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL + // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL // dynamic lights on every cell (stable), not the per-object sphere-overlap cull that - // let the portal set flip as the flood shifted → floor-lighting flap. + // let the portal set flip as the flood shifted → floor-lighting flap. AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set); } cached.FrameGeneration = _lightFrameGeneration; @@ -1414,9 +1414,9 @@ internal sealed unsafe class EnvCellRenderer : int passIdx = (int)renderPass; if (passIdx < 0 || passIdx > 2) return; - // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads. + // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads. // Without the global VAO nothing can draw, and returning AFTER the pass state - // was established leaked it (same early-out shape as the totalDraws==0 leak — + // was established leaked it (same early-out shape as the totalDraws==0 leak — // see the comment on the state-establish block below). var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u; if (globalVao == 0) return; @@ -1468,14 +1468,14 @@ internal sealed unsafe class EnvCellRenderer : // transparent). Restored to opaque defaults at the end of the draw loop so a // Transparent pass can't leak into later draws. // - // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the + // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the // totalDraws==0 early-out above. It used to run before the batch grouping, so a // Transparent pass over a cell whose batches are ALL opaque (a plain cottage // interior) set Blend-on/DepthMask-off and then returned at the count check // WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's // glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world // fragment failed GL_LESS against its own previous-frame depth ghost, and the - // whole screen dropped to the fog-tinted clear color — onset-locked to the + // whole screen dropped to the fog-tinted clear color — onset-locked to the // building-flood merge (the first frame a flooded building shell draws), holding // until camera rotation dropped the cell from the flood. From here down every // path reaches the end-of-pass restore. @@ -1689,14 +1689,14 @@ internal sealed unsafe class EnvCellRenderer : // Phase U.4: upload the per-instance clip-slot buffer (binding=3). When // _cellIdToSlot is set (indoor routing), each cell shell instance is gated // to its cell's CellClip slot via allInstances[i].CellId; cells absent from - // the map (shouldn't happen — the Render filter is the map's keys) and the + // the map (shouldn't happen — the Render filter is the map's keys) and the // U.3 path both map to slot 0 (no-clip). allInstances is laid out in the // SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so // instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance. if (_clipSlotData.Length < uniqueInstanceCount) _clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)]; // #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell - // instance to slot 0 (no-clip) — retail draws cell shells WHOLE. + // instance to slot 0 (no-clip) — retail draws cell shells WHOLE. if (_cellIdToSlot is null || AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim) { @@ -1728,7 +1728,7 @@ internal sealed unsafe class EnvCellRenderer : // #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light // sets read through the just-cleared cache against THIS frame's - // _pointSnapshot — the exact data the SSBO upload below carries. + // _pointSnapshot — the exact data the SSBO upload below carries. if (renderPass == WbRenderPass.Opaque && AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled) EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter); @@ -1766,7 +1766,7 @@ internal sealed unsafe class EnvCellRenderer : PersistActiveDynamicBufferCapacities(); // WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier. - // (globalVao validated at the top of the method — a return here would leak the + // (globalVao validated at the top of the method — a return here would leak the // pass state established above.) if (_currentVao != globalVao) { @@ -1865,16 +1865,16 @@ internal sealed unsafe class EnvCellRenderer : } // --------------------------------------------------------------------------- - // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. + // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. // The in-engine replacement for the RenderDoc pixel-history the pipeline - // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern - // startup gate throws). Per opaque pass: for each target cell — flood + // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern + // startup gate throws). Per opaque pass: for each target cell — flood // membership, every shell instance (count + translation, F3 z shows the - // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the + // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the // cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 + // intensity; raw indices shuffle when the pool rebuilds). Plus the - // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are - // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity + // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are + // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity // flipping with flood membership = the snapshot-scope mechanism; two // coincident instances = the z-fight. See RenderingDiagnostics. // --------------------------------------------------------------------------- @@ -1995,8 +1995,8 @@ internal sealed unsafe class EnvCellRenderer : /// Uploads 's handles to /// when a new one was registered since the last flush, then (re)binds it at /// . - /// A genuinely new handle is rare — new dat surfaces/atlases, not every - /// frame — so this is not part of the ring-buffered per-frame SSBO set; + /// A genuinely new handle is rare — new dat surfaces/atlases, not every + /// frame — so this is not part of the ring-buffered per-frame SSBO set; /// see GlBindlessHandleTable's doc comment. /// private void FlushAndBindTextureTable() @@ -2068,7 +2068,7 @@ internal sealed unsafe class EnvCellRenderer : GLEnum.DynamicDraw, "allocating EnvCell fallback clip SSBO"); allocated = true; - // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. + // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. Span zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes]; zero.Clear(); fixed (byte* p = zero) @@ -2103,12 +2103,12 @@ internal sealed unsafe class EnvCellRenderer : private List GetPooledList() { - // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse + // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse // branch MUST clear the list before returning. PrepareRenderBatches' // merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`, // which assumes the list is empty. Without the clear, lists grow // unbounded across frames and each frame's draw includes all prior - // frames' stale data. Original port omitted the Clear() call — root + // frames' stale data. Original port omitted the Clear() call — root // cause of post-Wave-5 visual chaos (FIX 2026-05-28). See // docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md. lock (_listPool) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs index aec47b79..55f0b34f 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs @@ -1,4 +1,4 @@ -// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs +// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs // Phase A8 extraction (2026-05-28). Verbatim port; adaptations: // - SceneryInstance -> EnvCellSceneryInstance (scope-narrow to env-cell rendering) // - ObjectLandblock -> EnvCellLandblock @@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Wb; /// Lightweight data for a single placed env-cell scenery object. /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 11-56) /// -internal struct EnvCellSceneryInstance +public struct EnvCellSceneryInstance { /// GfxObj or Setup ID from DAT. public ulong ObjectId; @@ -67,7 +67,7 @@ internal struct EnvCellSceneryInstance /// Shared by both scenery and static object render managers. /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 62-160) /// -internal class EnvCellLandblock +public class EnvCellLandblock { /// Grid X coordinate of this landblock. public int GridX { get; set; } diff --git a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs index e3eeeaa1..77120b39 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace AcDream.App.Rendering.Wb; @@ -9,14 +9,14 @@ namespace AcDream.App.Rendering.Wb; /// narrowed to the fields actually consumes /// (BatchedByCell + VisibleLandblocks + PostPreparePoolIndex). /// The scenery-side VisibleGroups / VisibleGfxObjIds / -/// IntersectingLandblocks are dropped — we render scenery through +/// IntersectingLandblocks are dropped — we render scenery through /// , not through this snapshot. /// /// Used as an immutable snapshot atomically swapped under the /// renderer's render lock so PrepareRenderBatches (worker-driven) and /// Render (render-thread-driven) can't race on a half-populated dict. /// -internal sealed class EnvCellVisibilitySnapshot +public sealed class EnvCellVisibilitySnapshot { /// Landblocks fully or partially inside the frustum at prepare time. public List VisibleLandblocks { get; init; } = new(); @@ -38,7 +38,7 @@ internal sealed class EnvCellVisibilitySnapshot /// cursor to a safe region past the snapshot's owned lists, so any /// GetPooledList calls inside Render don't trample data the /// snapshot still references. Dropping this field caused the post-Wave-5 - /// visual chaos — see + /// visual chaos — see /// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md. /// public int PostPreparePoolIndex { get; init; } diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs index 7b85e242..8e0640d6 100644 --- a/src/AcDream.App/Rendering/Wb/GLHelpers.cs +++ b/src/AcDream.App/Rendering/Wb/GLHelpers.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Silk.NET.Core.Native; using Silk.NET.OpenGL; using System; @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { - internal static class GLHelpers { + public static class GLHelpers { public static OpenGLGraphicsDevice? Device { get; set; } public static ILogger? Logger { get; set; } diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs index fa610dd7..f4eabecf 100644 --- a/src/AcDream.App/Rendering/Wb/GLSLShader.cs +++ b/src/AcDream.App/Rendering/Wb/GLSLShader.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using AcDream.App.Rendering; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -14,7 +14,7 @@ using System.Xml.Linq; namespace AcDream.App.Rendering.Wb { - internal unsafe class GLSLShader : BaseShader, IDisposable { + public unsafe class GLSLShader : BaseShader, IDisposable { private OpenGLGraphicsDevice _device; private Dictionary _uniformLocations = []; private Dictionary _uniformValues = []; diff --git a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs index 09369907..5b5ea099 100644 --- a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs +++ b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; // Phase O-T7: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils into // the AcDream.App.Rendering.Wb namespace so the WorldBuilder.Shared project @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb; -internal static class GeometryUtils { +public static class GeometryUtils { public static bool RayIntersectsBox(Vector3 rayOrigin, Vector3 rayDirection, Vector3 min, Vector3 max, out float distance) { distance = 0; diff --git a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs index 0510761c..ba074462 100644 --- a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using AcDream.App.Rendering; @@ -73,7 +73,7 @@ internal enum GlobalMeshCapacityResult /// ObjectMeshManager owns allocation lifetime and releases a mesh's ranges /// when its zero-reference LRU entry is evicted. /// -internal sealed class GlobalMeshBuffer : IDisposable +public sealed class GlobalMeshBuffer : IDisposable { internal const int InitialVertexCapacity = 1024 * 1024; internal const int InitialIndexCapacity = 3 * 1024 * 1024; diff --git a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs index 8151e0c9..2b4862c0 100644 --- a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs +++ b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb { /// /// Resource types for GPU memory tracking. /// - internal enum GpuResourceType { + public enum GpuResourceType { Texture, Buffer, Shader, @@ -21,17 +21,17 @@ namespace AcDream.App.Rendering.Wb { /// /// Details about a GPU resource type. /// - internal record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes); + public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes); /// /// Details about a specific named buffer. /// - internal record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes); + public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes); /// /// Tracks manual VRAM allocations for buffers and textures. /// - internal static class GpuMemoryTracker { + public static class GpuMemoryTracker { private static long _allocatedBytes; private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues().Length]; private static readonly int[] _resourceCountsByType = new int[Enum.GetValues().Length]; diff --git a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs index 8faf54d9..dc6c3745 100644 --- a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs +++ b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Logical-owner lifetime seam for per-entity texture composites. Retail /// CSurface::Destroy (0x005361F0) releases its current ImgTex; /// live-object teardown must do the same for modern bindless composites. /// -internal interface IEntityTextureLifetime +public interface IEntityTextureLifetime { /// Release every composite acquired by one local entity id. void ReleaseOwner(uint localEntityId); diff --git a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs index f20a7e33..3ade216b 100644 --- a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Reports the physical outcome when a mesh-reference callback cannot provide @@ -6,7 +6,7 @@ /// a transactional owner reconcile its marker without guessing whether a /// throwing backend already changed the reference count. /// -internal sealed class MeshReferenceMutationException : Exception +public sealed class MeshReferenceMutationException : Exception { public MeshReferenceMutationException( string message, @@ -25,7 +25,7 @@ internal sealed class MeshReferenceMutationException : Exception /// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter) /// can be unit-tested without a real WB pipeline behind them. /// -internal interface IWbMeshAdapter +public interface IWbMeshAdapter { /// /// Acquires one logical reference. A normal exception guarantees that no diff --git a/src/AcDream.App/Rendering/Wb/InstanceData.cs b/src/AcDream.App/Rendering/Wb/InstanceData.cs index 993403bf..bf911ecd 100644 --- a/src/AcDream.App/Rendering/Wb/InstanceData.cs +++ b/src/AcDream.App/Rendering/Wb/InstanceData.cs @@ -1,9 +1,9 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { [StructLayout(LayoutKind.Sequential, Pack = 16)] - internal struct InstanceData { + public struct InstanceData { public const uint INSTANCE_FLAG_DISQUALIFIED = 1u; public Matrix4x4 Transform; // 64 bytes diff --git a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs index 242824c3..2e5949a4 100644 --- a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.World; namespace AcDream.App.Rendering.Wb; @@ -8,9 +8,9 @@ namespace AcDream.App.Rendering.Wb; /// reference-count lifecycle. Tier-aware by design: only atlas-tier /// entities (procedural / dat-hydrated, identified by /// ServerGuid == 0) drive ref counts. Server-spawned entities -/// (per-instance tier) are skipped — those go through +/// (per-instance tier) are skipped — those go through /// EntitySpawnAdapter and the owner-scoped texture path -/// (see Phase N.4 spec, Architecture → Two-tier rendering split). +/// (see Phase N.4 spec, Architecture → Two-tier rendering split). /// /// /// On load: walks the landblock's atlas-tier entities, collects unique @@ -44,7 +44,7 @@ namespace AcDream.App.Rendering.Wb; /// on the owning render/update thread. /// /// -internal sealed class LandblockSpawnAdapter +public sealed class LandblockSpawnAdapter { private sealed class ReferenceRegistration { diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs index 107dcab3..7a3b190a 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Silk.NET.OpenGL; using System; using System.Collections.Generic; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET. /// - internal class ManagedGLFramebuffer : IFramebuffer { + public class ManagedGLFramebuffer : IFramebuffer { private readonly OpenGLGraphicsDevice _device; private GL _gl => _device.GL; private readonly uint _fboId; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs index 95a6aae3..7443fc58 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Silk.NET.OpenGL; using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage; @@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL index buffer /// - internal unsafe class ManagedGLIndexBuffer : IIndexBuffer { + public unsafe class ManagedGLIndexBuffer : IIndexBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private void* _mappedPtr; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs index a3ff538a..31e252e4 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs @@ -1,9 +1,9 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb { - internal unsafe class ManagedGLTexture : ITexture { + public unsafe class ManagedGLTexture : ITexture { private uint _texture; private readonly OpenGLGraphicsDevice _device; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs index 1520645a..1115cede 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs @@ -1,7 +1,7 @@ -using AcDream.Core.Rendering.Wb; +using AcDream.Core.Rendering.Wb; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; -// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly +// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; using AcDream.App.Rendering; namespace AcDream.App.Rendering.Wb { - internal class ManagedGLTextureArray : ITextureArray { + public class ManagedGLTextureArray : ITextureArray { private readonly bool[] _usedLayers; private readonly GL GL; private readonly OpenGLGraphicsDevice _device; @@ -46,7 +46,7 @@ namespace AcDream.App.Rendering.Wb { /// #105 diagnostic: staged layer updates (retained decoded payloads) not yet /// applied to the GL texture by . Layers with /// a pending update sample UNDEFINED content (TexStorage3D contents) until the - /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism. + /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism. /// public int PendingUpdateCount { get { lock (_mipmapLock) { return _pendingUpdates.Count; } } diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs index 4ab11f4c..ebcc56d2 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System.Runtime.InteropServices; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL uniform buffer /// - internal unsafe class ManagedGLUniformBuffer : IUniformBuffer { + public unsafe class ManagedGLUniformBuffer : IUniformBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private GL GL => _device.GL; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs index 27abfc4d..583e82d9 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Silk.NET.OpenGL; using System; @@ -9,7 +9,7 @@ using System.Threading.Tasks; using VertexAttribType = Silk.NET.OpenGL.VertexAttribType; namespace AcDream.App.Rendering.Wb { - internal unsafe class ManagedGLVertexArray : IVertexArray { + public unsafe class ManagedGLVertexArray : IVertexArray { private readonly OpenGLGraphicsDevice _device; private GL GL => _device.GL; private uint _vaoId = 0; diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs index 92b09777..d9a3752a 100644 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; @@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL vertex buffer /// - internal unsafe class ManagedGLVertexBuffer : IVertexBuffer { + public unsafe class ManagedGLVertexBuffer : IVertexBuffer { private uint bufferId; private readonly OpenGLGraphicsDevice _device; private void* _mappedPtr; diff --git a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs index 4d8192aa..a3b497eb 100644 --- a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs +++ b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using DatReaderWriter.Enums; using Chorizite.Core.Render; @@ -9,17 +9,17 @@ namespace AcDream.App.Rendering.Wb { /// bindless handle) and the layer index within the shared/pooled array. /// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape /// as mesh_modern.vert's BatchData: TextureTableIndex/Reserved/TextureIndex/ - /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests. + /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests. /// [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct ModernBatchData { - public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table - public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12 - public uint TextureIndex; // 4 bytes — layer within the texture array - public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags + public struct ModernBatchData { + public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table + public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12 + public uint TextureIndex; // 4 bytes — layer within the texture array + public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags } - internal struct LandblockMdiCommand { + public struct LandblockMdiCommand { public ulong SortKey; public ulong ObjectId; public DrawElementsIndirectCommand Command; diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index 6ecc75b9..d3d97c4f 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Lib; +using Chorizite.Core.Lib; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using DatReaderWriter.DBObjs; @@ -26,7 +26,7 @@ namespace AcDream.App.Rendering.Wb /// /// GPU-side render data created on the main thread. /// - internal class ObjectRenderData + public class ObjectRenderData { public uint VAO { get; set; } public uint VBO { get; set; } @@ -74,7 +74,7 @@ namespace AcDream.App.Rendering.Wb /// /// A single GPU draw batch: IBO + texture array layer. /// - internal class ObjectRenderBatch + public class ObjectRenderBatch { public uint IBO { get; set; } public int IndexCount { get; set; } @@ -101,7 +101,7 @@ namespace AcDream.App.Rendering.Wb /// Key design: mesh data is prepared on background threads via PrepareMeshData(), /// then GPU resources are created on the main thread via UploadMeshData(). /// - internal class ObjectMeshManager : IDisposable + public class ObjectMeshManager : IDisposable { private readonly OpenGLGraphicsDevice _graphicsDevice; private readonly IPreparedAssetSource _preparedAssets; @@ -171,7 +171,7 @@ namespace AcDream.App.Rendering.Wb private volatile bool _arenaBackpressured; /// #125: how many times a failed GL upload is re-staged before - /// giving up loudly. Small — a transient GL error clears on the next + /// giving up loudly. Small — a transient GL error clears on the next /// frame; anything that fails this many times is a genuine defect to /// surface, not retry forever. See . public const int MaxUploadRetries = 3; @@ -179,8 +179,8 @@ namespace AcDream.App.Rendering.Wb /// /// #125: drain one staged upload, returning whether it should be /// re-staged for a later frame. The caller (the per-frame Tick drain) - /// collects the re-stages and re-enqueues them AFTER the drain loop — - /// never inside it — so a deterministic failure can't spin the queue in + /// collects the re-stages and re-enqueues them AFTER the drain loop — + /// never inside it — so a deterministic failure can't spin the queue in /// a single frame. increments the mesh /// data's own counter only when new upload work actually starts (not /// while a prior rollback waits); this drain gives up loudly past @@ -197,7 +197,7 @@ namespace AcDream.App.Rendering.Wb if (UploadMeshData(meshData) is not null) { _stagedMeshData.Complete(item); - return false; // success (incl. legitimate 0-vertex → empty render data) + return false; // success (incl. legitimate 0-vertex → empty render data) } if (HasRenderData(meshData.ObjectId)) { @@ -215,7 +215,7 @@ namespace AcDream.App.Rendering.Wb if (meshData.UploadAttempts < MaxUploadRetries) return true; // re-stage for next frame _stagedMeshData.Complete(item); - Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)"); + Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)"); return false; } @@ -570,7 +570,7 @@ namespace AcDream.App.Rendering.Wb /// /// #105 diagnostic: counts staged-but-unflushed texture layer updates across all /// shared atlases (see ). - /// Render thread only — _globalAtlases is render-thread-owned. + /// Render thread only — _globalAtlases is render-thread-owned. /// public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats() { @@ -838,7 +838,7 @@ namespace AcDream.App.Rendering.Wb _cpuMeshCache.Clear(); } - internal struct EnvCellGeomRequest + public struct EnvCellGeomRequest { public uint SourceCellId; public uint EnvironmentId; @@ -1520,14 +1520,14 @@ namespace AcDream.App.Rendering.Wb { // 0-vertex mesh: every polygon was gated out at extraction. #119 // (2026-06-11) dat-verified this is LEGITIMATE for all-no-draw - // models (all polys NoPos + Base1Solid surfaces — retail's + // models (all polys NoPos + Base1Solid surfaces — retail's // skipNoTexture never draws them either; 0x010002B4/0x010008A8 // are this class, Issue119UpNullGfxObjDumpTests). The empty // cache is the correct terminal state for those. The line stays // as a tripwire for the OTHER way to get here (extraction - // dropped textured polys — a real defect; dat-verify with the + // dropped textured polys — a real defect; dat-verify with the // dump test before treating as one). - Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)"); + Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)"); renderData = new ObjectRenderData(); } @@ -1611,7 +1611,7 @@ namespace AcDream.App.Rendering.Wb /// /// Plans the actual GL work the next object would trigger against the /// current atlas inventory. This includes array storage, global-buffer - /// growth/copies, and one full mip generation per newly-dirtied array—not merely the + /// growth/copies, and one full mip generation per newly-dirtied array—not merely the /// source byte arrays held by ObjectMeshData. /// internal MeshUploadCost PlanUploadCost( @@ -1852,7 +1852,7 @@ namespace AcDream.App.Rendering.Wb #region Private: Background Preparation /// - /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP — + /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP — /// the polys retail actually renders (D3DPolyRender traverses the BSP; /// dictionary-orphaned polys are physics/no-draw geometry). Returns null /// when the model has no drawing BSP (caller draws everything). @@ -1985,7 +1985,7 @@ namespace AcDream.App.Rendering.Wb } atlasManager.LastUseSequence = ++_atlasUseSequence; - // MP1a: AcDream.Content is Silk.NET-free — the extraction records + // MP1a: AcDream.Content is Silk.NET-free — the extraction records // carry Content-owned UploadPixelFormat/UploadPixelType enums whose // underlying values are the GL ABI constants (numerically identical // to Silk.NET.OpenGL.PixelFormat/PixelType), so this lifted nullable @@ -2591,7 +2591,7 @@ namespace AcDream.App.Rendering.Wb // disposes the DatCollection right after this adapter chain, which // unmaps the dats' memory-mapped views. A worker still inside // MemoryMappedBlockAllocator.ReadBlock at that point dereferences the - // dead view pointer — an uncatchable, process-fatal AccessViolation + // dead view pointer — an uncatchable, process-fatal AccessViolation // (dat-race investigation 2026-06-09). Setting IsDisposed under the // queue lock publishes it to workers, which re-check it before every // dequeue; draining the queue means each worker exits after at most diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs index 997c39f4..38bf7e1d 100644 --- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs +++ b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs @@ -1,4 +1,4 @@ -using Chorizite.Core.Render; +using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Vertex; using AcDream.App.Rendering; @@ -20,7 +20,7 @@ namespace AcDream.App.Rendering.Wb { /// /// OpenGL graphics device /// - internal unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice { + public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice { private readonly ILogger _log; private readonly DebugRenderSettings _renderSettings; private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement; diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs index fb62cd2c..6453ed7e 100644 --- a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs +++ b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Tracks currently-bound GL state to skip redundant rebinds across the @@ -7,9 +7,9 @@ /// here in Phase O-T7 to eliminate the WorldBuilder project reference. /// /// Semantics are identical to the WB originals: -/// CurrentAtlas — slot index of the currently bound texture atlas. -/// CurrentVAO — OpenGL name of the currently bound vertex array object. -/// CurrentIBO — OpenGL name of the currently bound index buffer object. +/// CurrentAtlas — slot index of the currently bound texture atlas. +/// CurrentVAO — OpenGL name of the currently bound vertex array object. +/// CurrentIBO — OpenGL name of the currently bound index buffer object. /// Sentinel value 0 means "no valid binding cached." /// /// @@ -18,7 +18,7 @@ /// corresponding glBind* call and read by the next dispatch on the /// same thread. /// -internal static class RenderStateCache +public static class RenderStateCache { public static uint CurrentAtlas = 0; public static uint CurrentVAO = 0; diff --git a/src/AcDream.App/Rendering/Wb/SceneData.cs b/src/AcDream.App/Rendering/Wb/SceneData.cs index 76ba5a19..2923404f 100644 --- a/src/AcDream.App/Rendering/Wb/SceneData.cs +++ b/src/AcDream.App/Rendering/Wb/SceneData.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { @@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Wb { /// Global scene data for Uniform Buffer Object (UBO) /// [StructLayout(LayoutKind.Sequential, Pack = 16)] - internal struct SceneData { + public struct SceneData { public Matrix4x4 View; // 64 bytes public Matrix4x4 Projection; // 64 bytes public Matrix4x4 ViewProjection; // 64 bytes diff --git a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs index 66bda875..ee9ed87f 100644 --- a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs +++ b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using Chorizite.Core.Render; using Chorizite.Core.Render.Enums; using DatReaderWriter.Enums; @@ -42,7 +42,7 @@ namespace AcDream.App.Rendering.Wb { /// Manages texture arrays grouped by (Width, Height, Format). /// Deduplicates textures by a TextureKey and supports reference counting. /// - internal class TextureAtlasManager : IDisposable { + public class TextureAtlasManager : IDisposable { private static uint _nextSlot = 1; private readonly OpenGLGraphicsDevice _graphicsDevice; private readonly int _textureWidth; diff --git a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs index 32d7b008..d3b00bb3 100644 --- a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs +++ b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs @@ -1,9 +1,9 @@ -using Chorizite.Core.Render.Enums; +using Chorizite.Core.Render.Enums; using Silk.NET.OpenGL; using System; namespace AcDream.App.Rendering.Wb { - internal static class TextureFormatExtensions { + public static class TextureFormatExtensions { public static SizedInternalFormat ToGL(this Chorizite.Core.Render.Enums.TextureFormat format) { return format switch { TextureFormat.RGBA8 => SizedInternalFormat.Rgba8, diff --git a/src/AcDream.App/Rendering/Wb/TextureParameters.cs b/src/AcDream.App/Rendering/Wb/TextureParameters.cs index f7c49af4..30a7be14 100644 --- a/src/AcDream.App/Rendering/Wb/TextureParameters.cs +++ b/src/AcDream.App/Rendering/Wb/TextureParameters.cs @@ -1,10 +1,10 @@ -using Silk.NET.OpenGL; +using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb { /// /// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering. /// - internal struct TextureParameters { + public struct TextureParameters { public TextureWrapMode WrapS; public TextureWrapMode WrapT; public TextureMinFilter MinFilter; @@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb { public bool EnableMipmaps; public bool EnableAnisotropicFiltering; - /// Standard tiling textures — Repeat + trilinear + aniso. + /// Standard tiling textures — Repeat + trilinear + aniso. public static readonly TextureParameters Default = new() { WrapS = TextureWrapMode.Repeat, WrapT = TextureWrapMode.Repeat, @@ -22,7 +22,7 @@ namespace AcDream.App.Rendering.Wb { EnableAnisotropicFiltering = true, }; - /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso. + /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso. public static readonly TextureParameters ClampToEdge = new() { WrapS = TextureWrapMode.ClampToEdge, WrapT = TextureWrapMode.ClampToEdge, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs index 81a2325a..b04df854 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Rendering; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Selection; @@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Wb; /// routes reuse the dispatcher's exact mesh, texture, light, translucency, /// selection, upload, and draw owners. /// -internal sealed unsafe partial class WbDrawDispatcher +public sealed unsafe partial class WbDrawDispatcher { private readonly Dictionary _packedEntityById = []; diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 0484da78..bb82c7e1 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -34,12 +34,12 @@ namespace AcDream.App.Rendering.Wb; /// WB. Native surfaces still reuse the WB atlas; only actual indexed-palette /// and original-texture replacements resolve through owner-scoped /// composites. is currently -/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges + +/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges + /// GfxObjDegradeResolver (Issue #47 close-detail mesh) into MeshRefs. /// /// /// -/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs +/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs /// and GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters. /// All visible (entity, batch) pairs are bucketed by ; /// each group becomes one DrawElementsIndirectCommand. Three GPU buffers @@ -54,7 +54,7 @@ namespace AcDream.App.Rendering.Wb; /// /// Shader: mesh_modern (bindless + gl_DrawIDARB / /// gl_BaseInstanceARB). Missing bindless/draw-parameters throws -/// at startup — there is no legacy fallback. +/// at startup — there is no legacy fallback. /// /// /// @@ -65,7 +65,7 @@ namespace AcDream.App.Rendering.Wb; /// glMultiDrawElementsIndirect. /// /// -internal sealed unsafe partial class WbDrawDispatcher : IDisposable +public sealed unsafe partial class WbDrawDispatcher : IDisposable { /// /// Which subset of entities to walk in a single Draw call. @@ -78,10 +78,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// the unified pass can re-introduce partitioning later without re-threading /// the call sites. /// - internal enum EntitySet + public enum EntitySet { /// Every entity walked, gated only by the existing - /// ParentCellId ∈ visibleCellIds filter. + /// ParentCellId ∈ visibleCellIds filter. All, } @@ -100,7 +100,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private readonly BindlessSupport _bindless; private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver; - internal readonly record struct DrawStats( + public readonly record struct DrawStats( EntitySet Set, int EntitiesWalked, int MeshRefs, @@ -419,16 +419,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Tier 1 cache (#53): per-entity classification results for static // entities (those NOT in GameWindow._animatedEntities). Wired here in - // Task 7 for plumbing only — Tasks 9-10 wire the per-entity + // Task 7 for plumbing only — Tasks 9-10 wire the per-entity // miss-populate / hit-fast-path through the loop. private readonly EntityClassificationCache _cache; - // #188 — per-(entity, Setup-part) translucency ramp state (fading doors / + // #188 — per-(entity, Setup-part) translucency ramp state (fading doors / // secret-passage walls). ClassifyBatches reads this per part to compute // the instance's opacity multiplier; never mutated here. private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades; - // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static + // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static // entity through the slow path. Read once in ctor. private readonly bool _tier1CacheDisabled = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DISABLE_TIER1_CACHE"), "1", StringComparison.Ordinal); @@ -453,7 +453,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to // _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3 - // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is + // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is // identical to pre-U.3. U.4 populates real slot indices. private uint _clipSlotSsbo; private int _clipSlotSsboCapacityBytes; @@ -461,7 +461,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two // SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot - // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4) + // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4) // holds the per-frame point-light snapshot (LightManager.PointSnapshot); // _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per // instance INTO it (-1 = unused), laid out parallel to _instanceSsbo. @@ -495,10 +495,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual // Vulkan global texture descriptor array (binding=9, - // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare — - // new dat surfaces/atlases, not every frame — so this single buffer is + // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare — + // new dat surfaces/atlases, not every frame — so this single buffer is // NOT part of the ring-buffered DynamicBufferSet below; see - // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why + // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why // this table is owned here rather than by GlGpuDevice. Lazily created. private readonly GlBindlessHandleTable _textureTable = new(); private uint _textureTableSsbo; @@ -537,15 +537,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count); private Vector2[] _selectionLightingData = new Vector2[256]; // This frame's point-light snapshot, handed in by GameWindow before Draw via - // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). + // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). private IReadOnlyList? _pointSnapshot; - // This entity's selected point/spot light set — computed ONCE per entity at + // This entity's selected point/spot light set — computed ONCE per entity at // the isNewEntity site (constant across the entity's parts/tuples), exactly // like _currentEntitySlot. -1 = unused slot. private readonly int[] _currentEntityLightSetScratch = new int[LightManager.MaxLightsPerObject]; private InstanceLightSet _currentEntityLightSet = InstanceLightSet.Disabled; - // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet, + // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet, // parallel to _currentEntityLightSet. True when IndoorObjectReceivesTorches fires // (ParentCellId is an EnvCell). Appended to InstanceGroup.IndoorFlags in // AppendCurrentLightSet; uploaded as binding=6 instanceIndoor[]. @@ -563,7 +563,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before // each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root / // no portal frame), every instance maps to slot 0 (no-clip) and no instance is - // culled — identical to U.3. When active, each instance's slot is resolved by + // culled — identical to U.3. When active, each instance's slot is resolved by // ResolveEntitySlot per the U.4 policy (cell-owned entities to their cell slot; // outdoor-owned entities to OutsideView; non-visible/unresolved indoors culled). private bool _clipRoutingActive; @@ -583,7 +583,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // across the entity's tuples; the per-tuple body skips all instance emission. private bool _currentEntityCulled; - // Per-frame scratch arrays — Tasks 9-10 fully wire these. + // Per-frame scratch arrays — Tasks 9-10 fully wire these. private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance private BatchData[] _batchData = new BatchData[256]; private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256]; @@ -600,7 +600,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at // offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8, - // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so + // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so // every existing CPU writer's offsets are unchanged (see // GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a // 64-bit ulong TextureHandle (an ARB_bindless_texture handle, uvec2 in @@ -611,7 +611,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private struct BatchData { public uint TextureIndex; // slot into the binding=9 handle table - public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12 + public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12 public uint TextureLayer; public uint Flags; } @@ -674,7 +674,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable => owner.ResetDeferredAlphaSubmissions(); } - // Per-frame scratch — reused across frames to avoid per-frame allocation. + // Per-frame scratch — reused across frames to avoid per-frame allocation. private readonly Dictionary _groups = new(); private readonly List _opaqueDraws = new(); private readonly List _translucentDraws = new(); @@ -704,7 +704,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable * Unsafe.SizeOf()); // A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list // instead of allocating a fresh List<(WorldEntity, int)> per frame. At - // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame + // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame // of GC pressure on the render thread under the original T17 shape. private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new(); // G2: the dispatcher consumes this acdream-owned value boundary, never @@ -712,16 +712,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // retained list; G4 will replace only that producer with RenderFrameView. private readonly List _candidateTupleScratch = new(); - // Tier 1 cache (#53) — per-entity classification collector. Reused across + // Tier 1 cache (#53) — per-entity classification collector. Reused across // frames; cleared at flush time when the per-entity loop crosses an entity // boundary in _walkScratch (and once more at end-of-loop for the last // entity). _walkScratch is in entity-order, so all MeshRefs of one entity - // are contiguous — accumulate them all before flushing one Populate call. + // are contiguous — accumulate them all before flushing one Populate call. // Animated entities skip this scratch entirely (collector = null). private readonly List _populateScratch = new(); private readonly List _populateSelectionScratch = new(); - // Per-entity-cull AABB radius. Conservative — covers most entities; large + // Per-entity-cull AABB radius. Conservative — covers most entities; large // outliers (long banners, tall columns) are still landblock-culled. private const float PerEntityCullRadius = 5.0f; @@ -763,7 +763,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private long _lastLogTick; // #128 self-heal: per-Draw dedup of point-of-use load re-requests - // (PrepareMeshDataAsync is idempotent while pending — the dedup just + // (PrepareMeshDataAsync is idempotent while pending — the dedup just // avoids redundant dictionary probes within one pass) + the once-per-id // [mesh-miss] diagnostic set (never cleared; diag-gated emission). private readonly HashSet _missRequested = new(); @@ -772,7 +772,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #119 decisive probe (2026-06-11): ACDREAM_DUMP_ENTITY one-shot entity // dump. Keyed by entity Id; the stored signature re-emits the header line // whenever (MeshRefs count, cache batch count, zero-translation count, - // culled) changes — e.g. the Tier-1 populate landing one frame after the + // culled) changes — e.g. the Tier-1 populate landing one frame after the // first slow-path draw. The full per-part listing prints only on first // sight. Inert (one Count==0 check per new entity) when the env var is // unset. Render-thread only. @@ -798,12 +798,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth]; private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth]; // #125: a glGenQueries name does not become a QUERY OBJECT until its first - // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION. + // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION. // The N.6 ring assumed ONE Draw per frame with both passes always non-empty; // the pview pipeline issues MANY small Draws per frame (landscape slices, // per-cell buckets, dynamics), where zero-draw passes routinely skip // BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an - // InvalidOperation EVERY frame — silently, until WB's diligent texture-path + // InvalidOperation EVERY frame — silently, until WB's diligent texture-path // glGetError checks ate the stale errors and treated their own successful // uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates' // check threw (process death; tower-wbdiag3.log). Track which slots were @@ -816,7 +816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable private bool _gpuQueriesInitialized; // Constructor accessibility is internal because EntityClassificationCache - // is internal — a public ctor with an internal-typed parameter would be + // is internal — a public ctor with an internal-typed parameter would be // an inconsistent-accessibility error. The dispatcher is constructed // exclusively from GameWindow (same assembly), so internal is fine. internal WbDrawDispatcher( @@ -898,7 +898,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (). Call once per frame BEFORE /// . The dispatcher uploads it to binding=4 and selects each /// object's up-to-8 lights from it () - /// by the object's bounding sphere — camera-independent. Pass null/empty to + /// by the object's bounding sphere — camera-independent. Pass null/empty to /// disable per-object point lights (only ambient + sun render). /// public void SetSceneLights(IReadOnlyList? pointSnapshot) @@ -923,11 +923,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Pair with on outdoor-root frames so the /// dispatcher reverts to the U.3 no-clip-everything behavior. /// - /// cellId → CellClip slot. A cell absent from the map - /// is NOT visible → its cell-static instances are culled. + /// cellId → CellClip slot. A cell absent from the map + /// is NOT visible → its cell-static instances are culled. /// Slot for outdoor scenery / building shells while /// indoors (the OutsideView slot, or 0 for no-clip over-include). - /// False ⇒ cull outdoor scenery / shells this frame + /// False ⇒ cull outdoor scenery / shells this frame /// (the OutsideView is empty). public void SetClipRouting(IReadOnlyDictionary cellIdToSlot, int outdoorSlot, bool outdoorVisible) { @@ -939,7 +939,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } /// - /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip), + /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip), /// nothing is culled by clip routing. Call on outdoor-root frames (camera /// outdoors) and any frame without a portal-visibility result. /// @@ -951,16 +951,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _outdoorVisible = false; } - // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change + // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change // signature + monotonic sequence + reusable histogram. See RenderingDiagnostics // .ProbeClipRouteEnabled for the full probe contract. private string? _lastClipRouteDispSig; private long _clipRouteDispSeq; private readonly SortedDictionary _clipRouteHist = new(); - // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3. + // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3. // grp.Slots is laid out 1:1 with grp.Matrices (binding=0), so this IS the slot content - // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish + // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish // into cullEnt) when the building flood merges, this line shows it directly. private void EmitClipRouteDispatchProbe(int culledEntities) { @@ -1001,7 +1001,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // are dropped entirely (not emitted into the binding=0 instance buffer NOR the // binding=3 slot buffer), matching the existing frustum / visible-cell cull. // Internal (not private) so the clip-slot unit tests can assert against it - // directly — see WbDrawDispatcherClipSlotTests. + // directly — see WbDrawDispatcherClipSlotTests. internal const int ClipSlotCull = -1; /// @@ -1014,14 +1014,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// ServerGuid != 0 with ParentCellId == null: CULL while routing is active. /// /// Only called when _clipRoutingActive (indoor root). On the U.3 / outdoor - /// path every instance is slot 0 and nothing is culled — see + /// path every instance is slot 0 and nothing is culled — see /// , which gates on that flag. /// /// INVARIANT: and the keys of /// MUST live in the same FULL cell-id space /// (lbMask | OtherCellId, e.g. 0xA9B40164). A bare-low-byte /// ParentCellId (e.g. 0x64) would never match a full-id key and would - /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in + /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in /// CLAUDE.md where player CellId was tracked without its landblock prefix. /// /// @@ -1073,12 +1073,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (Slot, Culled) pair the per-entity loop body consumes. Wraps /// with the /// gate: when routing is INACTIVE (outdoor root / no portal frame), every entity - /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so + /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so /// the resolver (and ) is bypassed entirely. - /// When active, a CULL sentinel maps to (0, culled=true) — the slot value + /// When active, a CULL sentinel maps to (0, culled=true) — the slot value /// is never emitted for a culled entity. /// internal static + pure so the whole policy (including the routing- - /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests. + /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests. /// internal static (uint Slot, bool Culled) ResolveSlotForFrame( bool clipRoutingActive, @@ -1106,7 +1106,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Entry for per-landblock iteration. /// Mirrors the shape yielded by GpuWorldState.LandblockEntries. /// - internal readonly record struct LandblockEntry( + public readonly record struct LandblockEntry( uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, @@ -1114,10 +1114,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable IReadOnlyDictionary? AnimatedById); /// - /// Result of — the list of (entity, meshRef index) + /// Result of — the list of (entity, meshRef index) /// pairs that passed all visibility filters, plus a diagnostic walk count. /// - internal struct WalkResult + public struct WalkResult { public int EntitiesWalked; public int BuildingShellAnchorPass; @@ -1142,7 +1142,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// A.5 T18 Change #2: per-entity AABB cull reads from the cached /// / /// (refreshed lazily if ), instead of - /// recomputing Position±5 each frame. + /// recomputing Position±5 each frame. /// /// /// @@ -1177,7 +1177,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// visibleCellIds or frustum filters, and [indoor-walk] lines for /// cell entities that pass all filters. Rate-limited by /// . Pass (the default) - /// to disable all probe emission — used by the test-friendly + /// to disable all probe emission — used by the test-friendly /// overload. /// /// @@ -1238,8 +1238,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (!EntityMatchesSet(entity, set)) continue; if (entity.MeshRefs.Count == 0) continue; - // Detect cell entity for indoor probes — first MeshRef.GfxObjId - // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute; + // Detect cell entity for indoor probes — first MeshRef.GfxObjId + // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute; // result reused for all probe checks below. ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId; bool isCellEntity = indoorProbeState is not null @@ -1265,7 +1265,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } if (shellScoped) result.BuildingShellAnchorPass++; - // Per-entity AABB frustum cull (perf #3). Animated entities bypass — + // Per-entity AABB frustum cull (perf #3). Animated entities bypass — // they're tracked at landblock level + need per-frame work regardless. // A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty. bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true; @@ -1291,7 +1291,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable continue; } - // Passed all filters — emit walk probe. + // Passed all filters — emit walk probe. if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled && indoorProbeState!.ShouldEmit(cellProbeId)) { @@ -1315,16 +1315,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// #119 ROOT-CAUSE FIX (2026-06-11): the Tier-1 cache hint must identify the /// entity's OWNING landblock, not the Draw call's tuple landblock. /// RetailPViewRenderer.DrawEntityBucket fabricates its tuple with the - /// PLAYER's landblock id, so every bucket entity that frame shared one hint — + /// PLAYER's landblock id, so every bucket entity that frame shared one hint — /// and colliding entity ids from different landblocks (the pre-fix /// 0x40YYFF00 interior namespace discarded the landblock X byte) mapped /// to the SAME cache key and served each other's batches: the AAB3 tower's /// 43-part staircase drew a 1-part entity's 3 zero-RestPose batches - /// (captured live, tower-dump-launch1.log) — the session-sticky "broken + /// (captured live, tower-dump-launch1.log) — the session-sticky "broken /// stairs + water barrel". Interior statics carry their owning cell; derive /// the hint from it, canonicalized to the same 0xXXYYFFFF key format /// the streaming entries and - /// use — which also makes owner-unload invalidation actually hit these + /// use — which also makes owner-unload invalidation actually hit these /// entries (bucket-hinted entries were previously orphaned forever). /// Entities without a ParentCellId (outdoor stabs / scenery / building /// shells via GpuWorldState entries) keep the tuple id, which IS their @@ -1404,7 +1404,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// (meshRefs, cacheBatches, zeroTranslations, culled) signature changes. /// Discriminates H-A (hydration-time MeshRef corruption: translations /// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a - /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose). + /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose). /// private void MaybeEmitEntityDump( in RenderInstanceCandidate entity, @@ -1503,7 +1503,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable out Matrix4x4 vp, out Vector3 camPos); - // ── Phase 1: clear groups, walk entities, build groups ────────────── + // ── Phase 1: clear groups, walk entities, build groups ────────────── // Draw is invoked several times per frame (landscape slices, late // dynamics, paperdoll). Per-dispatch payloads reset here, while group // retirement happens once in BeginFrame from whole-frame liveness. @@ -1534,7 +1534,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled) { // _currentFrame is snapped at construction time. Construct - // once per Draw() call only — a second construction within + // once per Draw() call only — a second construction within // the same frame would stamp the dictionary with the // (already-advanced) counter value, suppressing the second // pass's emissions for IndoorProbeRateLimitFrames frames. @@ -1572,35 +1572,35 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // end of the loop for the last entity. Flushing per-tuple would // overwrite earlier MeshRefs (the cache is keyed by entity.Id), so // multi-part Setup-backed entities would only retain their LAST - // MeshRef's batches — bug fixed in commit after 2f489a8. + // MeshRef's batches — bug fixed in commit after 2f489a8. uint? populateEntityId = null; uint populateLandblockId = 0; - // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by - // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity — + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by + // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity — // cheap enough to count unconditionally; emission below is probe-gated. int probeCulledEntities = 0; - // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a + // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a // FLAT list of batches across all MeshRefs of an entity, so a single // ApplyCacheHit call already drew every batch. _walkScratch yields // one tuple per (entity, MeshRefIndex), so without this guard a // 3-MeshRef static entity on a frame-2 cache hit would call - // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances - // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit + // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances + // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit // on every multi-part static entity (buildings, statues, multi-MeshRef // NPCs). The fast path must fire only on the FIRST tuple of each // entity; subsequent tuples skip via this tracker. uint? lastHitEntityId = null; - // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of + // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of // the current entity has _meshAdapter.TryGetRenderData return null // (mesh still async-decoding via ObjectMeshManager.PrepareMeshDataAsync), // we mark the entity incomplete and DROP the accumulated populate // scratch at entity boundary instead of writing it to the cache. // Otherwise the cache would hold a partial classification (some parts // missing), and frame-2 cache hits would persist that partial render - // even after the missing mesh loads — every subsequent frame sees the + // even after the missing mesh loads — every subsequent frame sees the // cache hit and skips re-classification, so the missing parts never // recover. User-visible symptom: the drudge statue on top of the // Foundry (multi-part Setup entity with AnimPartChange) renders with @@ -1642,9 +1642,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable lastHitEntityId = null; } - // Tier 1 cache (#53) — drop the previous entity's accumulated + // Tier 1 cache (#53) — drop the previous entity's accumulated // populate scratch BEFORE MaybeFlushOnEntityChange runs. If the - // previous entity ended incomplete (≥1 null renderData), we MUST + // previous entity ended incomplete (≥1 null renderData), we MUST // NOT cache its partial classification: clear scratch and null // the tracker so MaybeFlushOnEntityChange sees the cleaned state // and no-ops for this entity. Reset the incomplete flag for the @@ -1653,11 +1653,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // CRITICAL: the flag reset must fire ONLY on entity change, not // every tuple. Resetting per-tuple within the same entity would // undo a null-renderData flag set by a previous tuple of the same - // entity → if the missing MeshRef sits in the MIDDLE of the + // entity → if the missing MeshRef sits in the MIDDLE of the // entity's MeshRefs list, a later valid tuple's reset would // re-mark the entity "complete" and let partial data populate // the cache. Trees with [trunk valid, branches null, leaves - // valid] hit this exactly — branches never recover. + // valid] hit this exactly — branches never recover. // #119 root-cause fix: cache operations key on the entity's OWNING // landblock, never the Draw call's tuple landblock (which is the // PLAYER's landblock on the bucket path). See ResolveCacheLandblockHint. @@ -1687,7 +1687,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B: select this entity's up-to-8 point/spot lights ONCE (the set // is constant across the entity's parts/tuples), by the entity's - // bounding sphere — camera-INDEPENDENT (minimize_object_lighting). + // bounding sphere — camera-INDEPENDENT (minimize_object_lighting). ComputeEntityLightSet(entity); _currentEntitySelectionLighting = _selectionLighting?.TryGetLighting( @@ -1749,7 +1749,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Placed AFTER the entity-change flush above so that, on a // hit, this iteration also finishes flushing any pending // populate state from a previous entity. Animated entities never - // enter this branch — the !isAnimated guard makes that explicit. + // enter this branch — the !isAnimated guard makes that explicit. // // Fires ONCE per entity: the first tuple reaches here, runs // ApplyCacheHit, sets lastHitEntityId, and continues. Subsequent @@ -1769,7 +1769,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // anyVao recovery: when the first visible entity in the frame // takes the fast path, no slow-path lookup has populated // anyVao yet. Look up THIS entity's first MeshRef once via - // the mesh adapter — cheap dict lookup, not a re-classify. + // the mesh adapter — cheap dict lookup, not a re-classify. if (anyVao == 0) { MeshRef firstMeshRef = tuple.MeshRef; @@ -1783,7 +1783,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable #if DEBUG // Cross-check guard: assert the membership predicate held at hit time. // The full re-classification cross-check (spec section 6.5) is a stretch - // goal; this simpler assert catches the prior Tier 1 bug class — a + // goal; this simpler assert catches the prior Tier 1 bug class — a // static entity that turns out to actually be animated would fire here. // // Structurally redundant with the `if (!isAnimated && ...)` branch @@ -1793,7 +1793,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // entities into the fast path; the assert catches that immediately. System.Diagnostics.Debug.Assert( !isAnimated, - $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated"); + $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated"); #endif continue; @@ -1806,7 +1806,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride); // Note: GameWindow's spawn path already applies - // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix — + // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix — // close-detail mesh swap for humanoids) to MeshRefs. We // trust MeshRefs as the source of truth here. AnimatedEntityState's // overrides become relevant only for hot-swap (0xF625 @@ -1816,7 +1816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable var renderData = _meshAdapter.TryGetRenderData(gfxObjId); - // [indoor-lookup] probe — emit once per cell entity per sec. + // [indoor-lookup] probe — emit once per cell entity per sec. // Fires BEFORE the null-renderData early-continue so a miss still // emits hit=false, distinguishing H2 (empty batches) from H6 // (dispatcher fails to traverse Setup). @@ -1863,7 +1863,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable currentEntityIncomplete = true; if (diag) _meshesMissing++; // #128 self-heal: a missing-but-referenced mesh re-requests - // its load HERE — the one site that touches it every frame — + // its load HERE — the one site that touches it every frame — // so a preparation lost to landblock churn (cancelled after // the last registration event) can never stay lost. Deduped // per Draw; PrepareMeshDataAsync is idempotent while pending. @@ -1888,11 +1888,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (renderData.IsSetup && renderData.SetupParts.Count > 0) { // #188: setupPartIndex is the SAME index space - // TransparentPartHook.PartIndex addresses — retail's CPartArray + // TransparentPartHook.PartIndex addresses — retail's CPartArray // numbers parts by their ordinal position in the Setup's own // part list (SetupPartTransforms.Compute is the other verified // consumer of this exact indexing: one rigid pose per - // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a + // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a // MeshRef is acdream's own decomposition of top-level // attachments (weapon/shield/etc), a different concept. for (int setupPartIndex = 0; setupPartIndex < renderData.SetupParts.Count; setupPartIndex++) @@ -1903,7 +1903,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable { // #128 self-heal + #53: a missing Setup PART must mark // the entity incomplete (else a partial batch set - // caches permanently — the same bug class one level + // caches permanently — the same bug class one level // deeper) and re-request its load like the MeshRef // path above. currentEntityIncomplete = true; @@ -1920,11 +1920,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable var model = ComposePartWorldMatrix( entityWorld, meshRef.PartTransform, partTransform); - // [indoor-xform] probe — only for the cell's synthetic + // [indoor-xform] probe — only for the cell's synthetic // geometry part (bit 32 set, per WB's PrepareEnvCellMeshData // cellGeomId convention). One line per part per sec. - // Disambiguates hypothesis H5 (transform double-apply — - // composedT lands at 2 × cellOrigin). + // Disambiguates hypothesis H5 (transform double-apply — + // composedT lands at 2 × cellOrigin). if ((partGfxObjId & 0x1_0000_0000UL) != 0 && RenderingDiagnostics.ProbeIndoorXformEnabled && ShouldEmitIndoorProbe(partGfxObjId)) @@ -1941,7 +1941,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a // part's translucency hits EXACTLY 1.0 (fully invisible), retail - // sets draw_state|=1 and skips the whole part outright — not a + // sets draw_state|=1 and skips the whole part outright — not a // blend to nothing. TranslucencyFadeManager.AdvanceAll guarantees // t=1 commits the bitwise-exact value so this check is safe. float opacityMultiplier = 1.0f; @@ -1968,7 +1968,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } else { - // #188: a bare (non-Setup) GfxObj entity has exactly one part — + // #188: a bare (non-Setup) GfxObj entity has exactly one part — // retail's CPartArray for such an object is a single-entry array, // so TransparentPartHook.PartIndex for it is always 0. float opacityMultiplier = 1.0f; @@ -2013,8 +2013,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (diag && drewAny) _entitiesDrawn++; } - // Tier 1 cache (#53) — drop the accumulated populate scratch if the - // LAST entity in the loop ended incomplete (had ≥1 null renderData). + // Tier 1 cache (#53) — drop the accumulated populate scratch if the + // LAST entity in the loop ended incomplete (had ≥1 null renderData). // Same reason as the entity-boundary handling above: avoid caching a // partial classification. The slow path will retry on the next frame // and populate correctly once all meshes have loaded. @@ -2033,7 +2033,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable populateEntityId, populateLandblockId, _cache, _populateScratch, _populateSelectionScratch); - // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance + // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance // histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the // culled-entity count. Routed draws only (the landscape pass under DrawInside) so the // unrouted per-cell bucket draws don't oscillate the print-on-change signature. @@ -2102,7 +2102,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable bool diag, bool observeCurrentPath) { - // Nothing visible — skip the GL pass entirely. + // Nothing visible — skip the GL pass entirely. if (anyVao == 0) { LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); @@ -2116,7 +2116,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return; } - // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ── + // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ── bool deferTransparent = _alphaQueue?.IsCollecting == true; var instanceCounts = PartitionInstanceGroups( groups, @@ -2152,7 +2152,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: size the per-instance clip-slot buffer to match the instance // count and lay it out in the SAME group order / cursor as _instanceData, // so instanceClipSlot[i] (binding=3) tracks Instances[i] (binding=0). On - // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3. + // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3. if (_clipSlotData.Length < immediateInstances) _clipSlotData = new uint[immediateInstances + 256]; @@ -2198,8 +2198,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // submission so the shared queue can interleave it with particles. // Immediate mode remains for sealed off-screen consumers such as the // paperdoll and UI Studio render stack. - // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent), - // fill via BuildIndirectArrays ────────────────────────────────── + // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent), + // fill via BuildIndirectArrays ────────────────────────────────── int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count; int totalDraws = _opaqueDraws.Count + immediateTransparentCount; TrackScratchDemand(Math.Max(totalInstances, totalDraws)); @@ -2260,7 +2260,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable deferTransparent, camPos); - // ── Phase 5: upload four buffers ──────────────────────────────────── + // ── Phase 5: upload four buffers ──────────────────────────────────── ActivateNextDynamicBufferSet(); fixed (float* ip = _instanceData) UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes, @@ -2273,9 +2273,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Phase U.4: per-instance clip-slot buffer (binding=3), one uint per // instance, laid out parallel to _instanceData in Phase 3's group loop so // instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the - // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to + // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to // U.3); under indoor routing it holds the per-instance slot from - // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances + // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances // entries; only [0..immediateInstances) is uploaded, so any stale tail is // never read by the shader. fixed (uint* sp = _clipSlotData) @@ -2284,14 +2284,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #142: per-instance indoor flag buffer (binding=6), one uint per instance, // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). + // is uploaded — stale tail never read (same guarantee as clip-slot above). fixed (uint* dp = _indoorData) UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes, dp, immediateInstances * sizeof(uint)); // #188: per-instance opacity buffer (binding=7), one float per instance, // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). + // is uploaded — stale tail never read (same guarantee as clip-slot above). fixed (float* ap = _alphaData) UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes, ap, immediateInstances * sizeof(float)); @@ -2306,7 +2306,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // Fix B: global point-light buffer (binding=4) + per-instance light-set // buffer (binding=5). The global buffer is this frame's PointSnapshot; the // per-instance buffer holds 8 int indices into it per instance, laid out - // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the + // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the // shader never reads an unbound SSBO on a no-lights frame. UploadGlobalLights(); fixed (int* lp = _lightSetData) @@ -2336,7 +2336,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // shader never reads an unbound SSBO at binding=2. BindClipRegionBinding2(); - // ── Phase 6: bind global VAO once ─────────────────────────────────── + // ── Phase 6: bind global VAO once ─────────────────────────────────── _gl.BindVertexArray(anyVao); if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) @@ -2346,11 +2346,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // result (the oldest data in the ring) before overwriting it with // frame N's queries. Hoisted to function scope so both the opaque // and transparent passes below can reference gpuQuerySlot. See spec - // §3 Q1/Q2 + §4 in + // §3 Q1/Q2 + §4 in // docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md. int gpuQuerySlot = _gpuQueryFrameIndex % GpuQueryRingDepth; // diag is part of the gate so the read/issue/increment trio stays - // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would + // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would // freeze the frame counter (gated by diag below) while the read kept // re-reading the same slot, producing duplicate stale samples. if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth) @@ -2391,14 +2391,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } } - // ── Phase 7: opaque pass ───────────────────────────────────────────── + // ── Phase 7: opaque pass ───────────────────────────────────────────── if (_opaqueDrawCount > 0) { _gl.Disable(EnableCap.Blend); _gl.DepthMask(true); - // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask + // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask // from the alpha written by mesh_modern.frag so foliage edges are - // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches. + // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches. // A.5 T22.5: gated by AlphaToCoverage property so Low/Medium presets // (no MSAA) skip the unnecessary GL state change. if (AlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); @@ -2418,7 +2418,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage); } - // ── Phase 8: transparent pass ──────────────────────────────────────── + // ── Phase 8: transparent pass ──────────────────────────────────────── if (_transparentDrawCount > 0) { _gl.Enable(EnableCap.Blend); @@ -2426,8 +2426,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _gl.DepthMask(false); // Phase Post-A.5 (ISSUE #52, 2026-05-10): transparent section of // Batches[] starts at index _opaqueDrawCount. Without this offset, - // each transparent draw reads BatchData[0..transparentCount) — the - // OPAQUE section — and the lifestone crystal's apparent texture + // each transparent draw reads BatchData[0..transparentCount) — the + // OPAQUE section — and the lifestone crystal's apparent texture // flickers to whatever opaque batch sorted first that frame. See // uDrawIDOffset comment in mesh_modern.vert. _shader.SetInt("uDrawIDOffset", _opaqueDrawCount); @@ -2481,7 +2481,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// explicit cell list (not the BFS-derived visibility set). The semantic /// difference is at the caller: cellIds = the camera-buildings' EnvCellIds, /// not the portal BFS result. The dispatcher's internal logic is identical - /// — it filters indoor entities by membership in the provided set. + /// — it filters indoor entities by membership in the provided set. /// public void Draw( ICamera camera, @@ -2494,7 +2494,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable HashSet? animatedEntityIds = null, EntitySet set = EntitySet.All) { - // Adapt IReadOnlyCollection → HashSet for the existing path. + // Adapt IReadOnlyCollection → HashSet for the existing path. // If the caller already passed a HashSet, avoid re-wrapping. HashSet cellIdSet = cellIds is HashSet hs ? hs : new HashSet(cellIds); Draw(camera, landblockEntries, @@ -3442,8 +3442,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// /// Fix B: pack into the binding=4 global light /// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes, - /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element - /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is + /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element + /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is /// a zeroed dummy that no instance set references (all sets are -1). /// private unsafe void UploadGlobalLights() @@ -3461,8 +3461,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// when a new one was registered since the /// last flush (by or ), /// then (re)binds it at . - /// A genuinely new handle is rare — new dat surfaces/composite overrides, - /// not every frame — so unlike the SSBOs above this is not part of the + /// A genuinely new handle is rare — new dat surfaces/composite overrides, + /// not every frame — so unlike the SSBOs above this is not part of the /// ring-buffered ; see /// 's doc comment. /// @@ -3511,7 +3511,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable if (_fallbackClipRegionSsbo == 0) { _fallbackClipRegionSsbo = _gl.GenBuffer(); - // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. + // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. var zero = stackalloc byte[ClipFrame.CellClipStrideBytes]; for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0; _gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo); @@ -3532,7 +3532,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable long gpuMed = MedianMicros(_gpuSamples); long gpuP95 = Percentile95Micros(_gpuSamples); // A.5 T23: flag when entity dispatcher median exceeds 2.0ms budget - // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix. + // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix. const long BudgetUs = 2000; string budgetFlag = cpuMed > BudgetUs ? " BUDGET_OVER" : ""; Console.WriteLine( @@ -3540,7 +3540,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable $"cpu_us={cpuMed}m/{cpuP95}p95 gpu_us={gpuMed}m/{gpuP95}p95"); _entitiesSeen = _entitiesDrawn = _meshesMissing = _drawsIssued = _instancesIssued = 0; _lastLogTick = now; - // Don't reset the sample buffers — they're a moving window of the + // Don't reset the sample buffers — they're a moving window of the // last 256 frames; clearing per 5s flush would lose recent history. } } @@ -3553,7 +3553,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable foreach (var v in copy) if (v > 0) nz++; if (nz == 0) return 0; // Sorted ascending: zero-padding front, samples at the back. (nz - 1) / 2 - // from the end keeps the offset >= 0 for all nz >= 1 — the original + // from the end keeps the offset >= 0 for all nz >= 1 — the original // nz / 2 form indexed copy[copy.Length] (crash) on the first diag flush // when exactly 1 sample was recorded. Same fix as GameWindow's // TerrainDiagMedianMicros twin. @@ -3571,7 +3571,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return copy[idx]; } - // ── Tier 1 cache (#53) helpers extracted for testability ───────────────── + // ── Tier 1 cache (#53) helpers extracted for testability ───────────────── // // Three pure-CPU static helpers carved out of Draw's per-entity loop so // unit tests can exercise the populate/flush algorithm + cache-hit fast @@ -3669,7 +3669,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// AND differs from , the previous /// entity's accumulated batches are committed to /// and is cleared. Returns the - /// updated tracker tuple — pass these back into the field locals in the + /// updated tracker tuple — pass these back into the field locals in the /// caller's loop. /// /// @@ -3711,7 +3711,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// so commit its accumulated batches here. No-op when no populate is /// pending (the last entity was animated, or the scratch is empty). /// - /// End-of-loop only — does NOT reset the caller's tracker locals + /// End-of-loop only — does NOT reset the caller's tracker locals /// (intentional, since they go out of scope immediately after). /// /// @@ -3784,11 +3784,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable grp.Matrices.Add(model); grp.LocalSortCenters.Add(localSortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); - grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices - AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices + grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices + AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices // #188: cache-hit entities are always non-animated (the Tier-1 cache // gates on !isAnimated), and TranslucencyFadeManager only ever holds - // state for entities whose animation hooks fired — so a cached + // state for entities whose animation hooks fired — so a cached // instance can never be mid-fade. Always unmodified opacity. grp.Opacities.Add(1.0f); grp.SelectionLighting.Add(_currentEntitySelectionLighting); @@ -3800,11 +3800,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// sphere. Camera-independent (), so /// a static building's torches stay constant as the viewer moves. Fills /// ; unused slots are -1. On the no-lights - /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light. + /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light. /// /// /// A7 Fix D round 2 (2026-06-19): retail lights OUTDOOR objects with the SUN + - /// ambient ONLY — never the static wall torches. The per-object torch step + /// ambient ONLY — never the static wall torches. The per-object torch step /// (minimize_object_lighting, 0x0054d480) runs ONLY in the indoor stage: /// RenderDeviceD3D::DrawMeshInternal (0x0059f398) calls it under /// if (Render::useSunlight == 0), and the outdoor landscape stage runs @@ -3815,16 +3815,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// creatures get the sun, not torches. We mirror that: only objects parented to /// an EnvCell (indoor) select torches; outdoor objects keep the all-(-1) set so /// the sun path alone lights them. This is what made the Holtburg meeting-hall - /// facade wash out warm — the dat's intensity-100 wall torches (range - /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights. + /// facade wash out warm — the dat's intensity-100 wall torches (range + /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights. /// The indoor "no sun" half is already handled by the global sun kill when the /// player is inside a cell (UpdateSunFromSky). See the divergence register /// (AP-43) and docs/research/2026-06-19-lighting-a7-fixD-round2-*. /// /// - // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One + // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One // [seam-ent] line per target-cell entity, re-emitted on state change: world - // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot, + // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot, // and the SelectForObject light set resolved to identities (owner-cell // low16 + intensity). Sig dict is bounded by the handful of entities that // ever live in the target cells. @@ -3865,7 +3865,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable { // #142: set the indoor flag first so it's available even when the early-return // fires below. Both the torch selection and the sun gate use the same predicate, - // so they can't disagree — one call, one truth. + // so they can't disagree — one call, one truth. _currentEntityIndoor = IndoorObjectReceivesTorches(entity.ParentCell); @@ -3889,7 +3889,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Retail's useSunlight gate for per-object torch lighting, as a pure /// predicate. An object receives the static wall torches (the indoor /// minimize_object_lighting pass) ONLY when it is parented to an EnvCell - /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100. + /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100. /// Outdoor objects (building shells with null , /// outdoor scenery in a land sub-cell 0x0001..0x00FF, outdoor creatures) /// are sun-lit only and return false. Mirrors @@ -3899,7 +3899,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable internal static bool IndoorObjectReceivesTorches(uint? parentCellId) => parentCellId.HasValue && (parentCellId.Value & 0xFFFFu) >= 0x0100u - && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor + && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor /// /// Fix B: append the current entity's 8-slot light set to a group's @@ -3931,7 +3931,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable // #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap // must route through the alpha-blend pass so mesh_modern.frag's - // (blend-enabled) shader actually composites the reduced alpha — + // (blend-enabled) shader actually composites the reduced alpha — // the no-blend opaque pass would ignore it. if (opacityMultiplier < 1.0f && IsOpaque(translucency)) translucency = TranslucencyKind.AlphaBlend; @@ -3956,9 +3956,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable grp.Matrices.Add(model); grp.LocalSortCenters.Add(renderData.SortCenter); grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++); - grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices - AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices - grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices + grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices + AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices + grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices grp.SelectionLighting.Add(_currentEntitySelectionLighting); collector?.Add(new CachedBatch( key, @@ -4085,7 +4085,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable EntitySet set) { // No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics / outdoor - // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating. + // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating. if (visibleCellIds is null) return true; @@ -4094,7 +4094,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable return visibleCellIds.Contains(entity.ParentCellId.Value); // ParentCellId == null (outdoor scenery / building shell): NOT a member of any interior cell, - // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional + // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional // `return true`). When such entities must draw (through the doorway), the caller passes // visibleCellIds: null and relies on ResolveEntitySlot's OutsideView routing instead. return false; @@ -4272,24 +4272,24 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable _gpuQueriesInitialized = false; } - // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── + // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── // // These are public so the pure-CPU unit tests in AcDream.Core.Tests can // exercise BuildIndirectArrays without needing a GL context. /// /// Stride in bytes of DrawElementsIndirectCommand in the indirect buffer. - /// 5 × uint = 20 bytes. Tests and callers reference this symbolically + /// 5 × uint = 20 bytes. Tests and callers reference this symbolically /// rather than hard-coding 20 so a layout change produces a compile error. /// - public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint + public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint /// - /// Public view of the per-group inputs to — used in tests. + /// Public view of the per-group inputs to — used in tests. /// Campaign V slice V2: TextureIndex is a slot into the binding=9 /// handle table (was a raw 64-bit bindless TextureHandle). /// - internal readonly record struct IndirectGroupInput( + public readonly record struct IndirectGroupInput( int IndexCount, uint FirstIndex, int BaseVertex, @@ -4305,7 +4305,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Tests verify the layout. Same field shape as the private BatchData. /// [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct BatchDataPublic + public struct BatchDataPublic { public uint TextureIndex; public uint Reserved; @@ -4314,7 +4314,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable } /// Result of . - internal readonly record struct IndirectLayoutResult( + public readonly record struct IndirectLayoutResult( int OpaqueCount, int TransparentCount, int TransparentByteOffset); @@ -4325,8 +4325,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// Pure CPU, no GL state. Caller passes pre-sized scratch arrays. /// /// - /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not - /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass. + /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not + /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass. /// public static IndirectLayoutResult BuildIndirectArrays( IReadOnlyList groups, @@ -4385,15 +4385,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// /// Public test shim for . Locks in the N.5 Decision 2 - /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend + - /// Additive + InvAlpha → transparent indirect. + /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend + + /// Additive + InvAlpha → transparent indirect. /// public static bool IsOpaquePublic(TranslucencyKind t) => IsOpaque(t); private static bool IsOpaque(TranslucencyKind t) => t == TranslucencyKind.Opaque || t == TranslucencyKind.ClipMap; - // ──────────────────────────────────────────────────────────────────────── + // ──────────────────────────────────────────────────────────────────────── /// /// Thin wrapper around an instance's rate-limit dictionary + frame @@ -4496,7 +4496,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable /// appended in lockstep (one entry per drawn instance) during group build, so /// they MUST all be cleared together each frame. Keeping the reset in one /// method stops a newly-added parallel list from silently drifting out of the - /// frame lifecycle — which is exactly the #193 OOM: #188 added + /// frame lifecycle — which is exactly the #193 OOM: #188 added /// alongside the others but left it out of the old /// inline clear loop, so it grew one float per instance per frame forever and /// leaked ~1 GB/min of LOH float[] as its backing array doubled. diff --git a/src/AcDream.App/Rendering/Wb/WbFrustum.cs b/src/AcDream.App/Rendering/Wb/WbFrustum.cs index 7e22c5cb..96a254e0 100644 --- a/src/AcDream.App/Rendering/Wb/WbFrustum.cs +++ b/src/AcDream.App/Rendering/Wb/WbFrustum.cs @@ -1,4 +1,4 @@ -// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs +// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs // Phase A8 extraction (2026-05-28). Verbatim algorithm; adaptations: // - Namespace: AcDream.App.Rendering.Wb // - Class renamed Frustum -> WbFrustum @@ -10,7 +10,7 @@ using System.Numerics; namespace AcDream.App.Rendering.Wb; -internal struct WbBoundingBox +public struct WbBoundingBox { public Vector3 Min; public Vector3 Max; @@ -27,7 +27,7 @@ internal struct WbBoundingBox Vector3.Max(a.Max, b.Max)); } -internal enum FrustumTestResult +public enum FrustumTestResult { Outside, Inside, @@ -39,7 +39,7 @@ internal enum FrustumTestResult /// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs /// Phase A8 extraction (2026-05-28). /// -internal sealed class WbFrustum +public sealed class WbFrustum { private struct Plane { diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index 7f59775b..1bc0fd88 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Content; using AcDream.App.Rendering.Residency; @@ -17,11 +17,11 @@ namespace AcDream.App.Rendering.Wb; /// /// /// As of Phase O-T7, all DAT I/O routes through the runtime-owned shared -/// facade — the separate +/// facade — the separate /// DefaultDatReaderWriter file-handle set has been removed. /// /// -internal sealed class WbMeshAdapter +public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter { @@ -291,12 +291,12 @@ internal sealed class WbMeshAdapter private WbMeshAdapter() { - // Uninitialized constructor — only for tests / flag-off cases where + // Uninitialized constructor — only for tests / flag-off cases where // the caller wants a Dispose-safe no-op instance. _isUninitialized = true; } - /// Test/init helper — produces a Dispose-safe instance with no + /// Test/init helper — produces a Dispose-safe instance with no /// underlying mesh manager. Public methods are all no-ops. public static WbMeshAdapter CreateUninitialized() => new(); @@ -310,7 +310,7 @@ internal sealed class WbMeshAdapter /// /// Returns the WB render data for , or null if not /// yet uploaded or if this adapter is uninitialized. Increments WB's - /// internal usage counter — use for + /// internal usage counter — use for /// render-loop lookups that should not affect lifecycle. /// public ObjectRenderData? GetRenderData(ulong id) @@ -353,15 +353,15 @@ internal sealed class WbMeshAdapter // auto-enqueues into _stagedMeshData (ObjectMeshManager line 510), // which Tick() drains onto the GPU. Until that completes, // TryGetRenderData(id) returns null and the dispatcher silently - // skips the entity — standard streaming flicker. + // skips the entity — standard streaming flicker. // // #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render - // data — NOT only on the first-ever registration. A first-only gate + // data — NOT only on the first-ever registration. A first-only gate // permanently lost any id whose initial read was cancelled before completing - // (landblock unload → CancelStagedUploads during login/teleport + // (landblock unload → CancelStagedUploads during login/teleport // churn) or whose upload was later LRU-evicted: every subsequent // registration skipped Prepare, so the mesh stayed invisible for the - // session with zero log output — the dispatcher's slow path just + // session with zero log output — the dispatcher's slow path just // counted meshMissing forever (issue #55's 1.45M/5s mountain was this // bug's heartbeat). User-visible: the AAB3 tower staircase rendering // partially or not at all depending on the session's landblock @@ -370,7 +370,7 @@ internal sealed class WbMeshAdapter // on existing render data, returns the in-flight task when already // pending, and dedups via _preparationTasks. // - // isSetup: false — acdream's MeshRefs already carry expanded + // isSetup: false — acdream's MeshRefs already carry expanded // per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is // unused. if (_meshManager.TryGetRenderData(id) is null) @@ -419,15 +419,15 @@ internal sealed class WbMeshAdapter /// /// #128 self-heal (2026-06-11): re-request a mesh load at the POINT OF - /// USE. Registration-time re-arming was insufficient — a preparation + /// USE. Registration-time re-arming was insufficient — a preparation /// cancelled by landblock churn AFTER the last registration event /// (running across blocks loads/unloads them repeatedly) left the mesh /// permanently unloadable with no later event to re-fire it. The draw /// dispatcher touches every missing-but-referenced mesh every frame (the - /// meshMissing slow path) — that is the one place a retry can never be + /// meshMissing slow path) — that is the one place a retry can never be /// missed. Cheap and idempotent: PrepareMeshDataAsync early-outs on /// existing render data and returns the in-flight task when pending. - /// Retail-equivalence: retail loads content synchronously — geometry is + /// Retail-equivalence: retail loads content synchronously — geometry is /// never permanently absent; this converges our async pipeline to the /// same guarantee. /// @@ -466,7 +466,7 @@ internal sealed class WbMeshAdapter _graphicsDevice!.ProcessGLQueue(); // #125: drain staged uploads; a FAILED upload (UploadMeshData returned // null from its catch) is re-staged for a LATER frame, not dropped. The - // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing + // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing // inside the while would let a deterministic failure spin the queue in a // single frame. UploadOrRequeue bounds the retries (MaxUploadRetries) so // a genuine defect surfaces loudly instead of the old silent sticky drop. @@ -594,14 +594,14 @@ internal sealed class WbMeshAdapter : default; // #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES - // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the + // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the // actual TexSubImage3D copies + mipmap regeneration happen in // ProcessDirtyUpdates, which WB drives ONCE PER FRAME from its render loop // (WB GameScene.cs:975 `_meshManager?.GenerateMipmaps()`, just before the // opaque pass). That call site lived in the GameScene file the N.4/O-T4 // extraction replaced with GameWindow, so the driver was silently dropped: // staged updates never reached TexSubImage3D, leaving undefined - // TexStorage3D content behind valid resident bindless handles — the + // TexStorage3D content behind valid resident bindless handles — the // intermittent white indoor walls (#105). Pre-fix evidence: 126 updates // stuck across 34/34 arrays at standstill (texflush-prefix.log). Tick() // runs before all draw passes (GameWindow OnRender), so this is the exact @@ -662,7 +662,7 @@ internal sealed class WbMeshAdapter meshManager.RejectUnsupportedStagedUpload(rejected, error); } - // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled. + // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled. private int _lastTexFlushBefore = -1; private int _texFlushHeartbeat; diff --git a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs index 753bffff..746050f3 100644 --- a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs +++ b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.Rendering.Wb; +namespace AcDream.App.Rendering.Wb; /// /// Phase A8 (2026-05-28): WB's RenderPass enum, extracted verbatim from @@ -9,7 +9,7 @@ /// Consumed by and matches the /// uRenderPass uniform in the modern mesh shaders. /// -internal enum WbRenderPass +public enum WbRenderPass { /// /// The opaque pass. Only non-transparent objects are rendered. diff --git a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs index cc5eeb81..6a6d31c2 100644 --- a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs +++ b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs @@ -58,7 +58,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics private readonly IWorldScenePViewDiagnosticSource _pview; private readonly IWorldSceneDebugStateSource _state; private readonly DebugLineRenderer? _lines; - private readonly ICurrentGpuFrameSource _currentFrame; private readonly PhysicsEngine _physics; private readonly ILocalPlayerModeSource _mode; private readonly IRuntimeLocalPlayerControllerSource _player; @@ -71,7 +70,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics IWorldScenePViewDiagnosticSource pview, IWorldSceneDebugStateSource state, DebugLineRenderer? lines, - ICurrentGpuFrameSource currentFrame, PhysicsEngine physics, ILocalPlayerModeSource mode, IRuntimeLocalPlayerControllerSource player, @@ -82,7 +80,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics _pview = pview ?? throw new ArgumentNullException(nameof(pview)); _state = state ?? throw new ArgumentNullException(nameof(state)); _lines = lines; - _currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame)); _physics = physics ?? throw new ArgumentNullException(nameof(physics)); _mode = mode ?? throw new ArgumentNullException(nameof(mode)); _player = player ?? throw new ArgumentNullException(nameof(player)); @@ -250,7 +247,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics LogNearbyCollisionObjects(position, drawn); } - _lines.Flush(camera.Camera.View, camera.Projection, _currentFrame.Current); + _lines.Flush(camera.Camera.View, camera.Projection); } private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn) diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs index 4beeda4b..8324c504 100644 --- a/src/AcDream.App/RuntimeOptions.cs +++ b/src/AcDream.App/RuntimeOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.IO; using AcDream.App.Rendering.Residency; @@ -15,19 +15,19 @@ namespace AcDream.App; /// /// /// -/// Scope: startup-time only — values that don't change +/// Scope: startup-time only — values that don't change /// once the window is up. Runtime diagnostic toggles /// (e.g. ACDREAM_DUMP_MOTION, ACDREAM_PROBE_*) belong in /// diagnostic owner classes (see AcDream.Core.Physics.PhysicsDiagnostics /// for the template), not here. /// /// -/// See docs/architecture/code-structure.md §2 Rule 4 for the -/// rule that drove this extraction, and §4 Step 1 for the broader +/// See docs/architecture/code-structure.md §2 Rule 4 for the +/// rule that drove this extraction, and §4 Step 1 for the broader /// extraction sequence this is the first cut of. /// /// -internal sealed record RuntimeOptions( +public sealed record RuntimeOptions( string DatDir, string PreparedAssetPath, bool LiveMode, diff --git a/src/AcDream.App/Spells/MagicRuntime.cs b/src/AcDream.App/Spells/MagicRuntime.cs index 14fd5163..100b8fd5 100644 --- a/src/AcDream.App/Spells/MagicRuntime.cs +++ b/src/AcDream.App/Spells/MagicRuntime.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Core.Spells; @@ -8,7 +8,7 @@ using AcDream.Content; namespace AcDream.App.Spells; /// One resolved formula cell in retail's spell examination subview. -internal readonly record struct SpellExamineComponent( +public readonly record struct SpellExamineComponent( uint SpellComponentId, SpellComponentDescriptor Descriptor, bool Owned); @@ -18,7 +18,7 @@ internal readonly record struct SpellExamineComponent( /// owner. The server owns turning, motion, fizzle, mana/component consumption, /// effects, and results. /// -internal sealed class MagicRuntime : IDisposable +public sealed class MagicRuntime : IDisposable { private readonly SpellComponentRequirementService _requirements; private IDisposable? _castOperationsBinding; diff --git a/src/AcDream.App/Streaming/DungeonStreamingGate.cs b/src/AcDream.App/Streaming/DungeonStreamingGate.cs index 048a0158..86aaeb5d 100644 --- a/src/AcDream.App/Streaming/DungeonStreamingGate.cs +++ b/src/AcDream.App/Streaming/DungeonStreamingGate.cs @@ -1,16 +1,16 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// Result of the per-frame dungeon streaming-gate decision. -/// Passed to — collapse +/// Passed to — collapse /// streaming to the single dungeon landblock. /// When non-null, override the streaming observer to this /// landblock key (the cell id's high 16 bits, 0xXXYY). Null leaves the caller's observer as-is. -internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey); +public readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey); /// /// AP-36: the dungeon streaming gate (#133 FPS). When the player stands in a SEALED /// EnvCell (an indoor cell that doesn't see outside), streaming collapses to the single -/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25 +/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25 /// window would pull in ~129 unrelated ocean-grid dungeons. The trigger is the player's /// CURRENT cell (CellGraph.CurrCell, set the moment the player is placed), and the /// observer is pinned to that cell's OWN landblock (the cell id high 16 bits) because a @@ -19,7 +19,7 @@ internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? Obse /// Extracted from GameWindow.OnUpdate as a pure function so the /// teleport-hold rule (below) is unit-testable without the GL/dat/network stack. /// -internal static class DungeonStreamingGate +public static class DungeonStreamingGate { /// /// Decide the streaming gate from the player's current cell. @@ -33,11 +33,11 @@ internal static class DungeonStreamingGate bool isTeleportHold, bool currCellIsSealedDungeon, uint currCellId) { // #145/#138: during a teleport hold the player is NOT yet placed, so CurrCell is - // the frozen SOURCE cell — where the player IS, not where they're going. Streaming + // the frozen SOURCE cell — where the player IS, not where they're going. Streaming // must follow the DESTINATION, which the PortalSpace observer pin already does, so // the source-cell gate is suppressed. Otherwise a teleport OUT of a dungeon keeps - // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor - // destination never hydrates → the TAS transit holds 600 frames → force-snap to + // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor + // destination never hydrates → the TAS transit holds 600 frames → force-snap to // ocean. A teleport INTO a dungeon is handled explicitly upstream by // StreamingController.PreCollapseToDungeon (and the controller's _collapsed latch // holds it through the hold), so suppressing the gate here doesn't regress it. diff --git a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs index 26c1f92c..0cfee190 100644 --- a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs +++ b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.Streaming; @@ -8,7 +8,7 @@ namespace AcDream.App.Streaming; /// asynchronously from the state transition, so a failed renderer cleanup /// cannot keep the old landblock logically resident. /// -internal sealed record GpuLandblockRetirement( +public sealed record GpuLandblockRetirement( uint LandblockId, LandblockRetirementKind Kind, IReadOnlyList Entities); @@ -24,7 +24,7 @@ internal sealed record GpuWorldRecenterRetirement( int SpatialOperationCount, Exception? ObserverFailure); -internal enum LandblockRetirementKind +public enum LandblockRetirementKind { Full, NearLayer, diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs index bcbc857d..5b2a32cd 100644 --- a/src/AcDream.App/Streaming/GpuWorldState.cs +++ b/src/AcDream.App/Streaming/GpuWorldState.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; @@ -56,7 +56,7 @@ internal sealed record GpuLandblockSpatialPublication( /// Threading: not thread-safe. All calls must happen on the render thread. /// /// -internal sealed class GpuWorldState : ILiveEntitySpatialQuery +public sealed class GpuWorldState : ILiveEntitySpatialQuery { private readonly LandblockSpawnAdapter? _wbSpawnAdapter; private readonly EntityScriptActivator? _entityScriptActivator; @@ -194,7 +194,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery && IsLiveEntityProjectionResident(key); /// - /// Try to grab the loaded record for a landblock — useful for callers + /// Try to grab the loaded record for a landblock — useful for callers /// that need to enumerate entities before the landblock is dropped /// (e.g. unregistering dynamic lights on a RemoveLandblock). /// @@ -1053,7 +1053,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery } /// - /// Mark a server-GUID as persistent — this entity survives landblock unloads + /// Mark a server-GUID as persistent — this entity survives landblock unloads /// and gets re-parked as pending for its current canonical landblock. /// public void MarkPersistent(uint serverGuid) @@ -1093,7 +1093,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery uint canonical = (newCanonicalLb & 0xFFFF0000u) | 0xFFFFu; - // Fast path: already drawn in the correct loaded bucket → nothing to do + // Fast path: already drawn in the correct loaded bucket → nothing to do // (avoids per-frame list churn for a settled, stationary entity). bool hasCurrent = _projectionLocations.TryGetValue( entity, @@ -1110,17 +1110,17 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery return; } - // Remove the entity from wherever it currently lives — a loaded bucket - // OR a pending bucket — then re-append to its current landblock. + // Remove the entity from wherever it currently lives — a loaded bucket + // OR a pending bucket — then re-append to its current landblock. // // The projection-location index is the 2026-07-03 fix for the cold-spawn / // run-out "invisible player" bug: a persistent (server-spawned) entity // that spawned into a not-yet-loaded landblock sits in _pendingByLandblock, - // and the old code scanned ONLY _loaded — so it silently no-op'd and left + // and the old code scanned ONLY _loaded — so it silently no-op'd and left // the player stranded, hidden, even after its landblock finished loading // (the AddLandblock pending-drain had already run empty before the churn // re-parked the player, and the player is excluded from the server-object - // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it, + // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it, // and it couldn't reach a pending entity). The index now reaches either // residency class in O(1). Re-appending routes the entity // to _loaded (drawn) when its landblock is loaded, or back to pending to @@ -1274,8 +1274,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery // landblock often hasn't streamed in yet, so the player lands in // _pendingByLandblock. If that same landblock is then unloaded (a // streaming churn / re-teleport before it finishes loading), the - // pending entry was silently dropped here — violating the - // "persistent ⇒ survives unload" contract and making the avatar + // pending entry was silently dropped here — violating the + // "persistent ⇒ survives unload" contract and making the avatar // vanish after a couple round-trips. Rescue them so DrainRescued // re-parks them at the next valid landblock. if (_pendingByLandblock.TryGetValue(canonical, out var pendingForLb)) @@ -1580,7 +1580,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// world withdrawal, or logical teardown. It owns no renderer/script /// lifecycle and is safe for a still-live incarnation. /// - /// Safe to call with a server guid that's not currently present — no-op. + /// Safe to call with a server guid that's not currently present — no-op. /// public void RemoveLiveEntityProjection(uint serverGuid) { @@ -1707,7 +1707,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery if (_loaded.ContainsKey(canonicalLandblockId)) { - // Hot path — append directly to the render-thread-owned mutable + // Hot path — append directly to the render-thread-owned mutable // resident bucket and flat view. AddLoadedProjection(canonicalLandblockId, entity, key); if (probePersistent) @@ -1715,7 +1715,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery return; } - // Cold path — landblock not yet loaded. Park the entity in the + // Cold path — landblock not yet loaded. Park the entity in the // pending bucket; AddLandblock will pick it up when the streamer // delivers the matching landblock. if (!_pendingByLandblock.TryGetValue(canonicalLandblockId, out var bucket)) @@ -1737,8 +1737,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// /// Drop all entities from a landblock without removing the terrain. Used - /// by two-tier streaming when a landblock crosses Near→Far hysteresis. - /// Per Phase A.5 spec §4.4. + /// by two-tier streaming when a landblock crosses Near→Far hysteresis. + /// Per Phase A.5 spec §4.4. /// /// /// Only dat-static entity layers demote. Live server projections retain @@ -1798,7 +1798,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery // for this landblock BEFORE we drop the entity list. The cache stores // canonical landblock ids (the dispatcher's _walkScratch carries // entry.LandblockId from GpuWorldState.LandblockEntries, whose keys are - // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b. + // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b. // C.1.5b: stop DefaultScript for each dat-hydrated entity about to // be dropped. Demote-tier entities are always atlas-tier (ServerGuid==0 @@ -1852,14 +1852,14 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery /// /// Merge entities into an existing-loaded landblock. Used by two-tier - /// streaming for the Far→Near promotion case (terrain already loaded; + /// streaming for the Far→Near promotion case (terrain already loaded; /// entity layer streaming in). Falls back to the pending bucket if the /// landblock isn't loaded yet (handles the rare "promote arrives before /// far load completes" race). - /// Per Phase A.5 spec §4.4. + /// Per Phase A.5 spec §4.4. /// /// - /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) — + /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) — /// callers may pass cell-resolved ids and they will key correctly. /// /// @@ -1913,7 +1913,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery if (!parkIfMissing) return null; - // Park as pending — same pattern as live projections for not-yet-loaded LBs. + // Park as pending — same pattern as live projections for not-yet-loaded LBs. if (!_pendingByLandblock.TryGetValue(canonical, out var bucket)) { bucket = new List(); @@ -2023,7 +2023,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery private readonly HashSet _persistentInFlatProbe = new(); // TEMP (#138-B): log when a persistent (player) entity enters/leaves the - // drawn flat view. Transition-gated → low volume (fires at teleport + // drawn flat view. Transition-gated → low volume (fires at teleport // boundaries, not every rebuild). Strip with EntityVanishProbe. private void ProbeFlatViewTransitions() { diff --git a/src/AcDream.App/Streaming/LandblockBuild.cs b/src/AcDream.App/Streaming/LandblockBuild.cs index 7f66c554..4a04929d 100644 --- a/src/AcDream.App/Streaming/LandblockBuild.cs +++ b/src/AcDream.App/Streaming/LandblockBuild.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.Core.World; namespace AcDream.App.Streaming; @@ -8,7 +8,7 @@ namespace AcDream.App.Streaming; /// until it posts the corresponding completion; the render thread then applies /// the landblock and its cell transaction together. /// -internal sealed record LandblockBuild( +public sealed record LandblockBuild( LoadedLandblock Landblock, EnvCellLandblockBuild? EnvCells = null, LandblockBuildOrigin Origin = default, diff --git a/src/AcDream.App/Streaming/LandblockBuildFactory.cs b/src/AcDream.App/Streaming/LandblockBuildFactory.cs index 25a13fc1..5e1e04cc 100644 --- a/src/AcDream.App/Streaming/LandblockBuildFactory.cs +++ b/src/AcDream.App/Streaming/LandblockBuildFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.Streaming; @@ -14,7 +14,7 @@ namespace AcDream.App.Streaming; /// CObjCell::init_objects (0x0052B420). /// See docs/architecture/worldbuilder-inventory.md. /// -internal sealed class LandblockBuildFactory +public sealed class LandblockBuildFactory { private readonly IDatReaderWriter _dats; private readonly IPreparedCollisionSource _preparedCollisions; @@ -51,7 +51,7 @@ internal sealed class LandblockBuildFactory /// /// ISSUE #54 (post-A.5): far-tier loads (kind == LoadFar) skip /// LandBlockInfo + scenery + interior hydration. They return only the - /// LandBlock heightmap dat record + an empty entity list — enough for + /// LandBlock heightmap dat record + an empty entity list — enough for /// terrain-mesh build on the next phase. Near-tier loads run the full /// path. This replaces Bug A's post-load entity strip in /// with an @@ -70,7 +70,7 @@ internal sealed class LandblockBuildFactory // gate through mesh/bounds hydration prevents another consumer from // interleaving reader cursor/cache state with this build. // tp-probe (2026-06-22, REMOVABLE): measure lock-WAIT (the _datLock - // contention signal — large only when the render thread is hammering the + // contention signal — large only when the render thread is hammering the // lock during a CreateObject flood) AND lock-HOLD (the intrinsic build // cost). Identical work in both branches; the probe branch only adds the // stopwatch + log. No behavior change when ProbeTeleportEnabled is false. @@ -123,7 +123,7 @@ internal sealed class LandblockBuildFactory { uint landblockId = request.LandblockId; - // ISSUE #54: far-tier early-out — heightmap only, empty entities. + // ISSUE #54: far-tier early-out — heightmap only, empty entities. // Skips the LandBlockInfo dat read AND all entity hydration (stabs // + buildings) AND the SceneryGenerator AND interior cells. Cuts // worker-thread cost per far-tier LB from ~tens of ms to a single @@ -203,7 +203,7 @@ internal sealed class LandblockBuildFactory } /// /// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single - /// landblock on the worker thread. Pure CPU — no GL calls. + /// landblock on the worker thread. Pure CPU — no GL calls. /// /// Ported from the pre-streaming preload loop in GameWindow.OnLoad /// (pre-Task-7 version, lines 329-405). Adapted to operate on a single @@ -305,7 +305,7 @@ internal sealed class LandblockBuildFactory float localX = spawn.LocalPosition.X; float localY = spawn.LocalPosition.Y; // Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ) - // — it uses the same AC2D render split-direction formula the + // — it uses the same AC2D render split-direction formula the // TerrainModernRenderer uses for the visible terrain mesh. This // guarantees trees are placed on the SAME Z height the player // walks on. If physics hasn't registered this landblock yet, @@ -313,14 +313,14 @@ internal sealed class LandblockBuildFactory var worldPx = localX + lbOffset.X; var worldPy = localY + lbOffset.Y; // FIX (trees-in-sky, 2026-06-22): scenery ground-Z comes from THIS - // landblock's OWN heightmap — the same triangle-aware Z the player walks on + // landblock's OWN heightmap — the same triangle-aware Z the player walks on // (TerrainSurface.SampleZFromHeightmap, lock-step with physics per #48), // scoped to the landblock being built. The former global // _physicsEngine.SampleTerrainZ(worldPx) query was structurally racy: at // build time this landblock is NOT registered in physics yet, so that query - // could only return null (→ this same own-heightmap) or a STALE neighbor's - // height — the previous location's terrain before the full old-window - // recenter retirement converges — planting scenery at the old location's + // could only return null (→ this same own-heightmap) or a STALE neighbor's + // height — the previous location's terrain before the full old-window + // recenter retirement converges — planting scenery at the old location's // altitude (trees-in-sky, deltaZ up to +500m; confirmed via the // [scenery-z-stale] probe 2026-06-22). Own-heightmap is correct in every // case, so the global query is removed (also drops its per-spawn cost). @@ -335,7 +335,7 @@ internal sealed class LandblockBuildFactory if (_dumpSceneryZ) { // groundZ now always comes from THIS landblock's own heightmap (the - // global physics query was removed — see the trees-in-sky fix above). + // global physics query was removed — see the trees-in-sky fix above). string source = "heightmap"; foreach (var mr in meshRefs) { @@ -422,7 +422,7 @@ internal sealed class LandblockBuildFactory /// /// Phase A.1 Task 8: walk a landblock's EnvCells and produce (a) the cell /// room-mesh entity (Phase 7.1) for each EnvCell with an EnvironmentId, and - /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls. + /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls. /// /// Portal cells and drawable shell placements are accumulated in the /// transaction-local . The render thread @@ -448,28 +448,28 @@ internal sealed class LandblockBuildFactory (lbY - origin.CenterY) * 192f, 0f); - // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator + // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator // for the full bit layout + history. Distinct from scenery (0x80000000+) and // landblock stabs (0xC0000000+, ids from LandblockLoader). // // #119 ROOT-CAUSE FIX (2026-06-11): this used to be // `0x40000000 | (landblockId & 0x00FFFF00)`, which for landblock keys 0xXXYYFFFF - // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a + // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a // map Y-row produced the same id base, so interior statics collided across // landblocks (Holtburg town A9B3's 9th stab == the AAB3 tower's 43-part spiral // staircase, both 0x40B3FF09). The Tier-1 classification cache then served one // entity's batches to the other (the cache hint at bucket-draw time was the - // player's landblock, identical for both) — the session-sticky "broken stairs + + // player's landblock, identical for both) — the session-sticky "broken stairs + // water barrel". // - // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow + // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow // past 0xFF still bleeds into the lbY byte." That residual manifested for real: // the Town Network hub (205 cells, one landblock) reached 277 interior entities // after the #79/#93 A7.L1 light-carrier hydration fix, aliasing into the NEXT // landblock's Y-byte (entity script/particle tracking is keyed on entity.Id - // directly — EntityScriptActivator — with no landblock-hint disambiguation, so + // directly — EntityScriptActivator — with no landblock-hint disambiguation, so // the fountain's water-spray script silently stopped firing). Widened the - // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for + // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for // why this is safe (nothing decodes X/Y back out of an entity id). uint interiorLbX = (landblockId >> 24) & 0xFFu; uint interiorLbY = (landblockId >> 16) & 0xFFu; @@ -484,7 +484,7 @@ internal sealed class LandblockBuildFactory { // TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix): // every id in [0x0100, 0x0100+NumCells) is derived from LandBlockInfo and - // MUST exist in the cell dat — a null here is always a read anomaly. + // MUST exist in the cell dat — a null here is always a read anomaly. Console.WriteLine($"[cell-miss] EnvCell 0x{envCellId:X8} null during interior hydration (NumCells={lbInfo.NumCells})"); continue; } @@ -498,7 +498,7 @@ internal sealed class LandblockBuildFactory { // TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix): // a null Environment means this cell's WALLS are silently never - // registered while its static objects still draw — the exact + // registered while its static objects still draw — the exact // white-walls geometry signature. Console.WriteLine($"[cell-miss] Environment 0x{0x0D000000u | envCell.EnvironmentId:X8} null for EnvCell 0x{envCellId:X8} -> walls not registered"); } @@ -510,14 +510,14 @@ internal sealed class LandblockBuildFactory // drawable-geometry predicate; the actual shell placement is now owned // by this streaming job's EnvCellLandblockBuild transaction. // Static objects inside the cell continue to flow through the dispatcher - // as WorldEntity records below — they have real GfxObj MeshRefs that work + // as WorldEntity records below — they have real GfxObj MeshRefs that work // fine; EnvCellRenderer receives only the completed shell transaction. - // Transforms — needed by the portal-visibility cell (unlifted) AND the + // Transforms — needed by the portal-visibility cell (unlifted) AND the // render/physics path. Computed for EVERY cell with a valid cellStruct, // not just drawable ones. Keep the small render lift out of physics; retail // BSP contact planes use the EnvCell origin verbatim. The lift constant is // shared with every draw-space consumer of portal polygons (OutsideView - // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130). + // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130). var physicsCellOrigin = envCell.Position.Origin + lbOffset; var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); @@ -532,9 +532,9 @@ internal sealed class LandblockBuildFactory // of whether CellMesh.Build produced drawable sub-meshes. A portals-only // pass-through connector (a ramp / stair / cellar mouth) yields 0 render // sub-meshes but MUST be in the visibility graph so the flood can traverse it - // to the cells beyond — otherwise the flood lookup-misses the unregistered + // to the cells beyond — otherwise the flood lookup-misses the unregistered // neighbour and the grey clear shows through the opening (#133: ramp - // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier + // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier // at the ramp; confirmed via [cellreg] registered=204/205 + [pv-trace] // skip=lookup-miss). Retail keeps the whole landblock cell array resident // before the flood runs; the cell-build transaction reads portals, NOT @@ -558,7 +558,7 @@ internal sealed class LandblockBuildFactory foreach (var stab in envCell.StaticObjects) { // #119 decisive probe: HYDRATE-side dump for ACDREAM_DUMP_ENTITY- - // targeted stabs. This is the MOMENT MeshRefs are constructed — + // targeted stabs. This is the MOMENT MeshRefs are constructed — // a degraded dat read here (setup null / placement frames short / // part GfxObj null) permanently corrupts the entity (H-A), and // nothing downstream ever rebuilds it. Inert when the set is empty. @@ -568,7 +568,7 @@ internal sealed class LandblockBuildFactory // #136: skip an EDITOR-ONLY placement marker. Such a dat object degrades to // nothing (GfxObj id 0) at any runtime distance, so retail's distance-based - // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the + // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the // WorldBuilder editor shows it at the origin. acdream's render path came from // WB (no distance LOD), so without this skip it draws the marker forever (the // red/green dungeon "cone"). Bare-GfxObj stabs are checked here; Setup stabs @@ -581,7 +581,7 @@ internal sealed class LandblockBuildFactory var interiorBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator(); // #79/#93 (2026-07-09): a Setup-sourced stab whose sole visual part is a // runtime-hidden marker (#136) flattens to zero mesh refs even though its - // Setup carries real Lights — a "light attach point" fixture (e.g. the Town + // Setup carries real Lights — a "light attach point" fixture (e.g. the Town // Network fountain room's ceiling light, Setup 0x02000365). Track the dat // Setup's Lights.Count here so the meshRefs==0 gate below doesn't also drop // the entity that otherwise carries those lights to the static @@ -618,7 +618,7 @@ internal sealed class LandblockBuildFactory { // #136: skip an editor-only marker PART (retail hides it at runtime // distance). The #136 dungeon "cone" is Setup 0x02000C39 whose sole - // part GfxObj 0x010028CA is such a marker — skipping it empties + // part GfxObj 0x010028CA is such a marker — skipping it empties // meshRefs and the whole stab drops below. if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(_dats, mr.GfxObjId)) continue; @@ -652,7 +652,7 @@ internal sealed class LandblockBuildFactory // Stabs inside EnvCells are already in landblock-local coordinates // (same space as LandBlockInfo.Objects stabs). Adding cellOrigin would - // be wrong — see Phase 2d comment in the pre-streaming preload. + // be wrong — see Phase 2d comment in the pre-streaming preload. var worldPos = stab.Frame.Origin + lbOffset; var worldRot = stab.Frame.Orientation; diff --git a/src/AcDream.App/Streaming/LandblockBuildRequest.cs b/src/AcDream.App/Streaming/LandblockBuildRequest.cs index 16d14e69..315753f4 100644 --- a/src/AcDream.App/Streaming/LandblockBuildRequest.cs +++ b/src/AcDream.App/Streaming/LandblockBuildRequest.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Immutable world-origin center captured by the update thread when a /// landblock load is admitted. Worker construction and render publication use /// this same value, so a later recenter cannot combine two coordinate frames. /// -internal readonly record struct LandblockBuildOrigin +public readonly record struct LandblockBuildOrigin { public LandblockBuildOrigin(int centerX, int centerY) { @@ -29,7 +29,7 @@ internal readonly record struct LandblockBuildOrigin /// is carried for matching and diagnostics only; /// residency policy remains owned by . /// -internal readonly record struct LandblockBuildRequest( +public readonly record struct LandblockBuildRequest( uint LandblockId, LandblockStreamJobKind Kind, ulong Generation, diff --git a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs index c4163f52..169d912c 100644 --- a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// publication. The render publisher commits buildings and EnvCells between /// these stages without recomputing the captured origin. /// -internal sealed class LandblockPhysicsPublication +public sealed class LandblockPhysicsPublication { internal LandblockPhysicsPublication( object owner, @@ -72,7 +72,7 @@ internal sealed class LandblockPhysicsPublication /// Cumulative update-thread diagnostics for physics publication and removal. /// Durations are ticks. /// -internal readonly record struct LandblockPhysicsPublisherDiagnostics( +public readonly record struct LandblockPhysicsPublisherDiagnostics( long BeginCount, long CompleteCount, long BasePublishTicks, @@ -103,7 +103,7 @@ internal readonly record struct LandblockPhysicsPublisherDiagnostics( /// use one multipart shadow owner or the mutually exclusive Setup /// cylinder/sphere fallback. /// -internal sealed class LandblockPhysicsPublisher +public sealed class LandblockPhysicsPublisher { private readonly object _receiptOwner = new(); private readonly RuntimePhysicsState _physics; @@ -953,7 +953,7 @@ internal sealed class LandblockPhysicsPublisher string sampleText = string.Join(",", samples.Select( value => $"0x{value:X8}")); Console.WriteLine( - $" → {missingCount} scenery entities had no visual bounds cached. " + + $" → {missingCount} scenery entities had no visual bounds cached. " + $"Samples: {sampleText}"); } } diff --git a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs index cef987b0..f5ae4f9d 100644 --- a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs +++ b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering.Wb; +using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Scene; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Streaming; /// Keeping this projection on the pipeline prevents the composition root from /// retaining each concrete publication owner independently. /// -internal readonly record struct LandblockPresentationDiagnostics( +public readonly record struct LandblockPresentationDiagnostics( LandblockRenderPublisherDiagnostics Render, LandblockPhysicsPublisherDiagnostics Physics, LandblockStaticPresentationDiagnostics Statics); @@ -37,7 +37,7 @@ internal readonly record struct LandblockPublicationAdvance( /// graph; the internal delegate constructor exists only for hermetic policy and /// retry characterization tests. /// -internal sealed class LandblockPresentationPipeline +public sealed class LandblockPresentationPipeline { private enum PublicationKind : byte { diff --git a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs index 7536fb1f..f9e0e937 100644 --- a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs +++ b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Lighting; +using AcDream.Core.Lighting; using AcDream.Core.Rendering; using AcDream.Core.World; @@ -16,7 +16,7 @@ namespace AcDream.App.Streaming; /// detaches spatial reachability first, then advances these concrete owners /// from the retained ticket without replaying successful stages or entities. /// -internal sealed class LandblockPresentationRetirementOwner +public sealed class LandblockPresentationRetirementOwner { private readonly LandblockRenderPublisher _render; private readonly LandblockPhysicsPublisher _physics; diff --git a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs index aa54bb89..5578bc1a 100644 --- a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Numerics; using AcDream.App.Rendering; @@ -13,7 +13,7 @@ namespace AcDream.App.Streaming; /// origin between and /// . /// -internal sealed class LandblockRenderPublication +public sealed class LandblockRenderPublication { private readonly IReadOnlyDictionary _visibilityCells; @@ -64,7 +64,7 @@ internal sealed class LandblockRenderPublication /// Times use ticks so callers can aggregate without /// losing sub-millisecond precision. /// -internal readonly record struct LandblockRenderPublisherDiagnostics( +public readonly record struct LandblockRenderPublisherDiagnostics( long BeginCount, long CompleteCount, long TerrainPublishTicks, @@ -90,7 +90,7 @@ internal readonly record struct LandblockRenderPublisherDiagnostics( /// WorldBuilder one-job/one-EnvCell-transaction seam documented in /// docs/architecture/worldbuilder-inventory.md. /// -internal sealed class LandblockRenderPublisher +public sealed class LandblockRenderPublisher { private readonly object _receiptOwner = new(); private readonly Action _publishTerrain; diff --git a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs index 460f63cd..82046fa1 100644 --- a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs +++ b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs @@ -1,9 +1,9 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.Streaming; [Flags] -internal enum LandblockRetirementStage : ushort +public enum LandblockRetirementStage : ushort { None = 0, MeshReferences = 1 << 0, @@ -31,7 +31,7 @@ internal enum LandblockRetirementOperationResult : byte /// One retryable landblock-retirement ledger. Successful owner operations are /// committed once; a later attempt resumes at the exact failed owner/entity. /// -internal sealed class LandblockRetirementTicket +public sealed class LandblockRetirementTicket { private readonly Dictionary _entityCursors = new(); private readonly Dictionary _failures = new(); @@ -240,7 +240,7 @@ internal sealed class LandblockRetirementTicket /// owner callback cannot recursively replay its active stage or mutate the /// pending-ticket map while it is being enumerated. /// -internal sealed class LandblockRetirementCoordinator +public sealed class LandblockRetirementCoordinator { private enum BudgetedAdvanceResult : byte { diff --git a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs index 8ba7c031..ac9e0121 100644 --- a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs +++ b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Lighting; +using AcDream.Core.Lighting; using AcDream.Core.Plugins; using AcDream.Core.Rendering; using AcDream.Core.World; @@ -11,7 +11,7 @@ namespace AcDream.App.Streaming; /// publication. The physics publisher calls the light/translucency stage at /// the existing per-object point immediately before ordinary collision. /// -internal sealed class LandblockStaticPresentationPublication +public sealed class LandblockStaticPresentationPublication { private readonly Dictionary _entities; private readonly Dictionary _snapshots; @@ -63,7 +63,7 @@ internal sealed class LandblockStaticPresentationPublication public IReadOnlyDictionary Snapshots => _snapshots; } -internal readonly record struct LandblockStaticPresentationDiagnostics( +public readonly record struct LandblockStaticPresentationDiagnostics( long BeginCount, long CompleteCount, long LightReplacementCount, @@ -87,7 +87,7 @@ internal readonly record struct LandblockStaticPresentationDiagnostics( /// logical spawn per retained static ID, while reapply only refreshes current /// state. /// -internal sealed class LandblockStaticPresentationPublisher +public sealed class LandblockStaticPresentationPublisher { private readonly object _receiptOwner = new(); private readonly LightingHookSink _lighting; diff --git a/src/AcDream.App/Streaming/LandblockStreamJob.cs b/src/AcDream.App/Streaming/LandblockStreamJob.cs index d69bd421..17017cd2 100644 --- a/src/AcDream.App/Streaming/LandblockStreamJob.cs +++ b/src/AcDream.App/Streaming/LandblockStreamJob.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// public abstract record LandblockStreamJob(uint LandblockId) { - internal sealed record Load( + public sealed record Load( uint LandblockId, LandblockStreamJobKind Kind, ulong Generation = 0, @@ -21,7 +21,7 @@ public abstract record LandblockStreamJob(uint LandblockId) public LandblockBuildRequest Request => new(LandblockId, Kind, Generation, Origin); } - internal sealed record Unload( + public sealed record Unload( uint LandblockId, ulong Generation = 0) : LandblockStreamJob(LandblockId); @@ -30,10 +30,10 @@ public abstract record LandblockStreamJob(uint LandblockId) /// priority queues, keeping Unloads. Posted by /// when the player enters a /// dungeon and the in-flight outdoor/neighbor window load must be cancelled - /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by + /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by /// convention; readers pattern-match on the type. /// - internal sealed record ClearLoads() : LandblockStreamJob(0); + public sealed record ClearLoads() : LandblockStreamJob(0); } /// @@ -49,7 +49,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) /// (terrain only) from Near (terrain + entities). /// is built off the render thread on the streaming worker. /// - internal sealed record Loaded( + public sealed record Loaded( uint LandblockId, LandblockStreamTier Tier, LandblockBuild Build, @@ -78,7 +78,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) /// GpuWorldState still merges only the entity layer so live entities already /// attached to the landblock are preserved. /// - internal sealed record Promoted( + public sealed record Promoted( uint LandblockId, LandblockBuild Build, LandblockMeshData MeshData, @@ -98,20 +98,20 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation) public IReadOnlyList Entities => Landblock.Entities; } - internal sealed record Failed( + public sealed record Failed( uint LandblockId, string Error, ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation); - internal sealed record Unloaded( + public sealed record Unloaded( uint LandblockId, ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation); /// /// The worker loop itself crashed with an unhandled exception. Not tied - /// to a specific landblock — distinguished from + /// to a specific landblock — distinguished from /// because consumers typically route this to a fatal-log path rather /// than retrying a single landblock later. LandblockId is 0 by /// convention; readers should pattern-match on the type, not the id. /// - internal sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0); + public sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0); } diff --git a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs index 4d6b9dc0..b0f01426 100644 --- a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs +++ b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; using AcDream.Core.World; @@ -12,7 +12,7 @@ namespace AcDream.App.Streaming; /// the managed heap. Exact array payloads and logical retained entries are /// charged consistently so admission has a deterministic unit. /// -internal readonly record struct LandblockStreamCostEstimate( +public readonly record struct LandblockStreamCostEstimate( StreamingWorkCost Work, long TerrainPayloadBytes, int Entities, @@ -26,7 +26,7 @@ internal readonly record struct LandblockStreamCostEstimate( int PhysicsSetups, int PhysicsGfxObjects); -internal static class LandblockStreamResultCost +public static class LandblockStreamResultCost { private const int ReferenceChargeBytes = 8; private const int DictionaryEntryChargeBytes = 16; diff --git a/src/AcDream.App/Streaming/LandblockStreamTier.cs b/src/AcDream.App/Streaming/LandblockStreamTier.cs index 5daf4393..c4a9e5d7 100644 --- a/src/AcDream.App/Streaming/LandblockStreamTier.cs +++ b/src/AcDream.App/Streaming/LandblockStreamTier.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Streaming-tier classification for a landblock. means /// terrain mesh only; means terrain + scenery + EnvCells + -/// entity registration with the WB dispatcher. Per Phase A.5 spec §3. +/// entity registration with the WB dispatcher. Per Phase A.5 spec §3. /// -internal enum LandblockStreamTier +public enum LandblockStreamTier { Far, Near, @@ -15,7 +15,7 @@ internal enum LandblockStreamTier /// What work the streaming worker should perform for a given job. Distinct /// from because /// reads only the entity layer (terrain mesh already loaded), while -/// reads everything from scratch. Per Phase A.5 spec §4.3. +/// reads everything from scratch. Per Phase A.5 spec §4.3. /// public enum LandblockStreamJobKind { @@ -23,6 +23,6 @@ public enum LandblockStreamJobKind LoadFar, /// Read LandBlock + LandBlockInfo, generate scenery, build mesh, full entity layer. LoadNear, - /// Read LandBlockInfo + scenery only — terrain already loaded for this LB. + /// Read LandBlockInfo + scenery only — terrain already loaded for this LB. PromoteToNear, } diff --git a/src/AcDream.App/Streaming/LandblockStreamer.cs b/src/AcDream.App/Streaming/LandblockStreamer.cs index ed6fa5f8..234b90e1 100644 --- a/src/AcDream.App/Streaming/LandblockStreamer.cs +++ b/src/AcDream.App/Streaming/LandblockStreamer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; @@ -28,12 +28,12 @@ namespace AcDream.App.Streaming; /// GameWindow's _datLock (Phase A.5 T10) serialises all /// DatCollection.Get<T> calls. Both factory closures passed at /// construction acquire that lock before reading dats. The worker never -/// touches DatCollection directly — it only calls the factories. +/// touches DatCollection directly — it only calls the factories. /// /// /// /// Unloads pass through the outbox as -/// records so the render thread can release GPU state on the next drain — +/// records so the render thread can release GPU state on the next drain — /// the streamer never touches GPU resources directly. /// /// @@ -43,7 +43,7 @@ namespace AcDream.App.Streaming; /// methods are thread-safe. /// /// -internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource +public sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource { /// /// Default drain batch size. Tuned to cap GPU upload work the render @@ -78,7 +78,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc { _loadLandblock = loadLandblock; _supportsRequestOrigin = supportsRequestOrigin; - // Default: no mesh build (returns null → Failed result). Production + // Default: no mesh build (returns null → Failed result). Production // wires in LandblockMesh.Build via the T12 construction site. _buildMeshOrNull = buildMeshOrNull ?? ((_, _) => null); _inbox = Channel.CreateUnbounded( @@ -131,7 +131,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc } /// - /// Back-compat overload — wraps a kind-agnostic factory so existing test code + /// Back-compat overload — wraps a kind-agnostic factory so existing test code /// that doesn't care about the JobKind branch keeps compiling. The wrapper /// ignores the kind and calls the factory once per LB regardless of tier. /// New production code should use . @@ -239,7 +239,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc /// control job which the worker /// honours at read time, dropping all pending Loads from both priority /// queues (Unloads survive). Used on the dungeon-entry edge to abort the - /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never + /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never /// finish loading (#133 FPS). Loads the worker has ALREADY dequeued still /// complete; the StreamingController's collapsed-sweep unloads those few. /// diff --git a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs index eb440650..147767aa 100644 --- a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs +++ b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace AcDream.App.Streaming; @@ -7,7 +7,7 @@ namespace AcDream.App.Streaming; /// channel. Peek plus read lets the update thread price a payload before /// adopting it into the scheduler. /// -internal interface ILandblockCompletionSource +public interface ILandblockCompletionSource { int BacklogCount { get; } bool TryPeek(out LandblockStreamResult? result); diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 01ae537d..08f20cc8 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using AcDream.App.Rendering.Wb; @@ -17,7 +17,7 @@ namespace AcDream.App.Streaming; /// Threading: not thread-safe. All calls must happen on the render thread. /// /// -internal sealed class StreamingController +public sealed class StreamingController : IStreamingFrameBackend, IWorldRevealStreamingScheduler { @@ -88,14 +88,14 @@ internal sealed class StreamingController // True while streaming is collapsed to the single dungeon landblock the // player stands in (the dungeon gate, #133 FPS). AC dungeons have NO - // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that - // are never visible, so we stop loading the 25×25 window entirely. + // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that + // are never visible, so we stop loading the 25×25 window entirely. private bool _collapsed; // The dungeon landblock id we collapsed onto. Once collapsed we key the // gate on this STABLE landblock, not the per-frame insideDungeon signal: // CurrCell can momentarily resolve to null/outdoor mid-frame, and gating - // expand on that flicker thrashes collapse↔expand (reload storms + a light + // expand on that flicker thrashes collapse↔expand (reload storms + a light // leak). We only expand when the observer actually moves to a different // landblock (teleport/portal out). private uint _collapsedCenter; @@ -610,16 +610,16 @@ internal sealed class StreamingController /// /// Advance one frame. / - /// are landblock coordinates (0..255) of the current viewer — the camera + /// are landblock coordinates (0..255) of the current viewer — the camera /// in offline mode, the server-sent player position in live. /// /// Two-tier model (Phase A.5 T13): /// - /// → enqueue LoadFar (terrain only, no entities) - /// → enqueue LoadNear (terrain + entities) - /// → enqueue PromoteToNear (entity layer for already-loaded terrain) - /// → drop entities on render thread immediately (terrain stays) - /// → enqueue full unload + /// → enqueue LoadFar (terrain only, no entities) + /// → enqueue LoadNear (terrain + entities) + /// → enqueue PromoteToNear (entity layer for already-loaded terrain) + /// → drop entities on render thread immediately (terrain stays) + /// → enqueue full unload /// /// public void Tick(int observerCx, int observerCy, bool insideDungeon = false) @@ -699,15 +699,15 @@ internal sealed class StreamingController if (_collapsed) { // Hysteresis. Cases: - // - Still in the SAME dungeon landblock → hold (sweep stragglers). + // - Still in the SAME dungeon landblock → hold (sweep stragglers). // - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon) - // → re-collapse onto it. + // → re-collapse onto it. // - CurrCell flickered null but the player hasn't gone anywhere: the // observer landblock reverts to the position-derived value, which for a // dungeon is only ever the ADJACENT off-by-one landblock (negative cell- - // local Y). Hold — never expand on an adjacent flicker. + // local Y). Hold — never expand on an adjacent flicker. // - Genuinely left to a DISTANT landblock (portal/teleport out, always far - // from the ocean-grid dungeon block) → expand. + // from the ocean-grid dungeon block) → expand. if (insideDungeon && centerId != _collapsedCenter) EnterDungeonCollapse(observerCx, observerCy, centerId); else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1) @@ -910,23 +910,23 @@ internal sealed class StreamingController /// /// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first - /// has a chance to bootstrap the full 25×25 window. Called + /// has a chance to bootstrap the full 25×25 window. Called /// from the login / teleport spawn path the instant the streaming center is /// recentered onto a SEALED dungeon landblock. /// /// The per-frame insideDungeon gate keys on the physics - /// CurrCell, which is only set once the player is PLACED — and placement + /// CurrCell, which is only set once the player is PLACED — and placement /// waits for the dungeon landblock to hydrate. So for the whole hydration window /// (tens of seconds for a ~200-cell dungeon) the gate reads false and /// would enqueue the ~24 unrelated ocean-grid neighbor /// dungeons (+ ~19k entities each); the collapse then only mops them up after - /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. + /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. /// /// Pre-collapsing means the EXPENSIVE dungeon-neighbour window is never /// enqueued. On teleport nothing is enqueued at all (this fires before the next /// Tick recenters). On login a brief Holtburg outdoor window may be enqueued by the /// frame-1 NormalTick (before the player's spawn arrives) and is immediately - /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the + /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the /// ocean-grid dungeons, and a handful of already-dequeued loads get swept next /// frame. Idempotent: a no-op when already collapsed onto this same landblock, so a /// re-sent spawn or a same-frame double call costs nothing. Render-thread only, @@ -954,7 +954,7 @@ internal sealed class StreamingController } /// - /// Outdoor / building-interior streaming — the original two-tier model. + /// Outdoor / building-interior streaming — the original two-tier model. /// private void NormalTick(int observerCx, int observerCy) { @@ -1012,11 +1012,11 @@ internal sealed class StreamingController /// /// Dungeon-entry edge: cancel the in-flight window load, unload every /// resident neighbor, and pin streaming to the player's single dungeon - /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks + /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks /// (ACE LandblockManager.GetAdjacentIDs returns empty for a dungeon); - /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and + /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and /// their thousands of emitters (#133 FPS). Unloading them also tears down - /// their lights, shrinking the static-light set toward retail's ≤40. + /// their lights, shrinking the static-light set toward retail's ≤40. /// private void EnterDungeonCollapse(int cx, int cy, uint centerId) { @@ -1049,7 +1049,7 @@ internal sealed class StreamingController /// /// While collapsed, unload any landblock that finished loading after the - /// collapse edge — a Load the worker had already dequeued before the + /// collapse edge — a Load the worker had already dequeued before the /// control job took /// effect. At steady state only the dungeon landblock is resident, so this /// is a no-op. @@ -1057,7 +1057,7 @@ internal sealed class StreamingController private void SweepCollapsed() { // Always preserve the true dungeon landblock (_collapsedCenter), never the - // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. + // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. foreach (var id in _state.LoadedLandblockIds) if (id != _collapsedCenter) EnqueueUnload(id); } diff --git a/src/AcDream.App/Streaming/StreamingMutationException.cs b/src/AcDream.App/Streaming/StreamingMutationException.cs index 70424ab1..5c74dc88 100644 --- a/src/AcDream.App/Streaming/StreamingMutationException.cs +++ b/src/AcDream.App/Streaming/StreamingMutationException.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Streaming; +namespace AcDream.App.Streaming; /// /// Reports whether a streaming queue/retirement mutation crossed its commit /// point before a callback failed. Retry ledgers use this to avoid submitting /// the same generation operation twice after a post-commit diagnostic error. /// -internal sealed class StreamingMutationException : Exception +public sealed class StreamingMutationException : Exception { public StreamingMutationException( string message, diff --git a/src/AcDream.App/Streaming/StreamingRegion.cs b/src/AcDream.App/Streaming/StreamingRegion.cs index 66264d9b..1118489f 100644 --- a/src/AcDream.App/Streaming/StreamingRegion.cs +++ b/src/AcDream.App/Streaming/StreamingRegion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -7,9 +7,9 @@ namespace AcDream.App.Streaming; /// /// Pure data type describing the set of landblocks currently considered /// "visible" by the streaming system. Given a center landblock (x, y) and -/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window. +/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window. /// -internal sealed class StreamingRegion +public sealed class StreamingRegion { public int CenterX { get; private set; } public int CenterY { get; private set; } @@ -17,7 +17,7 @@ internal sealed class StreamingRegion public int NearRadius { get; } public int FarRadius { get; } - // Strictly the (2r+1)×(2r+1) window (clamped to world bounds). + // Strictly the (2r+1)×(2r+1) window (clamped to world bounds). private readonly HashSet _visible = new(); // Everything currently loaded: window + hysteresis-retained landblocks. @@ -42,7 +42,7 @@ internal sealed class StreamingRegion /// LandblockLoader. /// /// - /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include + /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include /// hysteresis-retained landblocks outside the window. Use /// to enumerate everything actually loaded. /// @@ -89,7 +89,7 @@ internal sealed class StreamingRegion /// /// Encode a landblock at (lbX, lbY) into the AC dat id form. Always uses /// the 0xFFFF terminator (LandBlock = terrain). The earlier - /// version of this method used 0xFFFE by mistake — that's the + /// version of this method used 0xFFFE by mistake — that's the /// LandBlockInfo id, and asking LandblockLoader.Load to read a /// LandBlock at the LandBlockInfo coords corrupts the dat reader's /// buffer position, returning a half-populated LandBlock.Height[] @@ -135,7 +135,7 @@ internal sealed class StreamingRegion /// /// Call once after to seed /// _tierResidence with the initial window. Every LB in the inner - /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far. + /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far. /// public void MarkResidentFromBootstrap() { @@ -191,8 +191,8 @@ internal sealed class StreamingRegion } /// - /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2. - /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null + /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2. + /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null /// unload. Requires (or a prior /// call to this method) to have seeded _tierResidence. /// @@ -214,7 +214,7 @@ internal sealed class StreamingRegion var toDemote = new List(); var toUnload = new List(); - // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote. + // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote. var newCenterIds = new HashSet(); for (int dx = -FarRadius; dx <= FarRadius; dx++) { @@ -231,18 +231,18 @@ internal sealed class StreamingRegion if (!_tierResidence.TryGetValue(id, out var current)) { - // Not resident at all — fresh load. + // Not resident at all — fresh load. if (inNear) toLoadNear.Add(id); else toLoadFar.Add(id); _tierResidence[id] = inNear ? TierResidence.Near : TierResidence.Far; } else if (current == TierResidence.Far && inNear) { - // Was Far, now inside Near ring — promote. + // Was Far, now inside Near ring — promote. toPromote.Add(id); _tierResidence[id] = TierResidence.Near; } - // Near→Near and Far→Far are no-ops. + // Near→Near and Far→Far are no-ops. } } @@ -259,7 +259,7 @@ internal sealed class StreamingRegion if (newCenterIds.Contains(id)) { - // Still in the far window — only Near→Far demote possible here. + // Still in the far window — only Near→Far demote possible here. if (current == TierResidence.Near && (absDx > NearRadius || absDy > NearRadius)) { if (distance > nearUnloadThreshold) @@ -271,7 +271,7 @@ internal sealed class StreamingRegion continue; } - // Outside the new window — demote / unload by threshold. + // Outside the new window — demote / unload by threshold. if (current == TierResidence.Near) { if (distance > nearUnloadThreshold) @@ -336,7 +336,7 @@ internal sealed class StreamingRegion toUnload.Add(id); } - // Update resident: (oldResident ∪ newVisible) ∖ toUnload. + // Update resident: (oldResident ∪ newVisible) ∖ toUnload. _resident.UnionWith(_visible); foreach (var id in toUnload) _resident.Remove(id); @@ -352,7 +352,7 @@ internal sealed class StreamingRegion /// Both lists are disjoint from the current /// set; the caller hands them to LandblockStreamer as jobs. /// -internal readonly record struct RegionDiff( +public readonly record struct RegionDiff( IReadOnlyList ToLoad, IReadOnlyList ToUnload); diff --git a/src/AcDream.App/Streaming/StreamingWorkBudget.cs b/src/AcDream.App/Streaming/StreamingWorkBudget.cs index 68618945..1d04f352 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudget.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudget.cs @@ -1,11 +1,11 @@ -using System.Diagnostics; +using System.Diagnostics; namespace AcDream.App.Streaming; /// /// Immutable, validated per-frame streaming work profile. /// -internal readonly record struct StreamingWorkBudget +public readonly record struct StreamingWorkBudget { public StreamingWorkBudget( TimeSpan maxUpdateTime, @@ -75,7 +75,7 @@ internal readonly record struct StreamingWorkBudget /// /// Conservative, known cost of one atomic streaming operation. /// -internal readonly record struct StreamingWorkCost( +public readonly record struct StreamingWorkCost( int CompletionAdmissions = 0, long AdoptedCpuBytes = 0, int EntityOperations = 0, @@ -117,14 +117,14 @@ internal readonly record struct StreamingWorkCost( left > long.MaxValue - right ? long.MaxValue : left + right; } -internal enum StreamingWorkAdmission : byte +public enum StreamingWorkAdmission : byte { Admitted, OversizedProgress, Yielded, } -internal enum StreamingWorkLimit : byte +public enum StreamingWorkLimit : byte { None, Time, @@ -144,7 +144,7 @@ internal enum StreamingWorkLane : byte /// /// Immutable observation of one frame's streaming work. /// -internal readonly record struct StreamingWorkMeterSnapshot( +public readonly record struct StreamingWorkMeterSnapshot( StreamingWorkCost Used, StreamingWorkCost DestinationUsed, StreamingWorkCost NonDestinationUsed, @@ -163,7 +163,7 @@ internal readonly record struct StreamingWorkMeterSnapshot( /// /// Read-only streaming scheduler facts published to lifecycle artifacts. /// -internal readonly record struct StreamingWorkDiagnostics( +public readonly record struct StreamingWorkDiagnostics( StreamingWorkMeterSnapshot LastFrame, long LifetimeFrameOverrunCount, long LifetimeOversizedProgressCount, @@ -187,7 +187,7 @@ internal readonly record struct StreamingWorkDiagnostics( /// Single-thread, frame-scoped admission meter. Callers reserve a known cost /// before an atomic operation and complete or fail that reservation afterward. /// -internal sealed class StreamingWorkMeter +public sealed class StreamingWorkMeter { private readonly StreamingWorkBudget _budget; private readonly Func _timestamp; diff --git a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs index d61532ce..edc4800c 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.Streaming; @@ -7,7 +7,7 @@ namespace AcDream.App.Streaming; /// These values limit work per frame; they never reduce the amount or /// quality of content that eventually becomes resident. /// -internal sealed record StreamingWorkBudgetOptions( +public sealed record StreamingWorkBudgetOptions( double MaxUpdateMilliseconds, int MaxCompletionAdmissions, long MaxAdoptedCpuBytes, diff --git a/src/AcDream.App/Streaming/TwoTierDiff.cs b/src/AcDream.App/Streaming/TwoTierDiff.cs index 7d08c9b9..2a24dab9 100644 --- a/src/AcDream.App/Streaming/TwoTierDiff.cs +++ b/src/AcDream.App/Streaming/TwoTierDiff.cs @@ -1,15 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace AcDream.App.Streaming; /// /// Output of for the two-tier model. /// Five disjoint lists describe what changed since the previous Tick. Per -/// Phase A.5 spec §4.2. +/// Phase A.5 spec §4.2. /// -internal readonly record struct TwoTierDiff( +public readonly record struct TwoTierDiff( IReadOnlyList ToLoadFar, // entered far window from null (terrain only) - IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport) + IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport) IReadOnlyList ToPromote, // entered near window from far-resident (entities only) IReadOnlyList ToDemote, // exited near window past hysteresis (drop entities) IReadOnlyList ToUnload); // exited far window past hysteresis (drop terrain) diff --git a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs index 41fa676c..5ebfd3e1 100644 --- a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs +++ b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs @@ -1,4 +1,4 @@ -using AcDream.App.Audio; +using AcDream.App.Audio; using AcDream.Core.Selection; using AcDream.Runtime.Entities; using AcDream.Runtime.World; @@ -10,7 +10,7 @@ namespace AcDream.App.Streaming; /// A hard login/portal reveal boundary makes the prior world unavailable /// immediately while its physical owners retire over later frames. /// -internal interface IWorldGenerationAvailability +public interface IWorldGenerationAvailability { bool IsWorldAvailable { get; } long QuiescedGeneration { get; } diff --git a/src/AcDream.App/Studio/DumpLayout.cs b/src/AcDream.App/Studio/DumpLayout.cs index 17279d2f..1f90f724 100644 --- a/src/AcDream.App/Studio/DumpLayout.cs +++ b/src/AcDream.App/Studio/DumpLayout.cs @@ -1,27 +1,27 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.UI; namespace AcDream.App.Studio; -// ───────────────────────────────────────────────────────────────────────────── -// DumpLayout — load a panel from the retail UI layout dump +// ───────────────────────────────────────────────────────────────────────────── +// DumpLayout — load a panel from the retail UI layout dump // // The dump stores every node's rect in ABSOLUTE screen coordinates (the // panel's design position in the retail UI, not relative to its parent). // Evidence: for the "inventory" panel, the root node is at x=500,y=138 and -// its direct children are also at x=500,y=161 — the child y=161 is only +// its direct children are also at x=500,y=161 — the child y=161 is only // 23 pixels below the parent y=138, which makes sense as a child offset // (the header row), not as the raw rect. If the rects were parent-relative, // (500,161) would place the child way off the window. // -// DumpLayout converts absolute → parent-relative by computing: +// DumpLayout converts absolute → parent-relative by computing: // child.Left = child.Rect.X - parent.Rect.X // child.Top = child.Rect.Y - parent.Rect.Y // // The root node (ParentTraversalIndex == null) is placed at (0,0) so the // whole tree sits at the UiHost origin rather than at the panel's retail // screen position. -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// /// Builds a static tree from the retail UI layout dump @@ -30,11 +30,11 @@ namespace AcDream.App.Studio; /// node's set to the dump's element_id and /// set to the widget_kind string. /// -/// This source is STATIC — no controllers, no FixtureProvider, no live +/// This source is STATIC — no controllers, no FixtureProvider, no live /// game data. It is a build reference for the UI Studio showing any of the 26 /// retail windows without needing the production panel wired up. /// -internal static class DumpLayout +public static class DumpLayout { /// /// Parse the dump at , find the panel whose slug @@ -42,7 +42,7 @@ internal static class DumpLayout /// /// /// maps a RenderSurface id (0x06xxxxxx) to a - /// (GL texture handle, native width, native height) triple — pass + /// (GL texture handle, native width, native height) triple — pass /// RenderStack.ResolveChrome from the studio, or a stub returning /// (1,1,1) for tests. /// @@ -52,10 +52,10 @@ internal static class DumpLayout public static UiElement? Load( string dumpPath, string slug, - Func resolve, + Func resolve, out string? error) { - // ── 1. Parse the dump JSON ──────────────────────────────────────── + // ── 1. Parse the dump JSON ──────────────────────────────────────── var dump = UiDumpModel.Parse(dumpPath); if (dump is null) { @@ -63,7 +63,7 @@ internal static class DumpLayout return null; } - // ── 2. Find the requested panel ─────────────────────────────────── + // ── 2. Find the requested panel ─────────────────────────────────── var panel = dump.Panels.FirstOrDefault( p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase)); if (panel is null) @@ -79,12 +79,12 @@ internal static class DumpLayout return null; } - // ── 3. Build a traversal-index → node lookup ────────────────────── + // ── 3. Build a traversal-index → node lookup ────────────────────── var byIndex = new Dictionary(panel.Nodes.Count); foreach (var n in panel.Nodes) byIndex[n.TraversalIndex] = n; - // ── 4. Create UiElement objects for every node ──────────────────── + // ── 4. Create UiElement objects for every node ──────────────────── var elements = new Dictionary(panel.Nodes.Count); foreach (var node in panel.Nodes) { @@ -92,7 +92,7 @@ internal static class DumpLayout elements[node.TraversalIndex] = el; } - // ── 5. Wire parent–child relationships + set parent-relative coords ─ + // ── 5. Wire parent–child relationships + set parent-relative coords ─ UiElement? root = null; foreach (var node in panel.Nodes) { @@ -100,7 +100,7 @@ internal static class DumpLayout if (node.ParentTraversalIndex is null) { - // Root node — place at (0,0) so the tree sits at the UiHost origin. + // Root node — place at (0,0) so the tree sits at the UiHost origin. // The panel's absolute rect offset is discarded here (it was the // retail design position inside the retail screen, which we don't need). el.Left = 0f; @@ -109,7 +109,7 @@ internal static class DumpLayout } else { - // Non-root: convert absolute → parent-relative by subtracting parent rect. + // Non-root: convert absolute → parent-relative by subtracting parent rect. // child.Left = child.Rect.X - parent.Rect.X // child.Top = child.Rect.Y - parent.Rect.Y // This preserves the visual layout inside each group without placing the @@ -137,11 +137,11 @@ internal static class DumpLayout return root; } - // ── Private helpers ─────────────────────────────────────────────────────── + // ── Private helpers ─────────────────────────────────────────────────────── private static UiElement BuildElement( DumpNode node, - Func resolve) + Func resolve) { uint imageId = UiDumpModel.PickImageId(node); var kind = node.WidgetKind ?? "Group"; @@ -149,7 +149,7 @@ internal static class DumpLayout UiElement el; if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase)) { - // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. + // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. el = new DumpSpriteElement(imageId, resolve) { Name = kind, @@ -159,7 +159,7 @@ internal static class DumpLayout } else { - // Group (or sprite without an image) — plain container, no own draw. + // Group (or sprite without an image) — plain container, no own draw. el = new DumpGroupElement() { Name = kind, @@ -168,7 +168,7 @@ internal static class DumpLayout }; } - // EventId is set from the dump's element_id (cast to uint — the decimal + // EventId is set from the dump's element_id (cast to uint — the decimal // values in the JSON represent the same dat handle used at runtime). el.EventId = (uint)node.ElementId; el.Left = node.Rect.X; // overwritten by caller per root/child logic @@ -180,27 +180,27 @@ internal static class DumpLayout } } -// ───────────────────────────────────────────────────────────────────────────── -// DumpSpriteElement — minimal element that draws a single sprite -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// DumpSpriteElement — minimal element that draws a single sprite +// ───────────────────────────────────────────────────────────────────────────── /// /// Draws a single sprite at its native size tiled to fill -/// × . Used for Sprite/Button/Scrollbar/Slider nodes from +/// × . Used for Sprite/Button/Scrollbar/Slider nodes from /// the retail UI dump. /// /// We do NOT reuse here because /// that class requires an ElementInfo with a populated StateMedia -/// dictionary — the dat-import plumbing — which is not needed for a static dump +/// dictionary — the dat-import plumbing — which is not needed for a static dump /// preview. A minimal subclass keeps the code simpler and the dependency surface /// smaller. /// internal sealed class DumpSpriteElement : UiElement { private readonly uint _imageId; - private readonly Func _resolve; + private readonly Func _resolve; - public DumpSpriteElement(uint imageId, Func resolve) + public DumpSpriteElement(uint imageId, Func resolve) { _imageId = imageId; _resolve = resolve; @@ -211,25 +211,25 @@ internal sealed class DumpSpriteElement : UiElement if (_imageId == 0) return; var (tex, tw, th) = _resolve(_imageId); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; - // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both + // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both // axes via GL_REPEAT, Width/tw and Height/th tile the texture). ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } } -// ───────────────────────────────────────────────────────────────────────────── -// DumpGroupElement — pure container (Group nodes from the dump) -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// DumpGroupElement — pure container (Group nodes from the dump) +// ───────────────────────────────────────────────────────────────────────────── /// -/// Container element for dump Group nodes — no own draw, just hosts children. +/// Container element for dump Group nodes — no own draw, just hosts children. /// Extending UiElement directly (no OnDraw override) gives transparent groups, /// which matches Group nodes in the retail layout that have no background sprite. /// internal sealed class DumpGroupElement : UiElement { - // No OnDraw — completely transparent container. + // No OnDraw — completely transparent container. } diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index 8489e688..eb375b40 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -1,4 +1,4 @@ -using AcDream.App.Rendering; +using AcDream.App.Rendering; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Combat; @@ -15,7 +15,7 @@ namespace AcDream.App.Studio; /// controller Bind methods against . /// /// -/// The studio is intentionally thin — there is no live game session, no +/// The studio is intentionally thin — there is no live game session, no /// server connection, and no network. FixtureProvider bridges that gap by /// feeding static fixtures (vitals percentages, a fake inventory, empty /// shortcut lists) so the bound widgets show plausible state instead of @@ -23,7 +23,7 @@ namespace AcDream.App.Studio; /// /// /// -/// IconIds approach: raw-resolve stub — resolve the base iconId +/// IconIds approach: raw-resolve stub — resolve the base iconId /// via and return the GL handle /// directly. This is intentionally simpler than GameWindow's full /// (5-layer composite). The raw icon is enough @@ -31,7 +31,7 @@ namespace AcDream.App.Studio; /// compositor is the live-game concern, not the layout preview concern. /// /// -internal static class FixtureProvider +public static class FixtureProvider { /// /// Populate with sample data appropriate for @@ -131,7 +131,7 @@ internal static class FixtureProvider { // Resolve the per-list empty-slot art from the dat cell template, matching the // exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem - // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty). + // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty). uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u); uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu); uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u); @@ -177,10 +177,10 @@ internal static class FixtureProvider return new RetainedPanelControllerGroup(inventory, paperdoll); } - case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E) + case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E) // Bind the REAL importer-mounted header + list elements (name/heritage/PK/level/ // total-XP/XP-meter + the 9-row attribute list + footer State-A). NOT the text-report - // sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController). + // sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController). // LargeDatFont (0x40000001, MaxCharHeight=18) is used for the attribute row text; // fallback to VitalsDatFont (0x40000000, 16px) if unavailable. CharacterStatController.Bind( @@ -192,12 +192,12 @@ internal static class FixtureProvider return null; default: - // Unknown layout — no-op; the panel renders structurally. + // Unknown layout — no-op; the panel renders structurally. return null; } } - // ── Helpers ───────────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────────── /// /// Build the iconIds delegate for toolbar / inventory controllers. @@ -206,15 +206,15 @@ internal static class FixtureProvider /// Raw-resolve stub: resolve the base (arg 2) /// via and return its GL handle. /// The remaining args (type, underlayId, overlayId, effects) are ignored - /// for the studio — a single-layer icon is sufficient for layout preview. + /// for the studio — a single-layer icon is sufficient for layout preview. /// /// /// This is what the task spec calls "v1 raw-resolve stub". /// - private static Func MakeIconIds(RenderStack stack) + private static Func MakeIconIds(RenderStack stack) => (_, iconId, _, _, _) => { - if (iconId == 0u) return GpuTextureSlot.Unassigned; + if (iconId == 0u) return 0u; var (handle, _, _) = stack.ResolveChrome(iconId); return handle; }; diff --git a/src/AcDream.App/Studio/LayoutSource.cs b/src/AcDream.App/Studio/LayoutSource.cs index ba61f2b8..78e53383 100644 --- a/src/AcDream.App/Studio/LayoutSource.cs +++ b/src/AcDream.App/Studio/LayoutSource.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI; +using AcDream.App.UI; using AcDream.App.UI.Layout; using DatReaderWriter; using AcDream.Content; @@ -6,21 +6,21 @@ using AcDream.Content; namespace AcDream.App.Studio; /// Which kind of source the studio is currently previewing. -internal enum LayoutSourceKind { DatLayout, Markup } +public enum LayoutSourceKind { DatLayout, Markup } /// /// Wraps the two ways the UI Studio can load a panel to preview: -/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now). +/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now). /// /// Call with the current to /// import the layout and get the root . The result is also /// cached in so can re-run the same /// source without re-reading the options. /// -internal sealed class LayoutSource +public sealed class LayoutSource { private readonly IDatReaderWriter _dats; - private readonly Func _resolve; + private readonly Func _resolve; private readonly UiDatFont? _datFont; private readonly Func? _fontResolve; @@ -33,17 +33,17 @@ internal sealed class LayoutSource /// /// Create a LayoutSource. /// - /// Optional per-element font resolver: FontDid → + /// Optional per-element font resolver: FontDid → /// (null when the font isn't in the dats). When supplied, /// elements with a non-zero FontDid receive their own dat font at build time /// instead of the shared global. Controllers that /// explicitly set after /// still override the build-time value. - /// Pass null (default) for the original single-font behavior — the live + /// Pass null (default) for the original single-font behavior — the live /// path passes null so it is provably unchanged. public LayoutSource( IDatReaderWriter dats, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -107,7 +107,7 @@ internal sealed class LayoutSource return LoadDat(LayoutId.Value); } - // ── Private ────────────────────────────────────────────────────────────────── + // ── Private ────────────────────────────────────────────────────────────────── private UiElement? LoadDat(uint layoutId) { diff --git a/src/AcDream.App/Studio/PanelFbo.cs b/src/AcDream.App/Studio/PanelFbo.cs index d8295aa1..4f61ec55 100644 --- a/src/AcDream.App/Studio/PanelFbo.cs +++ b/src/AcDream.App/Studio/PanelFbo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.App.UI; @@ -19,11 +19,11 @@ namespace AcDream.App.Studio; /// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image) /// so the image appears right-side-up in ImGui's top-left coordinate system. /// -internal sealed unsafe class PanelFbo : IDisposable +public sealed unsafe class PanelFbo : IDisposable { private readonly GL _gl; - // Off-screen target — lazily (re)created when the requested size changes. + // Off-screen target — lazily (re)created when the requested size changes. private uint _fbo; private uint _colorTex; private uint _depthRbo; @@ -37,7 +37,7 @@ internal sealed unsafe class PanelFbo : IDisposable /// /// Render (a full draw pass) into a - /// private FBO at × pixels. + /// private FBO at × pixels. /// Returns the GL color texture handle (0 on failure). The texture is valid until /// the next call to with a different size, or until . /// @@ -50,7 +50,7 @@ internal sealed unsafe class PanelFbo : IDisposable // Seal the entire pass: GLStateScope saves + restores every GL state the // UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state - // — set up by BeginFrame and expected intact by Render — is untouched. + // — set up by BeginFrame and expected intact by Render — is untouched. using var scope = new GLStateScope(_gl); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); @@ -95,7 +95,7 @@ internal sealed unsafe class PanelFbo : IDisposable return buf; } - // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ────── + // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ────── private void EnsureFramebuffer(int width, int height) { diff --git a/src/AcDream.App/Studio/SampleData.cs b/src/AcDream.App/Studio/SampleData.cs index 2422ba59..e523fc8e 100644 --- a/src/AcDream.App/Studio/SampleData.cs +++ b/src/AcDream.App/Studio/SampleData.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Items; namespace AcDream.App.Studio; @@ -25,18 +25,18 @@ namespace AcDream.App.Studio; /// Equipped chest armor : 0x060011CFu /// Equipped melee weapon : 0x060011CBu /// -/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes +/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes /// as `iconId` into the iconIds lambda. FixtureProvider resolves them via /// and returns the raw GL handle. /// -internal static class SampleData +public static class SampleData { - // ── Guids ──────────────────────────────────────────────────────────────── + // ── Guids ──────────────────────────────────────────────────────────────── /// Fake server guid for the studio's synthetic player. public const uint PlayerGuid = 0x50000001u; - // Items in main pack (slots 0–5). + // Items in main pack (slots 0–5). private const uint SwordGuid = 0x50000010u; private const uint ChestGuid = 0x50000011u; private const uint GlovesGuid = 0x50000012u; @@ -53,7 +53,7 @@ internal static class SampleData private const uint ChestEqGuid = 0x50000031u; private const uint WeaponEqGuid = 0x50000032u; - // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ─────────────────────────── + // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ─────────────────────────── // These are the same underlay/fallback icon ids the IconComposer tests pin. private const uint IconWeapon = 0x060011CBu; // weapon underlay @@ -62,7 +62,7 @@ internal static class SampleData private const uint IconJewelry = 0x060011D5u; // jewelry underlay private const uint IconMisc = 0x060011D4u; // misc / fallback underlay - // ── Public API ────────────────────────────────────────────────────────── + // ── Public API ────────────────────────────────────────────────────────── /// /// Build a fresh populated with the @@ -74,7 +74,7 @@ internal static class SampleData { var t = new ClientObjectTable(); - // ── Player object ───────────────────────────────────────────────── + // ── Player object ───────────────────────────────────────────────── t.AddOrUpdate(new ClientObject { ObjectId = PlayerGuid, @@ -89,7 +89,7 @@ internal static class SampleData AetheriaUnlocks.PropertyId, (int)AetheriaUnlockState.All); - // ── Loose items in main pack (slots 0–5) ────────────────────────── + // ── Loose items in main pack (slots 0–5) ────────────────────────── AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60); AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200); @@ -98,14 +98,14 @@ internal static class SampleData AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30); AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100); - // ── Side bags (Container items in main pack, slots 6 & 7) ───────── + // ── Side bags (Container items in main pack, slots 6 & 7) ───────── AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1", containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24); AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2", containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24); - // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ── + // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ── AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear); AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor); @@ -114,13 +114,13 @@ internal static class SampleData return t; } - // ── Sample vital constants (used by FixtureProvider) ──────────────────── + // ── Sample vital constants (used by FixtureProvider) ──────────────────── public const float HealthPct = 0.8f; public const float StaminaPct = 0.6f; public const float ManaPct = 0.9f; - // ── Sample character sheet (used by CharacterController in the Studio) ─── + // ── Sample character sheet (used by CharacterController in the Studio) ─── /// /// Returns a representative for the studio's @@ -145,12 +145,12 @@ internal static class SampleData XpToNextLevel = 42_000_000, XpFraction = 0.63f, - // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal). + // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal). HealthCurrent = 5, HealthMax = 5, StaminaCurrent = 10, StaminaMax = 10, ManaCurrent = 10, ManaMax = 10, - // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal). + // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal). Strength = 200, Endurance = 10, Quickness = 200, @@ -171,9 +171,9 @@ internal static class SampleData // Raise costs in retail display order (Strength, Endurance, Coordination, Quickness, // Focus, Self, Health, Stamina, Mana). - // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable. - // Focus@10 → 110 matches the authoritative retail screenshot (spec §4). - // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10). + // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable. + // Focus@10 → 110 matches the authoritative retail screenshot (spec §4). + // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10). AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L }, AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L }, @@ -207,7 +207,7 @@ internal static class SampleData BurdenMax = 4500, }; - // ── Helpers ───────────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────────── private static void AddItem( ClientObjectTable t, diff --git a/src/AcDream.App/Studio/StudioInspector.cs b/src/AcDream.App/Studio/StudioInspector.cs index 76f2f9a9..26e6dc0d 100644 --- a/src/AcDream.App/Studio/StudioInspector.cs +++ b/src/AcDream.App/Studio/StudioInspector.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.UI; using ImGuiNET; @@ -8,7 +8,7 @@ namespace AcDream.App.Studio; /// All canvas mouse events gathered by in one frame. /// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot). /// -internal readonly struct CanvasInputEvent +public readonly struct CanvasInputEvent { /// Mouse is currently hovering the canvas image. When false all other fields are 0 / false. public readonly bool IsHovered; @@ -37,13 +37,13 @@ internal readonly struct CanvasInputEvent /// /// Four-pane ImGui IDE for the acdream UI Studio: /// -/// Toolbar — panel picker (slug combo) across the top. -/// Canvas — shows the panel FBO texture; in Interact mode mouse events +/// Toolbar — panel picker (slug combo) across the top. +/// Canvas — shows the panel FBO texture; in Interact mode mouse events /// are forwarded to the panel UiHost (buttons/tabs respond); in Inspect mode a /// left-click hit-tests and selects the element under the cursor. -/// Tree — recursive ImGui tree of the element hierarchy; clicking a node +/// Tree — recursive ImGui tree of the element hierarchy; clicking a node /// sets . -/// Properties — shows the element's geometry, +/// Properties — shows the element's geometry, /// anchors, and z-order. /// /// @@ -52,20 +52,20 @@ internal readonly struct CanvasInputEvent /// sub-window. After ImGui.Image we call ImGui.GetItemRectMin() to get the /// screen-space top-left of the drawn image (accounting for the sub-window's title bar, /// padding, and any scrolling). Subtracting that from the raw mouse screen position gives -/// panel-local pixels directly — no additional scale factor is needed because the image is +/// panel-local pixels directly — no additional scale factor is needed because the image is /// drawn 1:1. /// -/// V-flip — no extra Y inversion needed: +/// V-flip — no extra Y inversion needed: /// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to /// ImGui.Image to flip V. After this flip, displayed row 0 (top of the image on /// screen) corresponds to panel Y=0 (the top of the UI panel), matching UiRoot's /// top-left origin. Therefore the panel-local Y computed above maps directly into UiRoot -/// without further inversion — do NOT flip Y again. +/// without further inversion — do NOT flip Y again. /// /// Layout: the four panes call SetNextWindowPos + SetNextWindowSize /// with ImGuiCond.FirstUseEver so they start docked but can be freely dragged. /// -internal sealed class StudioInspector +public sealed class StudioInspector { /// Currently selected element (set by tree-click or canvas-click in Inspect mode). public UiElement? Selected { get; set; } @@ -77,7 +77,7 @@ internal sealed class StudioInspector /// public bool InteractMode { get; set; } = true; - // ── Toolbar ─────────────────────────────────────────────────────────────────── + // ── Toolbar ─────────────────────────────────────────────────────────────────── /// /// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and @@ -124,7 +124,7 @@ internal sealed class StudioInspector return result; } - // ── Canvas ──────────────────────────────────────────────────────────────────── + // ── Canvas ──────────────────────────────────────────────────────────────────── /// /// Draw the "Canvas" ImGui window containing the panel FBO texture and return all @@ -133,13 +133,13 @@ internal sealed class StudioInspector /// Coordinate mapping: After ImGui.Image, GetItemRectMin() /// returns the actual screen-space top-left of the drawn image (accounting for the /// sub-window title bar, padding, and scrolling). Subtracting that from the raw ImGui - /// mouse position gives panel-local pixels directly — no scale factor because the + /// mouse position gives panel-local pixels directly — no scale factor because the /// image is drawn 1:1. /// - /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the + /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the /// GL bottom-left origin is flipped to top-left on screen. After the flip, screen /// row 0 = panel Y 0 (top of the UI), so the computed Y already matches UiRoot's - /// top-left origin — do NOT flip Y again. + /// top-left origin — do NOT flip Y again. /// /// If is non-null a bright-green 2-pixel outline is /// drawn over it using the window draw list. @@ -163,7 +163,7 @@ internal sealed class StudioInspector // This is what lets us translate raw mouse screen coords into panel-local pixels. var rectMin = ImGui.GetItemRectMin(); - // ── Selection highlight ─────────────────────────────────────────────── + // ── Selection highlight ─────────────────────────────────────────────── var el = Selected; if (el is not null && el.Width > 0f && el.Height > 0f) { @@ -176,7 +176,7 @@ internal sealed class StudioInspector 0f, ImDrawFlags.None, 2f); } - // ── Gather canvas mouse events ──────────────────────────────────────── + // ── Gather canvas mouse events ──────────────────────────────────────── // IsItemHovered is true when the mouse is over the Image item (not just the window). bool hovered = ImGui.IsItemHovered(); int mx = 0, my = 0; @@ -187,7 +187,7 @@ internal sealed class StudioInspector { var mousePos = ImGui.GetMousePos(); // Panel-local pixel = mouse offset from the image's screen-space top-left. - // Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary. + // Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary. int ix = (int)(mousePos.X - rectMin.X); int iy = (int)(mousePos.Y - rectMin.Y); // Clamp to image bounds (mouse can be on the image edge pixel). @@ -202,7 +202,7 @@ internal sealed class StudioInspector } else { - // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel. + // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel. hovered = false; } } @@ -211,7 +211,7 @@ internal sealed class StudioInspector return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll); } - // ── Tree ────────────────────────────────────────────────────────────────────── + // ── Tree ────────────────────────────────────────────────────────────────────── /// Draw the "Tree" ImGui window. Clicking a node sets . public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH) @@ -252,7 +252,7 @@ internal sealed class StudioInspector } } - // ── Properties ─────────────────────────────────────────────────────────────── + // ── Properties ─────────────────────────────────────────────────────────────── /// Draw the "Properties" ImGui window for . public void DrawProperties(int windowX, int windowY, int windowW, int windowH) diff --git a/src/AcDream.App/Studio/StudioOptions.cs b/src/AcDream.App/Studio/StudioOptions.cs index 1af8c075..8e2a3c2e 100644 --- a/src/AcDream.App/Studio/StudioOptions.cs +++ b/src/AcDream.App/Studio/StudioOptions.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.Studio; +namespace AcDream.App.Studio; /// /// Parsed options for the acdream UI Studio standalone tool. /// Constructed by from the command-line tokens that follow /// the ui-studio dispatch token. /// -internal sealed record StudioOptions( +public sealed record StudioOptions( string DatDir, uint? LayoutId, string? MarkupPath, @@ -24,11 +24,11 @@ internal sealed record StudioOptions( /// --layout 0xNNNN: hex LayoutDesc dat id to preview. /// --markup <path>: path to a KSML markup file (Task 6, unsupported for now). /// --dump <slug>: load a panel from the retail UI dump JSON by slug - /// (e.g. inventory, radar, toolbar). Static mockup — no controllers. + /// (e.g. inventory, radar, toolbar). Static mockup — no controllers. /// --dump-file <path>: override the default dump file path /// (docs/research/2026-06-25-retail-ui-layout-dump.json from the solution root). /// Only meaningful when --dump is also given. - /// --screenshot <path>: headless mode — render the loaded panel to a PNG + /// --screenshot <path>: headless mode — render the loaded panel to a PNG /// at and exit without showing an interactive window. /// Combines with --dump or --layout. /// When neither --layout, --markup, nor --dump is given the @@ -96,7 +96,7 @@ internal sealed record StudioOptions( if (string.IsNullOrWhiteSpace(datDir)) throw new InvalidOperationException( - "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR."); + "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR."); // Default layout: vitals (0x2100006C), unless a dump slug or markup is requested. if (!mockup && layoutId is null && markupPath is null && dumpSlug is null) diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs index 4817f943..be43d853 100644 --- a/src/AcDream.App/Studio/StudioWindow.cs +++ b/src/AcDream.App/Studio/StudioWindow.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Content; using AcDream.App.Platform; using AcDream.App.Audio; @@ -26,18 +26,18 @@ namespace AcDream.App.Studio; /// /// Task 3 adds an ImGui IDE on top of the panel FBO: /// -/// Canvas pane — the panel rendered off-screen via . -/// Tree pane — the element hierarchy; click-to-select. -/// Properties pane — geometry/anchors/flags of the selected element. -/// Click-to-inspect — a left-click in the canvas selects the topmost +/// Canvas pane — the panel rendered off-screen via . +/// Tree pane — the element hierarchy; click-to-select. +/// Properties pane — geometry/anchors/flags of the selected element. +/// Click-to-inspect — a left-click in the canvas selects the topmost /// element under the cursor via . /// /// -/// The window is intentionally thin: no game world, no physics, no streaming — +/// The window is intentionally thin: no game world, no physics, no streaming — /// just GL + UiHost + the layout under test, identical to how the panel /// appears inside GameWindow. /// -internal sealed class StudioWindow : IDisposable +public sealed class StudioWindow : IDisposable { private readonly StudioOptions _opts; private readonly ApplicationPathSet _applicationPaths; @@ -63,7 +63,7 @@ internal sealed class StudioWindow : IDisposable private string? _dumpFile; // resolved dump file path (once, in OnLoad) private IReadOnlyList _dumpSlugs = Array.Empty(); // all slugs from the dump - // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime + // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime // so the controller subscriptions (ObjectAdded/ObjectMoved etc.) fire correctly. private AcDream.Core.Items.ClientObjectTable? _objects; private IRetainedPanelController? _fixtureController; @@ -114,7 +114,7 @@ internal sealed class StudioWindow : IDisposable { _platformServices.ConfigureWindowBackend(); // Resolve quality settings the same way GameWindow.Run() does - // (SettingsStore → QualitySettings.From → WithEnvOverrides). + // (SettingsStore → QualitySettings.From → WithEnvOverrides). var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore( _applicationPaths.SettingsFile); var startupDisplay = startupStore.LoadDisplay(); @@ -131,7 +131,7 @@ internal sealed class StudioWindow : IDisposable ContextFlags.ForwardCompatible, new APIVersion(4, 3)), VSync = false, - // MSAA from quality preset — must be baked into the GL context at creation. + // MSAA from quality preset — must be baked into the GL context at creation. Samples = startupQuality.MsaaSamples, PreferredStencilBufferBits = 8, // Headless screenshot mode: hide the window so no desktop flash occurs. @@ -195,7 +195,7 @@ internal sealed class StudioWindow : IDisposable _dats = RuntimeDatCollectionFactory.OpenReadOnly(_opts.DatDir); - // Build QualitySettings for RenderBootstrap (same as Run() above — re-read + // Build QualitySettings for RenderBootstrap (same as Run() above — re-read // after the GL context is confirmed, mirroring GameWindow.OnLoad). var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore( _applicationPaths.SettingsFile); @@ -265,7 +265,7 @@ internal sealed class StudioWindow : IDisposable // Task 4: populate the panel with sample data via production controllers, // so inventory / vitals / toolbar panels render with plausible content. - // Dump source is static — no FixtureProvider needed. + // Dump source is static — no FixtureProvider needed. if (_opts.DumpSlug is null && _source.CurrentLayout is not null) { uint layoutId = _opts.LayoutId ?? 0x2100006Cu; @@ -275,7 +275,7 @@ internal sealed class StudioWindow : IDisposable } } - // Task 3: ImGui IDE — interactive mode only. + // Task 3: ImGui IDE — interactive mode only. // Headless screenshot mode needs only PanelFbo; ImGui/inspector/input are skipped. _panelFbo = new PanelFbo(_gl); if (_opts.ScreenshotPath is null) @@ -337,14 +337,14 @@ internal sealed class StudioWindow : IDisposable try { - // ── HEADLESS SCREENSHOT PATH ────────────────────────────────────────────── + // ── HEADLESS SCREENSHOT PATH ────────────────────────────────────────────── if (_opts.ScreenshotPath is not null) { if (_screenshotDone) return; // fire exactly once _screenshotDone = true; // Pick render size from the loaded root's bounds (clamped to sane limits). - // Fall back to 1280×720 when the root has no explicit size. + // Fall back to 1280×720 when the root has no explicit size. int w = 1280, h = 720; if (!_opts.Mockup && _panelRoot is not null) { @@ -372,7 +372,7 @@ internal sealed class StudioWindow : IDisposable return; } - // Flip rows vertically: FBO bottom-left → PNG top-left. + // Flip rows vertically: FBO bottom-left → PNG top-left. int stride = w * 4; byte[] flipped = new byte[pixels.Length]; for (int row = 0; row < h; row++) @@ -394,7 +394,7 @@ internal sealed class StudioWindow : IDisposable return; } - // ── INTERACTIVE PATH ────────────────────────────────────────────────────── + // ── INTERACTIVE PATH ────────────────────────────────────────────────────── if (_opts.Mockup) { var mockupGl = _stack.Gl; @@ -415,12 +415,12 @@ internal sealed class StudioWindow : IDisposable int iw = _window!.Size.X; int ih = _window!.Size.Y; - // 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta). + // 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta). _stack.UiHost.Tick(dt); // 2. Render the panel into the off-screen FBO; get the color texture. // The FBO is the same logical size as the window, so element rects map 1:1 to - // FBO pixels — no scale factor needed when displaying the canvas at full size. + // FBO pixels — no scale factor needed when displaying the canvas at full size. uint panelTex = _panelFbo.Render(iw, ih, _stack.UiHost); // 3. Clear the window back-buffer (the dark ImGui background shows behind panes). @@ -430,8 +430,8 @@ internal sealed class StudioWindow : IDisposable // 4. Begin the ImGui frame. _imgui.BeginFrame((float)dt); - // ── Layout constants (fixed pane arrangement, FirstUseEver) ────────────── - // MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it + // ── Layout constants (fixed pane arrangement, FirstUseEver) ────────────── + // MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it // is never covered by the floating panes (replaces the old 40px toolbar). // Tree: 280px wide on the left, below menu bar. // Canvas: centre strip between tree and properties. @@ -445,7 +445,7 @@ internal sealed class StudioWindow : IDisposable int paneY = kMenuBarH; int paneH = Math.Max(1, ih - kMenuBarH); - // 5. Main menu bar — panel picker combo pinned to the window top. + // 5. Main menu bar — panel picker combo pinned to the window top. // BeginMainMenuBar returns true when the bar is visible (always is); the combo // inside it is always-on-top and is never occluded by Tree/Canvas/Props panes. string? pickedSlug = null; @@ -471,7 +471,7 @@ internal sealed class StudioWindow : IDisposable if (pickedSlug is not null) LoadDumpPanel(pickedSlug); - // 6. Canvas pane — show the FBO texture; gather canvas mouse events. + // 6. Canvas pane — show the FBO texture; gather canvas mouse events. var canvasEvt = default(CanvasInputEvent); if (panelTex != 0) canvasEvt = _inspector.DrawCanvas( @@ -501,10 +501,10 @@ internal sealed class StudioWindow : IDisposable if (_inspector.InteractMode) { - // ── Interact: live panel interaction ────────────────────────────── + // ── Interact: live panel interaction ────────────────────────────── if (canvasEvt.LeftDown) { - Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})"); + Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})"); root.OnMouseDown(UiMouseButton.Left, mx, my); } if (canvasEvt.LeftUp) @@ -514,7 +514,7 @@ internal sealed class StudioWindow : IDisposable } else { - // ── Inspect: click selects an element in the tree ───────────────── + // ── Inspect: click selects an element in the tree ───────────────── if (canvasEvt.LeftDown) { var hit = root.Pick(mx, my); @@ -634,7 +634,7 @@ internal sealed class StudioWindow : IDisposable _imgui = null; _panelFbo = null; // If OnClosing wasn't called (e.g. an exception before Run() completed), dispose the FULL - // stack anyway — the review flagged that disposing only UiHost here leaked the rest. + // stack anyway — the review flagged that disposing only UiHost here leaked the rest. _stack?.Dispose(); _dats?.Dispose(); _audio?.Dispose(); diff --git a/src/AcDream.App/Studio/UiDumpModel.cs b/src/AcDream.App/Studio/UiDumpModel.cs index 174e34a5..c33e4b9e 100644 --- a/src/AcDream.App/Studio/UiDumpModel.cs +++ b/src/AcDream.App/Studio/UiDumpModel.cs @@ -1,10 +1,10 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; namespace AcDream.App.Studio; -// ───────────────────────────────────────────────────────────────────────────── -// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json +// ───────────────────────────────────────────────────────────────────────────── +// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json // // Schema (v1): // { "version":1, "panels":[ { "id":int, "slug":string, "title":string, @@ -25,10 +25,10 @@ namespace AcDream.App.Studio; // Rect coordinates are ABSOLUTE (screen-space origin = panel's design position // in retail layout, NOT relative to the parent). DumpLayout.Load converts them // to parent-relative when building the UiElement tree. -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// Top-level container for the retail UI layout dump. -internal sealed class UiDump +public sealed class UiDump { [JsonPropertyName("version")] public int Version { get; set; } @@ -38,7 +38,7 @@ internal sealed class UiDump } /// One panel (window) exported from the retail UI. -internal sealed class DumpPanel +public sealed class DumpPanel { [JsonPropertyName("id")] public long Id { get; set; } @@ -66,7 +66,7 @@ internal sealed class DumpPanel } /// One element node within a panel's traversal list. -internal sealed class DumpNode +public sealed class DumpNode { [JsonPropertyName("traversal_index")] public int TraversalIndex { get; set; } @@ -96,8 +96,8 @@ internal sealed class DumpNode public DumpStateSet StateSet { get; set; } = new(); } -/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement). -internal sealed class DumpRect +/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement). +public sealed class DumpRect { [JsonPropertyName("x")] public float X { get; set; } @@ -112,8 +112,8 @@ internal sealed class DumpRect public float Height { get; set; } } -/// State set for a node — default image plus per-state overrides. -internal sealed class DumpStateSet +/// State set for a node — default image plus per-state overrides. +public sealed class DumpStateSet { [JsonPropertyName("default_image")] public DumpImage? DefaultImage { get; set; } @@ -123,7 +123,7 @@ internal sealed class DumpStateSet } /// Image reference (RenderSurface dat id + optional separate alpha surface). -internal sealed class DumpImage +public sealed class DumpImage { [JsonPropertyName("image_id")] public long ImageId { get; set; } @@ -133,7 +133,7 @@ internal sealed class DumpImage } /// A named state override. -internal sealed class DumpState +public sealed class DumpState { [JsonPropertyName("state_id")] public int StateId { get; set; } @@ -142,14 +142,14 @@ internal sealed class DumpState public DumpImage Image { get; set; } = new(); } -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── // Helper statics -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── /// /// Parsing helpers for the retail UI dump JSON. /// -internal static class UiDumpModel +public static class UiDumpModel { private static readonly JsonSerializerOptions _opts = new() { diff --git a/src/AcDream.App/UI/ClientCommandController.cs b/src/AcDream.App/UI/ClientCommandController.cs index 2e1510ac..08eef64d 100644 --- a/src/AcDream.App/UI/ClientCommandController.cs +++ b/src/AcDream.App/UI/ClientCommandController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Physics; +using AcDream.Core.Physics; using AcDream.Core.Ui; using AcDream.Core.Social; using AcDream.UI.Abstractions; @@ -10,9 +10,9 @@ namespace AcDream.App.UI; /// remain backend- and network-agnostic; this controller owns the boundary /// between verified command behavior and live session/UI services. /// -internal sealed class ClientCommandController +public sealed class ClientCommandController { - internal sealed record Bindings( + public sealed record Bindings( Action TeleportToLifestone, Action TeleportToMarketplace, Action TeleportToPkArena, diff --git a/src/AcDream.App/UI/ControlsIni.cs b/src/AcDream.App/UI/ControlsIni.cs index 42e03e74..2812d696 100644 --- a/src/AcDream.App/UI/ControlsIni.cs +++ b/src/AcDream.App/UI/ControlsIni.cs @@ -1,16 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Numerics; namespace AcDream.App.UI; /// -/// Minimal reader for retail's controls.ini — a flat INI with one +/// Minimal reader for retail's controls.ini — a flat INI with one /// [section] per element type. Colors are #AARRGGBB (alpha /// first). Optional: a missing file yields an empty sheet (callers fall back -/// to hardcoded defaults). See the D.2b spec §7. +/// to hardcoded defaults). See the D.2b spec §7. /// -internal sealed class ControlsIni +public sealed class ControlsIni { private readonly Dictionary> _sections; diff --git a/src/AcDream.App/UI/CursorFeedbackController.cs b/src/AcDream.App/UI/CursorFeedbackController.cs index fbfa3597..babab5d6 100644 --- a/src/AcDream.App/UI/CursorFeedbackController.cs +++ b/src/AcDream.App/UI/CursorFeedbackController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Combat; +using AcDream.Core.Combat; using AcDream.Runtime.Gameplay; namespace AcDream.App.UI; @@ -62,7 +62,7 @@ public enum RetailGlobalCursorKind TargetInvalid, } -internal readonly record struct CursorFeedback( +public readonly record struct CursorFeedback( CursorFeedbackKind Kind, UiCursorMedia Cursor = default, RetailGlobalCursorKind GlobalKind = RetailGlobalCursorKind.Default) @@ -72,7 +72,7 @@ internal readonly record struct CursorFeedback( public readonly record struct CursorFeedbackSnapshot( object? DragPayload = null, - DragAcceptState DragAccept = DragAcceptState.None, + UiItemSlot.DragAcceptState DragAccept = UiItemSlot.DragAcceptState.None, ResizeEdges ActiveResizeEdges = ResizeEdges.None, ResizeEdges HoverResizeEdges = ResizeEdges.None, bool WindowMoveActive = false, @@ -85,7 +85,7 @@ public readonly record struct CursorFeedbackSnapshot( RetailCursorTargetMode TargetMode = RetailCursorTargetMode.None, CombatMode CombatMode = CombatMode.NonCombat); -internal sealed class CursorFeedbackController +public sealed class CursorFeedbackController { private readonly ItemInteractionController? _itemInteraction; private readonly Func? _worldTargetProvider; @@ -110,8 +110,8 @@ internal sealed class CursorFeedbackController UiElement? hover = root.Pick(root.MouseX, root.MouseY); // Retail UpdateCursorState (0x00564630) keys the target-mode cursor off - // the SmartBox found object — the WORLD entity under the cursor. A UI - // window occludes the world (no found object → pending). The one + // the SmartBox found object — the WORLD entity under the cursor. A UI + // window occludes the world (no found object → pending). The one // UI-side source retail-style cells contribute is an occupied item // slot's own item. RetailCursorTargetMode targetMode = ModeFromInteraction(_itemInteraction); @@ -127,7 +127,7 @@ internal sealed class CursorFeedbackController var snapshot = new CursorFeedbackSnapshot( DragPayload: root.DragPayload, - DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? DragAcceptState.None, + DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? UiItemSlot.DragAcceptState.None, ActiveResizeEdges: root.ActiveResizeEdges, HoverResizeEdges: root.HoverResizeEdges, WindowMoveActive: root.IsWindowMoveActive, @@ -168,8 +168,8 @@ internal sealed class CursorFeedbackController { return snapshot.DragAccept switch { - DragAcceptState.Accept => CursorFeedbackKind.DragAccept, - DragAcceptState.Reject => CursorFeedbackKind.DragReject, + UiItemSlot.DragAcceptState.Accept => CursorFeedbackKind.DragAccept, + UiItemSlot.DragAcceptState.Reject => CursorFeedbackKind.DragReject, _ => CursorFeedbackKind.Drag, }; } @@ -197,10 +197,10 @@ internal sealed class CursorFeedbackController } // Retail UpdateCursorState (0x00564630), TARGET_MODE 3: no found - // object → the 0x27 four-arrows pending cursor — INCLUDING over + // object → the 0x27 four-arrows pending cursor — INCLUDING over // UI chrome. Valid/invalid exist only with a target under the - // cursor. (The earlier HoverUi → Invalid arm was a non-retail - // invention — 2026-07-03 visual gate.) + // cursor. (The earlier HoverUi → Invalid arm was a non-retail + // invention — 2026-07-03 visual gate.) return CursorFeedbackKind.TargetPending; } diff --git a/src/AcDream.App/UI/GameplayConfirmationController.cs b/src/AcDream.App/UI/GameplayConfirmationController.cs index 2ef0ebf7..b23686d0 100644 --- a/src/AcDream.App/UI/GameplayConfirmationController.cs +++ b/src/AcDream.App/UI/GameplayConfirmationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Net.Messages; namespace AcDream.App.UI; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// completion; this semantic owner retains the server type/context and sends the /// matching confirmation response. /// -internal sealed class GameplayConfirmationController : IDisposable +public sealed class GameplayConfirmationController : IDisposable { private readonly RetailDialogFactory _dialogs; private readonly Action _sendResponse; diff --git a/src/AcDream.App/UI/IItemListDragHandler.cs b/src/AcDream.App/UI/IItemListDragHandler.cs index 64543527..3c149142 100644 --- a/src/AcDream.App/UI/IItemListDragHandler.cs +++ b/src/AcDream.App/UI/IItemListDragHandler.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Visual result of a drag-over query. Retail handlers can consume a drag-over /// message without setting either accept or reject art; shortcut aliases over a /// physical item list use that neutral path. /// -internal enum ItemDragAcceptance +public enum ItemDragAcceptance { None, Accept, @@ -18,13 +18,13 @@ internal enum ItemDragAcceptance /// (RegisterItemListDragHandler, decomp 230461; confirmed acclient /// 0x004a539e + the gmToolbarUI block 0x004bdd89). /// decides the neutral/accept/reject overlay only (advisory). -/// is authoritative — it performs the action, or +/// is authoritative — it performs the action, or /// no-ops to reject. /// -internal interface IItemListDragHandler +public interface IItemListDragHandler { - /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag - /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its + /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag + /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its /// model + wire so the source slot empties immediately. The item is "in hand" until /// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel. void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload); diff --git a/src/AcDream.App/UI/IRetainedPanelController.cs b/src/AcDream.App/UI/IRetainedPanelController.cs index d75d0d33..4ea2ae40 100644 --- a/src/AcDream.App/UI/IRetainedPanelController.cs +++ b/src/AcDream.App/UI/IRetainedPanelController.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -7,7 +7,7 @@ namespace AcDream.App.UI; /// Implementations own panel-specific subscriptions and reactions; the window /// manager owns visibility, focus, capture, geometry, and teardown ordering. /// -internal interface IRetainedPanelController : IDisposable +public interface IRetainedPanelController : IDisposable { /// Called once for each hidden-to-shown transition. void OnShown() { } diff --git a/src/AcDream.App/UI/IRetainedWindowStateController.cs b/src/AcDream.App/UI/IRetainedWindowStateController.cs index 6ed1d086..cdb129bc 100644 --- a/src/AcDream.App/UI/IRetainedWindowStateController.cs +++ b/src/AcDream.App/UI/IRetainedWindowStateController.cs @@ -1,7 +1,7 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Panel state that is not completely described by outer-frame bounds. -internal readonly record struct RetainedWindowState( +public readonly record struct RetainedWindowState( bool Collapsed = false, bool Maximized = false, float? PersistedTop = null, @@ -11,7 +11,7 @@ internal readonly record struct RetainedWindowState( /// Optional state seam used by retained-window persistence. Bounds are restored /// first; the controller then reapplies collapsed/maximized presentation. /// -internal interface IRetainedWindowStateController +public interface IRetainedWindowStateController { RetainedWindowState CaptureWindowState(); void RestoreWindowState(RetainedWindowState state); diff --git a/src/AcDream.App/UI/IUiDatStateful.cs b/src/AcDream.App/UI/IUiDatStateful.cs index 6fb9768a..7a86e9b8 100644 --- a/src/AcDream.App/UI/IUiDatStateful.cs +++ b/src/AcDream.App/UI/IUiDatStateful.cs @@ -1,16 +1,16 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Narrow numeric-state bridge for widgets imported from retail LayoutDesc data. /// Controllers use retail state ids without knowing how names/media are stored. /// -internal interface IUiDatStateful +public interface IUiDatStateful { uint ActiveRetailStateId { get; } bool TrySetRetailState(uint stateId); } -internal static class RetailUiStateIds +public static class RetailUiStateIds { public const uint Closed = 11u; public const uint Open = 12u; diff --git a/src/AcDream.App/UI/IUiGlobalTimeListener.cs b/src/AcDream.App/UI/IUiGlobalTimeListener.cs index 54d5a3f1..36680887 100644 --- a/src/AcDream.App/UI/IUiGlobalTimeListener.cs +++ b/src/AcDream.App/UI/IUiGlobalTimeListener.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Opt-in recipient of retail UI global message 3, broadcast once per UI frame /// after tooltip deadline processing. /// -internal interface IUiGlobalTimeListener +public interface IUiGlobalTimeListener { void OnGlobalUiTime(double nowSeconds); } diff --git a/src/AcDream.App/UI/IUiViewportRenderer.cs b/src/AcDream.App/UI/IUiViewportRenderer.cs index 0a2a182b..13023078 100644 --- a/src/AcDream.App/UI/IUiViewportRenderer.cs +++ b/src/AcDream.App/UI/IUiViewportRenderer.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Renders a 3-D mini-scene into an off-screen buffer and returns the GL color-texture /// handle. Called by the per-frame pre-UI hook (GameWindow), NOT from UiViewport.OnDraw. Implemented /// by PaperdollViewportRenderer in AcDream.App.Rendering. Intra-App decoupling so the UI widget /// doesn't depend on WbDrawDispatcher/GameWindow. -internal interface IUiViewportRenderer +public interface IUiViewportRenderer { /// Render at (width,height); return the color-texture GL handle, or 0 if nothing rendered. uint Render(int width, int height); diff --git a/src/AcDream.App/UI/IconComposer.cs b/src/AcDream.App/UI/IconComposer.cs index 196678da..77dacf2b 100644 --- a/src/AcDream.App/UI/IconComposer.cs +++ b/src/AcDream.App/UI/IconComposer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -17,9 +17,9 @@ namespace AcDream.App.UI; /// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded /// DIRECTLY (the D.2b RenderSurface-vs-Surface rule). /// -/// Layer order (bottom → top), matching retail: +/// Layer order (bottom → top), matching retail: /// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from -/// the portal MasterMap) — +/// the portal MasterMap) — /// 2. item custom underlay (e.g. "magic" tint strip) /// 3. base icon /// 4. item custom overlay (e.g. "enchanted" sparkle) @@ -31,28 +31,28 @@ namespace AcDream.App.UI; /// /// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple. /// -internal sealed class IconComposer +public sealed class IconComposer { private readonly IDatReaderWriter _dats; private readonly TextureCache _cache; - private readonly Dictionary<(uint, uint, uint, uint, uint), GpuTextureSlot> _byTuple = new(); + private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new(); private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new(); - private readonly Dictionary _spellIcons = new(); - private readonly Dictionary _componentIcons = new(); + private readonly Dictionary _spellIcons = new(); + private readonly Dictionary _componentIcons = new(); - private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, GpuTextureSlot Texture); + private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture); - // ── type-default underlay resolve (EnumIDMap 0x10000004) ───────────────── - // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008). - // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21. - // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940. + // ── type-default underlay resolve (EnumIDMap 0x10000004) ───────────────── + // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008). + // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21. + // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940. private EnumIDMap? _underlaySubMap; private bool _underlayResolveTried; private readonly Dictionary _underlayDidByIndex = new(); - // ── effect overlay resolve (EnumIDMap 0x10000005) ──────────────────────── - // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009). - // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21. + // ── effect overlay resolve (EnumIDMap 0x10000005) ──────────────────────── + // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009). + // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21. // Refs: IconData::RenderIcons 0x0058d180 (effect path); the effect tile is a // ReplaceColor tint SOURCE, not a blit layer (see RESOLVED doc, divergence DR-1). private EnumIDMap? _effectSubMap; @@ -68,13 +68,13 @@ internal sealed class IconComposer /// /// Resolve the type-default underlay DID for via the - /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c + + /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c + /// DBCache::GetDIDFromEnum 0x413940). /// /// index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set. /// /// NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case - /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that + /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that /// path is out of scope here (paperdoll phase). /// internal uint ResolveUnderlayDid(ItemType itemType) @@ -97,7 +97,7 @@ internal sealed class IconComposer uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; - if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008 + if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008 if (_dats.Portal.TryGet(subDid, out var sub)) _underlaySubMap = sub; } @@ -105,7 +105,7 @@ internal sealed class IconComposer /// Resolve the effect-overlay DID for via the EnumIDMap /// 0x10000005 chain. index = LowestSetBit(effects)+1; if the entry is missing/zero, /// retail falls back to index 0x21 (the solid-black tile). NOTE: the effect path has - /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss → + /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss → /// fallback. (Retail IconData::RenderIcons 0x0058d180.) /// internal uint ResolveEffectDid(uint effects) @@ -129,16 +129,16 @@ internal sealed class IconComposer uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000 if (masterDid == 0) return; if (!_dats.Portal.TryGet(masterDid, out var master)) return; - if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009 + if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009 if (_dats.Portal.TryGet(subDid, out var sub)) _effectSubMap = sub; } /// /// Retail SurfaceWindow::ReplaceColor SURFACE overload (0x004415b0): for every - /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) → + /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) → /// 0xFFFFFFFF), copy the SAME (x,y) pixel from the source effect tile. This preserves /// the effect tile's texture/gradient (NOT a flat color). Retail requires the source to - /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged. + /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged. /// Mutates in place. /// internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh) @@ -160,7 +160,7 @@ internal sealed class IconComposer /// /// The decoded effect tile for (enum 0x10000005). The tile is /// a 32x32 textured RenderSurface whose pixels ARE the per-effect coloring (blue=Magical, - /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into + /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into /// the icon's white pixels (gradient), so we need the whole tile, not a representative /// color. Cached per DID. /// @@ -224,27 +224,27 @@ internal sealed class IconComposer /// effects==0 resolves to the 0x21 solid-black fallback tile, so pure-white pixels become /// black (matching retail); magical items take the per-effect hue instead. /// - public GpuTextureSlot GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) + public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) { - if (iconId == 0) return GpuTextureSlot.Unassigned; + if (iconId == 0) return 0; uint typeUnderlayDid = ResolveUnderlayDid(itemType); var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects); if (_byTuple.TryGetValue(key, out var tex)) return tex; - // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor. + // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor. // RenderIcons retains this as a distinct Graphic because the cursor ghost must not // carry the type/custom underlay that fills an inventory cell. ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects); - // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag. + // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag. var layers = new List<(byte[] rgba, int w, int h)>(); AddLayer(layers, typeUnderlayDid); AddLayer(layers, underlayId); if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height)); - if (layers.Count == 0) return GpuTextureSlot.Unassigned; + if (layers.Count == 0) return 0; var (rgba, w, h) = Compose(layers); - GpuTextureSlot handle = _cache.UploadRgba8(rgba, w, h, nearest: true); + uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true); _byTuple[key] = handle; return handle; } @@ -255,15 +255,13 @@ internal sealed class IconComposer /// UIElement_ItemList::PrepareDragIcon obtains exactly this graphic through /// ACCWeenieObject::GetDragIcon (0x004e2a50 / 0x0058d180). /// - public GpuTextureSlot GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) + public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects) { // itemType/underlayId are deliberately unused: keeping the resolver signature identical // to GetIcon lets every item-panel binding request the two retail siblings from one model. _ = itemType; _ = underlayId; - return iconId == 0 - ? GpuTextureSlot.Unassigned - : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? GpuTextureSlot.Unassigned; + return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u; } private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects) @@ -277,11 +275,11 @@ internal sealed class IconComposer if (dragLayers.Count == 0) return null; var composed = Compose(dragLayers); - // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180): + // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180): // the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null // even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons // calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying - // the textured effect tile per-pixel into the icon's pure-white pixels — so + // the textured effect tile per-pixel into the icon's pure-white pixels — so // magical items take the tile's GRADIENT hue and mundane items go solid black. // (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a // gradient, not a flat tint, and the no-mana scroll's edges are black.) @@ -289,7 +287,7 @@ internal sealed class IconComposer ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h, tile.Rgba8, tile.Width, tile.Height); - GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); + uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture); _dragByTuple[key] = created; return created; @@ -309,12 +307,12 @@ internal sealed class IconComposer /// Retail ClientMagicSystem::CompositeSpellIcon (0x00567550): power-level /// backing, spell art, reversed/normal recolor, then self/fellow overlay. /// - public GpuTextureSlot GetSpellIcon(uint spellId) + public uint GetSpellIcon(uint spellId) { - if (_spellIcons.TryGetValue(spellId, out GpuTextureSlot cached)) return cached; + if (_spellIcons.TryGetValue(spellId, out uint cached)) return cached; DatReaderWriter.DBObjs.SpellTable? table = _dats.Get(0x0E00000Eu); - if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return GpuTextureSlot.Unassigned; + if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return 0u; uint power = spell.Components.Count == 0 ? 0u @@ -323,7 +321,7 @@ internal sealed class IconComposer var layers = new List<(byte[] rgba, int w, int h)>(); AddLayer(layers, powerBacking); AddLayer(layers, spell.Icon); - if (layers.Count == 0) return GpuTextureSlot.Unassigned; + if (layers.Count == 0) return 0u; var composed = Compose(layers); uint tintIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.Reversed) != 0 @@ -346,7 +344,7 @@ internal sealed class IconComposer (overlay.Rgba8, overlay.Width, overlay.Height)]); } - GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); + uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); _spellIcons[spellId] = texture; return texture; } @@ -355,11 +353,11 @@ internal sealed class IconComposer /// Retail ClientMagicSystem::CompositeSpellComponentIcon (0x00567720). /// Components use their raw DAT art with pure white replaced by black. /// - public GpuTextureSlot GetSpellComponentIcon(uint iconId) + public uint GetSpellComponentIcon(uint iconId) { - if (iconId == 0u) return GpuTextureSlot.Unassigned; - if (_componentIcons.TryGetValue(iconId, out GpuTextureSlot cached)) return cached; - if (!TryDecode(iconId, out DecodedTexture icon)) return GpuTextureSlot.Unassigned; + if (iconId == 0u) return 0u; + if (_componentIcons.TryGetValue(iconId, out uint cached)) return cached; + if (!TryDecode(iconId, out DecodedTexture icon)) return 0u; byte[] rgba = (byte[])icon.Rgba8.Clone(); for (int i = 0; i + 3 < rgba.Length; i += 4) { @@ -367,7 +365,7 @@ internal sealed class IconComposer continue; rgba[i] = rgba[i + 1] = rgba[i + 2] = 0; } - GpuTextureSlot texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true); + uint texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true); _componentIcons[iconId] = texture; return texture; } diff --git a/src/AcDream.App/UI/ItemDragPayload.cs b/src/AcDream.App/UI/ItemDragPayload.cs index eb0ff2c1..f4f93234 100644 --- a/src/AcDream.App/UI/ItemDragPayload.cs +++ b/src/AcDream.App/UI/ItemDragPayload.cs @@ -1,25 +1,25 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.UI; /// -/// Where a dragged item came from — the retail InqDropIconInfo flag +/// Where a dragged item came from — the retail InqDropIconInfo flag /// distinction (flags & 0xE == 0 fresh-from-inventory vs /// flags & 4 within-list reorder) expressed as a typed enum. The drop /// handler maps SourceKind + target back to the fresh-vs-reorder decision. /// Decomp anchors: gmToolbarUI 0x004bd162 / 0x004bd1af; InqDropIconInfo 230533. /// -internal enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground } +public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground } /// /// Snapshot of a drag-in-progress, taken at drag-begin (so a server move arriving /// mid-drag can't mutate it under us). Port of retail's m_dragElement + /// InqDropIconInfo out-params (objId/container/flags, decomp 230533). /// SourceContainer is intentionally NOT stored: the handler resolves the -/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the +/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the /// same container id retail reads off the dragged element, single source of truth. /// -internal sealed record ItemDragPayload( +public sealed record ItemDragPayload( uint ObjId, // dragged weenie guid (retail itemID, +0x5FC) ItemDragSource SourceKind, // what kind of slot it left int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged) diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs index 68f6cd0e..db968d08 100644 --- a/src/AcDream.App/UI/ItemInteractionController.cs +++ b/src/AcDream.App/UI/ItemInteractionController.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Runtime.Gameplay; @@ -6,14 +6,14 @@ using AcDream.Runtime.Gameplay; namespace AcDream.App.UI; /// Result of offering a primary click to active item-target mode. -internal enum ItemPrimaryClickResult +public enum ItemPrimaryClickResult { NotActive, ConsumedSuccess, ConsumedRejected, } -internal readonly record struct PendingBackpackPlacement( +public readonly record struct PendingBackpackPlacement( ulong Token, uint ItemId, uint ContainerId, @@ -25,7 +25,7 @@ internal readonly record struct PendingBackpackPlacement( /// target acquisition, and drag-out drops here instead of duplicating /// ItemHolder::UseObject fragments in each panel. /// -internal sealed class ItemInteractionController : IDisposable +public sealed class ItemInteractionController : IDisposable { internal const string InventoryRequestBusyMessage = "You can only move or use one item at a time"; @@ -1230,7 +1230,7 @@ internal sealed class ItemInteractionController : IDisposable failures); } - internal readonly record struct AppraisalResponseAcceptance( + public readonly record struct AppraisalResponseAcceptance( bool Accepted, bool FirstResponse); @@ -1308,7 +1308,7 @@ internal sealed class ItemInteractionController : IDisposable { var target = _objects.Get(targetGuid); if (target is null) - return false; // retail: GetWeenieObject(target) null → incompatible + return false; // retail: GetWeenieObject(target) null → incompatible return ItemInteractionPolicy.IsTargetCompatible( Snapshot(source), Snapshot(target), _playerGuid()); } diff --git a/src/AcDream.App/UI/Layout/AppraisalUiController.cs b/src/AcDream.App/UI/Layout/AppraisalUiController.cs index a2d2d0de..a3052bd7 100644 --- a/src/AcDream.App/UI/Layout/AppraisalUiController.cs +++ b/src/AcDream.App/UI/Layout/AppraisalUiController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.App.Spells; using AcDream.Core.Combat; @@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout; /// appraisal response/subview lifecycle; the imported LayoutDesc owns all /// chrome, geometry, fonts, and scrollbars. /// -internal sealed class AppraisalUiController : IRetainedPanelController +public sealed class AppraisalUiController : IRetainedPanelController { public const uint LayoutId = 0x2100006Bu; public const uint RootId = 0x100005F2u; @@ -73,8 +73,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates; private readonly CreatureDisplayNameResolver _creatureNames; private readonly RetailAppraisalNameResolver _itemNames; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveComponentIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveComponentIcon; private readonly Func> _spellComponents; private readonly Func _magicSkill; private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates; @@ -128,8 +128,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController CreatureAppraisalRowTemplateFactory? creatureRowTemplates, CreatureDisplayNameResolver? creatureNames, RetailAppraisalNameResolver? itemNames, - Func? resolveSpellIcon, - Func? resolveComponentIcon, + Func? resolveSpellIcon, + Func? resolveComponentIcon, Func>? spellComponents, Func? magicSkill, SpellExamineComponentTemplateFactory? spellComponentTemplates) @@ -155,8 +155,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController ?? new CreatureDisplayNameResolver( new Dictionary()); _itemNames = itemNames ?? RetailAppraisalNameResolver.Empty; - _resolveSpellIcon = resolveSpellIcon ?? (_ => GpuTextureSlot.Unassigned); - _resolveComponentIcon = resolveComponentIcon ?? (_ => GpuTextureSlot.Unassigned); + _resolveSpellIcon = resolveSpellIcon ?? (_ => 0u); + _resolveComponentIcon = resolveComponentIcon ?? (_ => 0u); _spellComponents = spellComponents ?? (_ => []); _magicSkill = magicSkill ?? (_ => 0u); _spellComponentTemplates = spellComponentTemplates; @@ -283,8 +283,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null, CreatureDisplayNameResolver? creatureNames = null, RetailAppraisalNameResolver? itemNames = null, - Func? resolveSpellIcon = null, - Func? resolveComponentIcon = null, + Func? resolveSpellIcon = null, + Func? resolveComponentIcon = null, Func>? spellComponents = null, Func? magicSkill = null, SpellExamineComponentTemplateFactory? spellComponentTemplates = null) @@ -496,7 +496,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController _characterObjectId = 0; _spellId = 0u; _refreshElapsed = 0; - _spellIcon.Texture = GpuTextureSlot.Unassigned; + _spellIcon.Texture = 0u; SetSpellText(_spellSchool, string.Empty); SetSpellText(_spellMana, string.Empty); SetSpellText(_spellDuration, string.Empty); @@ -1037,7 +1037,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController } } -internal enum AppraisalView +public enum AppraisalView { Item, Creature, diff --git a/src/AcDream.App/UI/Layout/CharacterController.cs b/src/AcDream.App/UI/Layout/CharacterController.cs index 1074c83d..6c60dc15 100644 --- a/src/AcDream.App/UI/Layout/CharacterController.cs +++ b/src/AcDream.App/UI/Layout/CharacterController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.Core.Items; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// localized report into m_pMainText; it is not a second character /// sheet and therefore does not repeat name, level, title, or current vitals. /// -internal static class CharacterController +public static class CharacterController { public const uint LayoutId = 0x2100006Eu; public const uint RootId = 0x10000183u; @@ -343,7 +343,7 @@ internal static class CharacterController /// Event-invalidated report owner for retail's character-information text. /// Stable frames borrow the same shaped line collection. /// -internal sealed class CharacterInformationUiController : IRetainedPanelController +public sealed class CharacterInformationUiController : IRetainedPanelController { private readonly Func _data; private readonly CharacterInfoStrings _strings; @@ -402,7 +402,7 @@ internal sealed class CharacterInformationUiController : IRetainedPanelControlle } } -internal sealed record CharacterInfoStrings( +public sealed record CharacterInfoStrings( string[] Birth, string[] Played, string DeathsNone, diff --git a/src/AcDream.App/UI/Layout/CharacterSheet.cs b/src/AcDream.App/UI/Layout/CharacterSheet.cs index 94a78e12..2da63597 100644 --- a/src/AcDream.App/UI/Layout/CharacterSheet.cs +++ b/src/AcDream.App/UI/Layout/CharacterSheet.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI.Layout; @@ -17,9 +17,9 @@ namespace AcDream.App.UI.Layout; /// UpdateAugmentations (0x004b9000), and /// UpdateLoad (0x004b8a20). /// -internal sealed class CharacterSheet +public sealed class CharacterSheet { - // ── Identity ────────────────────────────────────────────────────────────── + // ── Identity ────────────────────────────────────────────────────────────── /// Character name (first line of the report). public string Name { get; init; } = string.Empty; @@ -39,8 +39,8 @@ internal sealed class CharacterSheet /// Title string, e.g. "the Adventurer". Null = omit. public string? Title { get; init; } - // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70, - // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ────────── + // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70, + // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ────────── /// Total accrued experience (retail PropertyInt64 1). Header value /// element 0x10000235 (m_pTotalXPText). @@ -50,7 +50,7 @@ internal sealed class CharacterSheet /// 0x10000238 (m_pXPToLevelText). public long XpToNextLevel { get; init; } - /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)). + /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)). /// Drives the header meter 0x10000236 (m_pXPToLevelMeter). public float XpFraction { get; init; } @@ -58,13 +58,13 @@ internal sealed class CharacterSheet /// 0x10000233 (m_pPKStatusText). Null = omit. public string? PkStatus { get; init; } - // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ───────── + // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ───────── - /// Formatted birth date string (retail InqInt(0x62) → strftime). + /// Formatted birth date string (retail InqInt(0x62) → strftime). /// Null = omit the birth line. public string? BirthDate { get; init; } - /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration). + /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration). /// Null = omit the age line. public string? PlayTime { get; init; } @@ -77,7 +77,7 @@ internal sealed class CharacterSheet /// Raw retail PropertyInt 0x7D seconds. Null means the quality was absent. public int? TotalPlayTimeSeconds { get; init; } - // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ───────────────────────────── + // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ───────────────────────────── public int HealthCurrent { get; init; } public int HealthMax { get; init; } @@ -86,7 +86,7 @@ internal sealed class CharacterSheet public int ManaCurrent { get; init; } public int ManaMax { get; init; } - // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ──────────── + // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ──────────── // InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self. public int Strength { get; init; } @@ -96,9 +96,9 @@ internal sealed class CharacterSheet public int Focus { get; init; } public int Self { get; init; } - // ── Skills (UpdateFakeSkills 0x004b8930) ──────────────────────────────── + // ── Skills (UpdateFakeSkills 0x004b8930) ──────────────────────────────── // Character Information uses 0xB5/0xC0 for Chess/Fishing; skill credits use 0x18. - // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab. + // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab. public int UnspentSkillCredits { get; init; } public int SpecializedSkillCredits { get; init; } @@ -111,21 +111,21 @@ internal sealed class CharacterSheet /// /// Available (unspent) skill credits shown in the Attributes tab footer State-A. - /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0). + /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0). /// Element 0x10000243 (footer line-1 value in the studio's 3-line layout). /// public int SkillCredits { get; init; } /// /// Unassigned (banked) experience points. - /// Retail InqInt64(2) — shown in footer line-2 in State-A display. + /// Retail InqInt64(2) — shown in footer line-2 in State-A display. /// Element 0x10000245 (footer line-2 value). /// public long UnassignedXp { get; init; } - // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ── - // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent. - // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent. + // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ── + // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent. + // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent. // Cost 0 means the attribute is at max or not trainable. Ordered to match AttrRows: // Strength, Endurance, Coordination, Quickness, Focus, Self, Health, Stamina, Mana. // Source: gmAttributeUI::GetCostToRaise/GetCostToRaise10 (0x0049cb80/0x0049cc70). @@ -150,7 +150,7 @@ internal sealed class CharacterSheet /// public IReadOnlyList Skills { get; init; } = Array.Empty(); - // ── Augmentations (UpdateAugmentations 0x004b9000) ───────────────────── + // ── Augmentations (UpdateAugmentations 0x004b9000) ───────────────────── // Retail InqInt(0x162) = AugmentationStat; string-switch 1..0xb. /// Augmentation name from the switch in UpdateAugmentations (0x004b9000), @@ -165,7 +165,7 @@ internal sealed class CharacterSheet public IReadOnlyDictionary CharacterInfoProperties { get; init; } = new Dictionary(); - // ── Burden / load (UpdateLoad 0x004b8a20) ─────────────────────────────── + // ── Burden / load (UpdateLoad 0x004b8a20) ─────────────────────────────── // Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance). public int BurdenCurrent { get; init; } @@ -173,7 +173,7 @@ internal sealed class CharacterSheet public int EncumbranceAugmentations { get; init; } } -internal enum CharacterSkillAdvancementClass +public enum CharacterSkillAdvancementClass { Inactive = 0, Untrained = 1, @@ -181,7 +181,7 @@ internal enum CharacterSkillAdvancementClass Specialized = 3, } -internal sealed record CharacterSkill( +public sealed record CharacterSkill( uint Id, string Name, uint IconDid, diff --git a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs index 56e05442..dc390f97 100644 --- a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs +++ b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Core.Player; @@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout; /// cited there too (gmAttributeUI::GetCostToRaise 0x0049cb80 family). /// /// State ownership: optimistic debits go through the owning -/// store's eventful APIs — / +/// store's eventful APIs — / /// (fires ObjectUpdated) /// when the player object is in the table, else /// / @@ -26,9 +26,9 @@ namespace AcDream.App.UI.Layout; /// Never write the raw property dictionaries from UI code. The next server /// snapshot remains authoritative over every optimistic value. /// -internal sealed class CharacterSheetProvider +public sealed class CharacterSheetProvider { - /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp. + /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp. private const uint UnassignedXpPropertyId = 2u; /// @@ -48,10 +48,10 @@ internal sealed class CharacterSheetProvider private readonly Action? _sendRaiseSkill; private readonly Action? _sendTrainSkill; - /// Portal SkillTable (0x0E000004) — set by the host once dats load. + /// Portal SkillTable (0x0E000004) — set by the host once dats load. public DatReaderWriter.DBObjs.SkillTable? SkillTable { get; set; } - /// Portal ExperienceTable (0x0E000018) — set by the host once dats load. + /// Portal ExperienceTable (0x0E000018) — set by the host once dats load. public DatReaderWriter.DBObjs.ExperienceTable? ExperienceTable { get; set; } public CharacterSheetProvider( @@ -89,7 +89,7 @@ internal sealed class CharacterSheetProvider return new ChangeBinding(this, changed); } - // ── Sheet assembly ───────────────────────────────────────────────────── + // ── Sheet assembly ───────────────────────────────────────────────────── /// Best display name: active toon key, else the live object's name, else "Player". public string CharacterName() @@ -240,8 +240,8 @@ internal sealed class CharacterSheetProvider /// /// Load the portal ExperienceTable (0x0E000018), falling back to a - /// type scan for older or odd dat collections. Failures are logged — - /// never silently swallowed — and leave raise costs unavailable (0). + /// type scan for older or odd dat collections. Failures are logged — + /// never silently swallowed — and leave raise costs unavailable (0). /// public static DatReaderWriter.DBObjs.ExperienceTable? LoadExperienceTable( IDatReaderWriter dats, Action? log = null) @@ -275,7 +275,7 @@ internal sealed class CharacterSheetProvider } /// XP still needed for the next level + fill fraction of the - /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction). + /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction). private (long toNext, float fraction) ComputeLevelXp(int level, long totalXp) { var levels = ExperienceTable?.Levels; @@ -394,7 +394,7 @@ internal sealed class CharacterSheetProvider } /// Cost to advance ranks along a retail - /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the + /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the /// curve end (retail GetCostToRaise/GetCostToRaise10 0x0049cb80/0x0049cc70). private static long RaiseCostFromXpCurve(uint[]? curve, uint ranks, uint spentXp, int amount) { @@ -427,13 +427,13 @@ internal sealed class CharacterSheetProvider private int VitalMax(LocalPlayerState.VitalKind kind) => _localPlayer.GetMaxApprox(kind) is { } max ? checked((int)Math.Min(int.MaxValue, max)) : 0; - // ── Raise-request flow ───────────────────────────────────────────────── + // ── Raise-request flow ───────────────────────────────────────────────── /// /// Send a raise/train action to the server and, when a send delegate /// fired, optimistically apply the local effect so the sheet stays /// current during the round trip. The next server snapshot remains - /// authoritative (a rejected raise is corrected by the property echo — + /// authoritative (a rejected raise is corrected by the property echo — /// pending/rollback ledger tracked as a follow-up issue). /// public void HandleRaiseRequest(CharacterStatController.RaiseRequest request) diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 6bfba90a..4ac8f737 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; @@ -6,14 +6,14 @@ using AcDream.App.UI; namespace AcDream.App.UI.Layout; /// -/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E, +/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E, /// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root /// type 0x1000002A) which in turn chains into the gmStatManagementUI header content. /// /// Unlike (which targets the SEPARATE text-report /// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element), /// this controller binds the real, statically-mounted header + list elements that the -/// importer already produces — every id below is confirmed present via +/// importer already produces — every id below is confirmed present via /// . /// /// Ported from gmStatManagementUI::UpdateCharacterInfo (0x004f0770) + @@ -23,7 +23,7 @@ namespace AcDream.App.UI.Layout; /// gmAttributeUI::PostInit (0x0049db70) + AttributeInfoRegion / Attribute2ndInfoRegion /// (0x004f1910 / 0x004f19e0). Row icons loaded via sub-element 0x10000129 in the retail /// dat template (each icon is a 0x06xxxxxx RenderSurface DataID from SubMap -/// 0x25000006 / 0x25000007, spec §2). +/// 0x25000006 / 0x25000007, spec §2). /// /// Footer State A (nothing selected) bound from /// DisplayDefaultFooter (0x0049cde0): title empty, line-1 value = @@ -39,13 +39,13 @@ namespace AcDream.App.UI.Layout; /// UIStateId.Closed (0x0B). The imported Type-12 tab owns its authored font color and /// propagates the state to its three chrome children through PassToChildren. /// -/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable +/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable /// (UIStateId.Normal, 0x01), state "Ghosted" = unaffordable or no selection /// (UIStateId.Ghosted, 0x0D). Source: gmAttributeUI::AttributeInfoRegion::Update (0x004f1910). /// -internal static class CharacterStatController +public static class CharacterStatController { - // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ── + // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ── public const uint NameId = 0x10000231u; // m_pNameText public const uint HeritageId = 0x10000232u; // m_pHeritageText public const uint PkStatusId = 0x10000233u; // m_pPKStatusText @@ -64,16 +64,16 @@ internal static class CharacterStatController public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer - // ── Footer STATE-A container id ────────────────────────────────────────── + // ── Footer STATE-A container id ────────────────────────────────────────── // 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row, - // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider + // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider // label widths (195px vs 145px in State B). _byId stores the LAST duplicate, which - // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there. + // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there. public const uint FooterStateAId = 0x10000240u; // State-A footer container (nothing selected) public const uint FooterStateBId = 0x10000241u; // State-B footer container (row selected) public const uint FooterStateCId = 0x10000247u; // State-C footer container (hide inactive) - // ── Tab bar element ids (LayoutDesc 0x2100002E root) ──────────────────── + // ── Tab bar element ids (LayoutDesc 0x2100002E root) ──────────────────── // These are imported Type-12 UIElement_Text tabs. Their Closed/Open states carry // the caption color and PassToChildren=true; their three retained children carry // the authored left/center/right chrome. Character and Spellbook therefore share @@ -89,7 +89,7 @@ internal static class CharacterStatController public const uint SkillsPageId = 0x1000022Cu; public const uint TitlesPageId = 0x10000539u; - // ── Footer element ids (gmStatManagementUI struct fields) ──────────────── + // ── Footer element ids (gmStatManagementUI struct fields) ──────────────── // Source: acclient.h / DisplayDefaultFooter (0x0049cde0) public const uint FooterTitleId = 0x1000024eu; // GetFooterTitleLabel public const uint FooterLine1Label = 0x10000242u; // GetFooterLineOneLabel @@ -97,26 +97,26 @@ internal static class CharacterStatController public const uint FooterLine2Label = 0x10000244u; // GetFooterLineTwoLabel public const uint FooterLine2Value = 0x10000245u; // GetFooterLineTwoValue - // ── Raise button element ids ────────────────────────────────────────────── + // ── Raise button element ids ────────────────────────────────────────────── // Source: gmAttributeUI::PostInit (0x0049db70); CM_Train::Event_TrainAttribute. // Button state "Normal" (UIStateId 0x01) = affordable (green/active); // "Ghosted" (UIStateId 0x0D) = disabled. Hidden when nothing is selected. - public const uint RaiseOneId = 0x10000246u; // raise × 1 - public const uint RaiseTenId = 0x100005EBu; // raise × 10 + public const uint RaiseOneId = 0x10000246u; // raise × 1 + public const uint RaiseTenId = 0x100005EBu; // raise × 10 private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold - /// Row highlight color — semi-translucent gold, matches retail + /// Row highlight color — semi-translucent gold, matches retail /// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent. private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f); private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f); - // ── Row layout constants ───────────────────────────────────────────────── + // ── Row layout constants ───────────────────────────────────────────────── // RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height // and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side. // The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which - // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height". + // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height". private const float RowHeight = 22f; private const float IconSize = 16f; private const float RowPadX = 4f; @@ -146,7 +146,7 @@ internal static class CharacterStatController Skills, } - internal enum RaiseTargetKind + public enum RaiseTargetKind { Attribute, Vital, @@ -154,7 +154,7 @@ internal static class CharacterStatController TrainSkill, } - internal readonly record struct RaiseRequest( + public readonly record struct RaiseRequest( RaiseTargetKind Kind, uint StatId, long Cost, @@ -170,7 +170,7 @@ internal static class CharacterStatController private sealed record SkillRowBinding(UiClickablePanel Panel, CharacterSkill Skill); - // ── Attribute row descriptors — retail display order per spec §1 ───────── + // ── Attribute row descriptors — retail display order per spec §1 ───────── private static readonly (string name, uint iconDid, uint statId)[] AttrRows = new[] { ("Strength", 0x060002C8u, 1u), @@ -210,7 +210,7 @@ internal static class CharacterStatController Func data, UiDatFont? datFont = null, UiDatFont? rowDatFont = null, - Func? spriteResolve = null, + Func? spriteResolve = null, RaiseRequestHandler? onRaiseRequest = null, Action? onClose = null) { @@ -231,35 +231,35 @@ internal static class CharacterStatController UiElement? contentPage = FindDirectChildById(layout.Root, AttributesPageId); // Name (18px from dat FontDid), Heritage (14px), PkStatus (14px): - // Fix C: pass null → Label's null-guard keeps the build-time dat font. + // Fix C: pass null → Label's null-guard keeps the build-time dat font. // Controllers still own the text color and the LinesProvider. - // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26). + // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26). Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name); Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data())); Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty); - // ── Header captions (new — retail labels above/left of each number) ────── - // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font. + // ── Header captions (new — retail labels above/left of each number) ────── + // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font. LabelTwoLine(layout, contentPage, LevelCaptionId, null, Body, "Character", "Level"); - // Level number: retail renders this as large gold centered text in the 65×50 element. + // Level number: retail renders this as large gold centered text in the 65×50 element. // Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build // time when the font resolver is provided (studio path). We no longer force rowDatFont - // here for the level — the dat's own FontDid drives the font. The Gold color is still + // here for the level — the dat's own FontDid drives the font. The Gold color is still // set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in // BuildAttributeRows) continue to use datFont directly since they have no dat origin. - // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770. + // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770. // runtime color, dat carries none. Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString()); - // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font. + // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font. LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):"); LabelRight(layout, contentPage, TotalXpId, null, Body, () => data().TotalXp.ToString("N0")); // XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70). // Fix 5: child elements 0x10000237 (label) and 0x10000238 (value) are now built by // the LayoutImporter as UiText children of the XP meter (non-Type-3 meter children - // are explicitly built and registered in byId — see LayoutImporter.BuildWidget). + // are explicitly built and registered in byId — see LayoutImporter.BuildWidget). // FindElement now returns them; the controller binds their LinesProvider. // The importer builds them as UiText via DatWidgetFactory.BuildText, applying their // dat-origin HJustify/VJustify/FontDid/FontColor at build time. The controller then @@ -272,21 +272,21 @@ internal static class CharacterStatController // Bind the dat-origin XP label (0x10000237) and value (0x10000238). // These are now real UiText children of the meter (built by the importer). // The retail layout places the caption + value ON TOP of the red bar - // (ref 2026-06-26: "value … with the red fill bar behind it"). - // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1). + // (ref 2026-06-26: "value … with the red fill bar behind it"). + // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1). if (FindTextByDatId(layout, contentPage, XpNextLabelId) is UiText xpLabel) { if (datFont is not null) xpLabel.DatFont = datFont; xpLabel.ClickThrough = true; xpLabel.Centered = false; // left-align (retail: aligns with Total XP label above) xpLabel.RightAligned = false; - xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall + xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall // Item 1: align the XP-next label's left edge to match the TotalXpLabel's // absolute left edge. The XP-next label is a child of the meter (local coords), - // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's + // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's // horizontal offset within the panel (the meter starts to the right of the - // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the + // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the // "XP for next level:" caption left-aligns with "Total Experience (XP):" above). if (FindElementByDatId(layout, contentPage, TotalXpLabelId) is { } totalXpLbl) { @@ -310,7 +310,7 @@ internal static class CharacterStatController // The tab visuals are already retained in the imported LayoutDesc. Controllers // bind only click behavior and the active Open/Closed state below. - // ── Attribute list — 9 rows in list box 0x1000023D ──────────────────── + // ── Attribute list — 9 rows in list box 0x1000023D ──────────────────── // Mutable selected-index box: -1 = nothing selected. // Gather EVERY copy of the raise buttons in the tree. The raise button ids @@ -322,7 +322,7 @@ internal static class CharacterStatController // At bind time the tree includes all three tab pages (the page-visibility pass // runs AFTER this). Collecting from the full tree is safe: once the page- // visibility pass hides the inactive pages their raise buttons are invisible - // regardless of the Visible flag we set here — but the Attributes page's + // regardless of the Visible flag we set here — but the Attributes page's // buttons (which are NOT hidden by the page pass) must be explicitly hidden. var allRaise1 = new List(); var allRaise10 = new List(); @@ -365,7 +365,7 @@ internal static class CharacterStatController foreach (var b in allRaise1) b.Visible = false; foreach (var b in allRaise10) b.Visible = false; - // ── Footer state visibility ─────────────────────────────────────────── + // ── Footer state visibility ─────────────────────────────────────────── // There are THREE footer state groups (A=0x10000240, B=0x10000241, C=0x10000247) // all stacked at the same position within the Attributes page. _byId stores only // the LAST copy of each id; the others live in the VISIBLE Attributes page and must @@ -373,13 +373,13 @@ internal static class CharacterStatController // // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): // All three group elements have DefaultState = StatManagement_Footer_Default - // (0x10000011) — the dat does NOT differentiate them by visibility. The parent + // (0x10000011) — the dat does NOT differentiate them by visibility. The parent // element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren= // true, but each child group also registers all three states (IncFlags=None, - // Media=0) — meaning the state-propagation produces no media change on any group. + // Media=0) — meaning the state-propagation produces no media change on any group. // Retail's gmStatManagementUI uses hardcoded element-id dispatch // (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247 - // for Meter) to access the right group's children at runtime — the groups themselves + // for Meter) to access the right group's children at runtime — the groups themselves // are never hidden/shown via the dat state mechanism. The controller is the correct // and only place for this visibility management. See retail decomp // gmStatManagementUI::GetFooterTitleLabel @0x004f0170. @@ -389,7 +389,7 @@ internal static class CharacterStatController // selected and owns the retail raise buttons; State C stays hidden for now. SetFooterSelected(false); - // ── Footer State A initial binding ──────────────────────────────────── + // ── Footer State A initial binding ──────────────────────────────────── // Walk to the State-A container directly (rather than _byId which returns the // last duplicate) so we get the wider-label copies (195px) for the unselected state. BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel, contentPage); @@ -428,10 +428,10 @@ internal static class CharacterStatController RetailTabBinding.SetClick(titlesTab, null); UpdateTabStates(); - // ── Active-page selection (fixes the dark-overlay) ───────────────────── + // ── Active-page selection (fixes the dark-overlay) ───────────────────── // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): // The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills, - // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no + // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no // visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI // via SetVisible(bool) on the page containers. The controller is the correct // and only place for initial tab-page selection. @@ -565,7 +565,7 @@ internal static class CharacterStatController ImportedLayout layout, UiElement? contentPage, UiElement? statList, - Func? spriteResolve) + Func? spriteResolve) { if (spriteResolve is null) return null; @@ -624,7 +624,7 @@ internal static class CharacterStatController private static void ConfigureSkillScrollbar( UiScrollbar bar, - Func spriteResolve) + Func spriteResolve) { bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); }; bar.TrackSprite = ScrollTrackSprite; @@ -649,12 +649,12 @@ internal static class CharacterStatController : SkillContentWidth; } - // ── 9-row attribute list ───────────────────────────────────────────────── + // ── 9-row attribute list ───────────────────────────────────────────────── private static List BuildAttributeRows( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, Func data, int[] sel, List allRaise1, @@ -736,7 +736,7 @@ internal static class CharacterStatController private static List BuildSkillRows( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, Func data, int[] sel, List allRaise1, @@ -792,7 +792,7 @@ internal static class CharacterStatController private static UiPanel AddSkillHeader( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, float left, float top, float width, @@ -878,14 +878,14 @@ internal static class CharacterStatController : Vector4.One; /// - /// Handles a row click: toggle (same row → deselect), else select new row. + /// Handles a row click: toggle (same row → deselect), else select new row. /// Updates highlight, footer providers, and raise-button state. /// private static void HandleRowClick( int clickedIndex, int[] sel, List rows, - Func? spriteResolve, + Func? spriteResolve, Func data, List allRaise1, List allRaise10) @@ -895,10 +895,10 @@ internal static class CharacterStatController // Log for live test confirmation (user tests selection in the studio). string rowName = GetRowName(newSel); - Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})"); + Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})"); // Update highlight on all rows. - // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars) + // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars) // for the selected row background. When spriteResolve is available, apply the // sprite; otherwise fall back to the translucent gold tint. const uint HighlightSprite = 0x06001397u; @@ -936,7 +936,7 @@ internal static class CharacterStatController int clickedIndex, int[] sel, List rows, - Func? spriteResolve, + Func? spriteResolve, Func data, List allRaise1, List allRaise10) @@ -956,7 +956,7 @@ internal static class CharacterStatController private static void ApplySkillSelectionVisuals( int selectedIndex, IReadOnlyList rows, - Func? spriteResolve) + Func? spriteResolve) { for (int i = 0; i < rows.Count; i++) { @@ -1227,7 +1227,7 @@ internal static class CharacterStatController private static UiClickablePanel AddRow( UiElement list, UiDatFont? datFont, - Func? spriteResolve, + Func? spriteResolve, float left, float top, float width, float height, uint iconDid, string nameText, @@ -1317,7 +1317,7 @@ internal static class CharacterStatController return row; } - // ── Footer — dynamic (State A + State B via sel[]) ──────────────────────── + // ── Footer — dynamic (State A + State B via sel[]) ──────────────────────── /// /// Bind all 5 footer elements with providers that close over : @@ -1361,7 +1361,7 @@ internal static class CharacterStatController // IMPORTANT: The footer state id (0x10000240) appears once per tab-page sub-layout // (Attributes / Skills / Titles). layout._byId stores only the LAST registered copy, // which ends up in the LAST-imported tab page (Titles). The page-visibility pass - // hides the Titles page → the bound footer elements would be invisible. + // hides the Titles page → the bound footer elements would be invisible. // // Fix: find State A/B inside the explicit Attributes page, not via _byId. The // id dictionary stores the last duplicate and can point at a hidden Skills/Titles @@ -1401,19 +1401,19 @@ internal static class CharacterStatController // The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so // without an override the text would center vertically in the 55px box, overlapping // line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the - // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping. - // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered. + // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping. + // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered. // BackgroundSprite cleared: its full-height sprite would cover line-1/line-2. titleEl.BackgroundSprite = 0; titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above) titleEl.OneLine = true; } - // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font. + // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font. // Fix C: the dat has a 20px font for the footer title. Let it drive. if (titleEl is not null) { - // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game). - // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here. + // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game). + // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here. // RightAligned stays false (BuildText default for a Center element). titleEl.ClickThrough = true; titleEl.LinesProvider = () => @@ -1440,7 +1440,7 @@ internal static class CharacterStatController }; } - // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font. + // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font. var l1L = ByPos(20f, 5f, FooterLine1Label); LabelProvider(l1L, null, Body, () => { @@ -1475,7 +1475,7 @@ internal static class CharacterStatController return cost > 0 ? cost.ToString("N0") : "Infinity!"; }); - // Line-2 elements: pass null → keep dat font. + // Line-2 elements: pass null → keep dat font. var l2L = ByPos(37f, 5f, FooterLine2Label); LabelProvider(l2L, null, Body, () => { @@ -1596,7 +1596,7 @@ internal static class CharacterStatController } } - // ── Helpers ────────────────────────────────────────────────────────────── + // ── Helpers ────────────────────────────────────────────────────────────── private static void SetCompatibilityAnchorsAllById( UiElement node, @@ -1707,7 +1707,7 @@ internal static class CharacterStatController /// renders multiple lines oldest-first (top-to-bottom), so /// line 0 = (top) and line 1 = (bottom). /// This replaces the single-line "Character Level" caption which truncated in the 65px element. - /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption). + /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption). private static void LabelTwoLine(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, string line1, string line2) => LabelTwoLine(layout, null, id, datFont, color, line1, line2); @@ -1719,7 +1719,7 @@ internal static class CharacterStatController { // Null = keep whatever the importer (dat FontDid resolver) set at build time. if (datFont is not null) t.DatFont = datFont; - t.Centered = false; // non-Centered → scroll/multi-line path + t.Centered = false; // non-Centered → scroll/multi-line path t.RightAligned = false; t.ClickThrough = true; t.Padding = 1f; @@ -1732,7 +1732,7 @@ internal static class CharacterStatController } /// Left-justified label (for captions that should be left-aligned, not centered). - /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without + /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without /// being clipped by the bottom-pin scroll math (top=Padding, bottom=H-Padding). private static void LabelLeft(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func text) => LabelLeft(layout, null, id, datFont, color, text); @@ -1776,8 +1776,8 @@ internal static class CharacterStatController /// Bind a directly-located widget with a provider. /// Used when the widget was found by subtree walk rather than FindElement. /// Sets Padding = 0 to prevent the scroll-clip from hiding text in small - /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height - /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank. + /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height + /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank. private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func text) { if (t is null) return; @@ -1798,7 +1798,7 @@ internal static class CharacterStatController /// The standard returns only the LAST widget /// registered for a given id; for elements duplicated across tab-page sub-layouts /// (raise buttons, close buttons) we need ALL copies so that visibility changes are - /// reflected in every page — not just the last-mounted one. + /// reflected in every page — not just the last-mounted one. /// /// /// @@ -1810,14 +1810,14 @@ internal static class CharacterStatController /// We therefore walk the tree recursively and collect every whose /// ActiveState reflects the dat default (before our code sets it), which is not a /// reliable discriminator. Instead, we gather ALL instances from - /// the subtree at the known spatial position (bottom of the panel) — but positions can + /// the subtree at the known spatial position (bottom of the panel) — but positions can /// overlap across pages. /// /// /// /// The correct approach: since _byId stores only one instance per id, we use the /// for the canonical id, then do a FULL tree walk - /// to find ADDITIONAL instances that have identical Width×Height to + /// to find ADDITIONAL instances that have identical Width×Height to /// the known button. This works because the three page copies share the same dat template /// and thus the same geometry. Collected via reference-equality guard to avoid duplicates. /// @@ -1833,7 +1833,7 @@ internal static class CharacterStatController _ = layout; // Walk the tree and collect ALL UiButton instances matching the canonical geometry. - // The canonical copy itself will also be found — that's fine; use a HashSet to dedup. + // The canonical copy itself will also be found — that's fine; use a HashSet to dedup. var seen = new HashSet(ReferenceEqualityComparer.Instance); CollectMatchingButtons(node, targetId, seen, result); } diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs index 7d577bf8..c41561dd 100644 --- a/src/AcDream.App/UI/Layout/ChatWindowController.cs +++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -10,7 +10,7 @@ using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.UI.Layout; /// -/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream +/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream /// analogue of retail ChatInterface + gmMainChatUI::PostInit @0x4ce130. /// /// @@ -24,20 +24,20 @@ namespace AcDream.App.UI.Layout; /// and bound in place. /// /// -internal sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController +public sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController { public const uint LayoutId = 0x21000006u; private bool _disposed; // Element ids from chat LayoutDesc 0x21000006 (confirmed in Task D/G1). private const uint RootId = 0x1000000Eu; - private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it) + private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it) private const uint TranscriptPanelId = 0x10000010u; - private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory + private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory private const uint TrackId = 0x10000012u; private const uint InputBarId = 0x10000013u; private const uint MenuId = 0x10000014u; - private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField + private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField private const uint SendId = 0x10000019u; private const uint MaxMinId = 0x1000046Fu; @@ -48,7 +48,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private const uint MenuItemRow = 0x0600124Eu; // item row bg (template 0x1000001E) private const uint MenuItemSelected = 0x0600124Du; // active channel row - // ── Public surface ───────────────────────────────────────────────────── + // ── Public surface ───────────────────────────────────────────────────── /// Root element of the imported layout (the chat window chrome). public UiElement Root { get; private set; } = null!; @@ -71,7 +71,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe public RetailWindowHandle? WindowHandle { get; private set; } public bool IsMaximized => _maximized; - // ── Private state ────────────────────────────────────────────────────── + // ── Private state ────────────────────────────────────────────────────── private ChatChannelKind _activeChannel = ChatChannelKind.Say; @@ -86,7 +86,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private BitmapFont? _cachedTranscriptDebugFont; internal int TranscriptLayoutBuildCount { get; private set; } - // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ── + // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ── private static readonly (string Label, ChatChannelKind? Channel)[] ChannelItems = { @@ -133,7 +133,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe private bool _maximized; private UiButton? _maxMinButton; - // ── Factory ──────────────────────────────────────────────────────────── + // ── Factory ──────────────────────────────────────────────────────────── /// /// Bind an imported chat layout to live behavior. @@ -156,7 +156,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe /// Retail dat font for transcript + input rendering. /// Fallback debug bitmap font (used when /// is null). - /// Dat RenderSurface id → (GL tex handle, px width, px height). + /// Dat RenderSurface id → (GL tex handle, px width, px height). /// Forwarded to and . public static ChatWindowController? Bind( ElementInfo rootInfo, @@ -165,7 +165,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe Func busProvider, UiDatFont? datFont, BitmapFont? debugFont, - Func resolve) + Func resolve) { // Their parent panels must exist as real widgets in the layout tree. var transcriptPanel = layout.FindElement(TranscriptPanelId); @@ -177,14 +177,14 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe Console.WriteLine( $"[D.2b] ChatWindowController.Bind: missing required elements " + $"(input={input is not null}, " + - $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " + + $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " + $"chat window will not be interactive."); return null; } // LayoutDesc 0x21000006 has SEVERAL top-level elements: the gmMainChatUI window // (RootId 0x1000000E) PLUS stray auxiliary elements that are NOT part of the docked - // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the + // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the // talk-focus highlight strip (0x1000001E), and a scroll-button prototype (0x10000526). // LayoutImporter.ImportInfos wraps all top-level elements in a synthetic Type-3 root, // so using layout.Root would render the strays overlapping the real window (the @@ -210,9 +210,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe transcriptPanel.Top = 0f; transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9) - // ── Transcript ─────────────────────────────────────────────────── + // ── Transcript ─────────────────────────────────────────────────── // The factory now builds the Type-12 transcript element (0x10000011) as a UiText. - // Find it in the widget tree and bind the live providers — no remove/add needed. + // Find it in the widget tree and bind the live providers — no remove/add needed. c.Transcript = layout.FindElement(TranscriptId) as UiText ?? throw new InvalidOperationException("chat transcript 0x10000011 not built as UiText"); c.Transcript.DatFont = datFont; @@ -228,7 +228,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm); - // ── Input ──────────────────────────────────────────────────────── + // ── Input ──────────────────────────────────────────────────────── // Editable/selectable/one-line semantics and state sprites came from the // imported property/state bags. The controller supplies runtime services only. c.Input = input; @@ -238,9 +238,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Input.SpriteResolve = resolve; c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel); - // ── Scrollbar — bind the factory-built Type-11 track element ──────── + // ── Scrollbar — bind the factory-built Type-11 track element ──────── // The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar - // directly. Find it, bind it in place — no remove/add needed. + // directly. Find it, bind it in place — no remove/add needed. var track = layout.FindElement(TrackId); if (track is UiScrollbar bar) { @@ -253,7 +253,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Scrollbar = bar; } - // ── Channel menu — bind the factory-built Type-6 UiMenu ────────── + // ── Channel menu — bind the factory-built Type-6 UiMenu ────────── if (layout.FindElement(MenuId) is UiMenu menu) { menu.DatFont = datFont; menu.Font = debugFont; menu.SpriteResolve = resolve; @@ -268,7 +268,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe menu.EnabledProvider = p => p is not ChatChannelKind ch || ChannelAvailable(ch); menu.ButtonLabelProvider = () => ChannelButtonLabel(c._activeChannel); // The widget reports the pick; the controller owns Selected. Only a talk-channel - // payload updates the active channel + highlight — the null-payload specials are + // payload updates the active channel + highlight — the null-payload specials are // deferred no-ops (see the chat re-drive deferred list) and leave selection intact. menu.OnSelect = p => { @@ -277,18 +277,18 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe c.Menu = menu; } - // ── Send button — Enter-alternate submit trigger ────────────────── + // ── Send button — Enter-alternate submit trigger ────────────────── // Retail's gmMainChatUI wires the Send button to the same ProcessCommand path. if (layout.FindElement(SendId) is UiButton sendEl) { sendEl.OnClick = () => c.Input.Submit(); - // The Send sprite is a blank gold button — retail draws the caption as text. + // The Send sprite is a blank gold button — retail draws the caption as text. sendEl.Label = "Send"; sendEl.LabelFont = datFont; sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f); } - // ── Size the channel button to its label + reflow the input field ─ + // ── Size the channel button to its label + reflow the input field ─ // Retail's talk-focus button autosizes to the selected channel name; the input // field then fills the gap from the button's right edge to the Send button. The // dat authors the button at a fixed 46px (too narrow for "Chat" once the LED + @@ -310,12 +310,12 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe ReflowInputRow(); } - // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ── + // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ── if (layout.FindElement(MaxMinId) is UiButton maxMinEl) { // The dat puts max/min and the scrollbar up-button at the SAME X (both // right-anchored), so at content width they overlap. Retail shows max/min - // just LEFT of the scrollbar column — shift it one button-width left. + // just LEFT of the scrollbar column — shift it one button-width left. if (track is not null) maxMinEl.Left = track.Left - maxMinEl.Width; maxMinEl.ResetAnchorCapture(); @@ -326,7 +326,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe return c; } - // ── Max/min implementation ───────────────────────────────────────────── + // ── Max/min implementation ───────────────────────────────────────────── /// /// Attach the typed outer-frame handle after the controller's imported content @@ -419,7 +419,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe return null; } - // ── Helpers ──────────────────────────────────────────────────────────── + // ── Helpers ──────────────────────────────────────────────────────────── /// /// Convert the ChatVM's detailed lines to the transcript's @@ -449,7 +449,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } // Word-wrap each message to the transcript's current pixel width (ports retail - // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would + // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would // exceed wrapWidth). The cache key re-evaluates it after window resize. Func measure = datFont is { } df ? s => df.MeasureWidth(s) @@ -486,8 +486,8 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe /// Greedy word-wrap: split into fragments that each fit in /// pixels (per ), breaking at spaces. /// A word that is itself wider than the line is broken at CHARACTER boundaries (no - /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL - /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same + /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL + /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same /// row as the start of the message. Mirrors retail GlyphList::Recalculate's per-GlyphLine /// emission (which breaks mid-glyph-run when a run exceeds the wrap width). /// @@ -510,7 +510,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } if (line.Length > 0 && measure(word) <= maxW) { - yield return line.ToString(); // word fits alone → push to a new line + yield return line.ToString(); // word fits alone → push to a new line line.Clear(); line.Append(word); continue; @@ -532,7 +532,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe } /// - /// Per- text color — the EXACT retail RGBA values read from a + /// Per- text color — the EXACT retail RGBA values read from a /// live retail client via cdb (the named RGBAColor constants at acclient /// 0x81c4a8+, e.g. colorWhite/colorBrightPurple/colorLightBlue/ /// colorGreen, used by ChatInterface::BuildChatColorLookupTable @0x4f31c0). diff --git a/src/AcDream.App/UI/Layout/CombatUiController.cs b/src/AcDream.App/UI/Layout/CombatUiController.cs index 14c9e411..f2413e0d 100644 --- a/src/AcDream.App/UI/Layout/CombatUiController.cs +++ b/src/AcDream.App/UI/Layout/CombatUiController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Combat; +using AcDream.Core.Combat; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions.Panels.Settings; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// ListenToElementMessage (0x004CC430), and /// RecvNotice_SetCombatMode (0x004CC620). /// -internal sealed class CombatUiController : IRetainedPanelController +public sealed class CombatUiController : IRetainedPanelController { public const uint LayoutId = 0x21000073u; public const uint BasicPanelId = 0x1000005Cu; @@ -231,7 +231,7 @@ internal sealed class CombatUiController : IRetainedPanelController } /// Localized labels assigned by retail gmCombatUI::PostInit. -internal sealed record CombatUiLabels( +public sealed record CombatUiLabels( string Speed, string Power, string RepeatAttacks, diff --git a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs index bccea804..b37dd408 100644 --- a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.UI.Layout; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// UIElement_ListBox::AddItemFromTemplateList used by /// gmSpellComponentUI::UpdateComponents @ 0x0048A910. /// -internal sealed class ComponentBookTemplateFactory +public sealed class ComponentBookTemplateFactory { public const uint LayoutId = 0x21000033u; public const uint CategoryTemplateId = 0x10000466u; @@ -35,7 +35,7 @@ internal sealed class ComponentBookTemplateFactory private readonly ElementInfo _categoryTemplate; private readonly ElementInfo _componentTemplate; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; private readonly string[] _categoryNames; @@ -43,7 +43,7 @@ internal sealed class ComponentBookTemplateFactory public ComponentBookTemplateFactory( ElementInfo categoryTemplate, ElementInfo componentTemplate, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null, IReadOnlyList? categoryNames = null) @@ -64,7 +64,7 @@ internal sealed class ComponentBookTemplateFactory /// public static ComponentBookTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -111,7 +111,7 @@ internal sealed class ComponentBookTemplateFactory public ComponentRow CreateComponentRow( uint componentId, - GpuTextureSlot iconTexture, + uint iconTexture, string name, int ownedCount, uint desiredCount) @@ -183,7 +183,7 @@ internal sealed class ComponentBookTemplateFactory CaptureFonts(child, resolveFont, fonts); } - internal readonly record struct ComponentRow( + public readonly record struct ComponentRow( UiTemplateListSlot Slot, UiField DesiredField); } diff --git a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs index 1b0cd189..a9eec101 100644 --- a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs +++ b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Numerics; using AcDream.Content; using AcDream.Core.Items; @@ -9,7 +9,7 @@ using DatReaderWriter.Types; namespace AcDream.App.UI.Layout; -internal enum CreatureAppraisalValueStyle +public enum CreatureAppraisalValueStyle { Normal, Positive, @@ -17,12 +17,12 @@ internal enum CreatureAppraisalValueStyle Incomplete, } -internal readonly record struct CreatureAppraisalRow( +public readonly record struct CreatureAppraisalRow( string Label, string Value, CreatureAppraisalValueStyle Style); -internal enum CreatureAppraisalRowLayer +public enum CreatureAppraisalRowLayer { Combined, Background, @@ -35,7 +35,7 @@ internal enum CreatureAppraisalRowLayer /// BasicCreatureExamineUI and the two Update overloads at /// 0x004F1D90/0x004F1E80. /// -internal static class CreatureAppraisalRows +public static class CreatureAppraisalRows { private const string Unknown = "???"; private const uint DamageRating = 0x133u; @@ -228,20 +228,20 @@ internal static class CreatureAppraisalRows /// Instantiates LayoutDesc 0x2100006B's InfoRegion token template /// 0x10000166, matching UIElement_ListBox::AddItemFromTemplateList. /// -internal sealed class CreatureAppraisalRowTemplateFactory +public sealed class CreatureAppraisalRowTemplateFactory { public const uint TemplateId = 0x10000166u; public const uint LabelId = 0x1000012Au; public const uint ValueId = 0x1000012Bu; private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public CreatureAppraisalRowTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -256,7 +256,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory public static CreatureAppraisalRowTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -368,7 +368,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory /// instance of the same authored template keeps label/value text above it. /// Both lists share one pixel scroll model so their rows cannot drift. /// -internal sealed class CreatureAppraisalLayeredList +public sealed class CreatureAppraisalLayeredList { public const float TextInset = 8f; @@ -488,7 +488,7 @@ internal sealed class CreatureAppraisalLayeredList /// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces /// underscores with spaces. /// -internal sealed class CreatureDisplayNameResolver +public sealed class CreatureDisplayNameResolver { public const uint MapperDid = 0x2200000Eu; private readonly IReadOnlyDictionary _names; diff --git a/src/AcDream.App/UI/Layout/DatStringResolver.cs b/src/AcDream.App/UI/Layout/DatStringResolver.cs index 10aa6f72..999d4d17 100644 --- a/src/AcDream.App/UI/Layout/DatStringResolver.cs +++ b/src/AcDream.App/UI/Layout/DatStringResolver.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// compute_str_hash @ 0x00413110. A StringInfo's token selects one /// localized string variant; ordinary UI labels use token zero. /// -internal sealed class DatStringResolver +public sealed class DatStringResolver { private readonly IDatReaderWriter _dats; private readonly Dictionary _tables = new(); diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index dd694b7f..2f28bb53 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AcDream.App.UI; @@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout; /// /// /// The meter's back/front 3-slice sprite ids live on grandchild image elements, -/// NOT on the meter element itself (format doc §11). +/// NOT on the meter element itself (format doc §11). /// walks two layers down to extract them: the two Type-3 container children /// ordered by (back behind = lower, front /// on top = higher), then within each container the image children that carry @@ -28,44 +28,44 @@ namespace AcDream.App.UI.Layout; /// /// /// The expand-detail overlay present in the front container carries ONLY named -/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the +/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the /// TryGetValue("") filter in excludes it /// automatically. /// /// -internal static class DatWidgetFactory +public static class DatWidgetFactory { /// /// Creates the for , sets its /// rect (Left/Top/Width/Height) and Anchors, and returns it. /// /// Resolved, merged element snapshot from the LayoutDesc importer. - /// RenderSurface id → (GL tex handle, pixel width, pixel height). + /// RenderSurface id → (GL tex handle, pixel width, pixel height). /// Returns (0,0,0) when the texture is not yet uploaded. /// Retail UI font for the meter's "cur/max" number overlay. - /// May be null pre-load — the meter falls back to the debug bitmap font. - /// Optional font resolver: FontDid → + /// May be null pre-load — the meter falls back to the debug bitmap font. + /// Optional font resolver: FontDid → /// (or null when the font can't be loaded). When non-null, any element whose /// is non-zero gets ITS OWN dat font applied instead of /// the shared fallback. Null = original behavior (use /// for every element). - /// The widget for this element. Never null — every type produces a widget. + /// The widget for this element. Never null — every type produces a widget. public static UiElement? Create(ElementInfo info, - Func resolve, UiDatFont? datFont, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) { // Retail Type 3 = UIElement_Field (reg :126190), but in acdream's CURRENT layouts // (vitals 0x2100006C / chat 0x21000006) Type-3 elements are sprite-bearing chrome + // containers (the 8-piece bevel corners/edges, the transcript/input panels), NOT - // editable fields — retail draws those as inert media-bearing Fields, which our + // editable fields — retail draws those as inert media-bearing Fields, which our // UiDatElement reproduces pixel-for-pixel (and without the spurious focus/edit // affordance a UiField would add). The one true editable field, the chat input // (0x10000016), resolves to Type 12 and is controller-placed as a UiField. So Type 3 // stays on the generic fallback here; register it as UiField only when a window // actually carries a factory-built editable Type-3 field (and UiField grows a // background-media draw + an opt-in editable flag at that point). UiField (the widget) - // still ships — it just isn't wired into the factory switch yet. + // still ships — it just isn't wired into the factory switch yet. // Resolve this element's own dat font if a resolver is provided and the element // has a FontDid. Falls back to the shared datFont when not set (FontDid==0) or // when the resolver returns null (font missing from dats). @@ -86,11 +86,11 @@ internal static class DatWidgetFactory // gmUIElement_*Indicator custom button classes 6 => new UiMenu(), // UIElement_Menu (reg :120163) 7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter - 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf + 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf 11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137) 12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text 0x13 => new UiDialogRoot(), // ConfirmationDialog - 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots + 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots 0x10000035u => BuildCheckbox( info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox _ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers) @@ -105,7 +105,7 @@ internal static class DatWidgetFactory e.Width = info.Width; e.Height = info.Height; - // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the + // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the // gmInventoryUI full-window backdrop at ZLevel 100 sits behind the ZLevel-0 panels, #145); // ReadOrder is the within-layer tiebreaker (higher = on top). K=10000 exceeds any window's // element count so ZLevel always dominates. Vitals (all ZLevel 0) keep ZOrder == ReadOrder. @@ -134,7 +134,7 @@ internal static class DatWidgetFactory /// private static UiScrollbar BuildScrollbar( ElementInfo info, - Func resolve) + Func resolve) { var bar = new UiScrollbar { @@ -302,39 +302,39 @@ internal static class DatWidgetFactory return stateName == "Normal" ? DefaultImage(info) : 0u; } - // ── Meter ──────────────────────────────────────────────────────────────── + // ── Meter ──────────────────────────────────────────────────────────────── /// /// Builds a and populates its sprite ids from the meter's - /// child/grandchild elements (format doc §11). Two shapes are handled: + /// child/grandchild elements (format doc §11). Two shapes are handled: /// /// - /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren): + /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren): /// /// meter (Type 7) - /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind) - /// │ ├── left-cap image (DirectState "" → File = back-left sprite) - /// │ ├── center image (DirectState "" → File = back-tile sprite) - /// │ └── right-cap image (DirectState "" → File = back-right sprite) - /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top) - /// │ ├── left-cap image (→ front-left sprite) - /// │ ├── center image (→ front-tile sprite) - /// │ ├── right-cap image (→ front-right sprite) - /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED) - /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController) + /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind) + /// │ ├── left-cap image (DirectState "" → File = back-left sprite) + /// │ ├── center image (DirectState "" → File = back-tile sprite) + /// │ └── right-cap image (DirectState "" → File = back-right sprite) + /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top) + /// │ ├── left-cap image (→ front-left sprite) + /// │ ├── center image (→ front-tile sprite) + /// │ ├── right-cap image (→ front-right sprite) + /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED) + /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController) /// /// /// /// - /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3 + /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3 /// child, no grandchildren): the back-track sprite is on the meter element's own DirectState; /// the fill sprite is on the single Type-3 child's own DirectState. Both are placed in the /// TILE slot (Back/FrontTile) with left/right caps 0, so tiles /// them across the full bar geometry (DrawMode=Normal) and clips the fill to the fraction. /// (retail: gmToolbarUI::HandleSelectionChanged :198635, UIElement_Meter::Initialize :123328) /// - /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E] - /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F] + /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E] + /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F] /// /// /// @@ -345,7 +345,7 @@ internal static class DatWidgetFactory /// /// private static UiMeter BuildMeter(ElementInfo info, - Func resolve, UiDatFont? datFont) + Func resolve, UiDatFont? datFont) { var m = new UiMeter { @@ -383,18 +383,18 @@ internal static class DatWidgetFactory // Single-image shape used by the toolbar selected-object meters // (health 0x100001A1, mana 0x100001A2). // - The back-track sprite lives on the meter ELEMENT's own DirectState ("" key of - // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E. + // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E. // - The fill sprite lives on the single Type-3 child's own DirectState ("" key of // containers[0].StateMedia). e.g. health fill = 0x0600193F. - // The fill child has NO image grandchildren, so SliceIds would return all-zero — + // The fill child has NO image grandchildren, so SliceIds would return all-zero — // read the container's StateMedia directly instead. // // These go in the TILE slot (not the left-cap slot): the sprites are DrawMode=Normal, // which retail renders as "tile at native width to fill the full element geometry" - // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter:: + // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter:: // DrawChildren :123574 clips the child's FULL 140px geometry box to the fill fraction). // With the sprite on BackLeft instead, UiMeter.DrawHBar would clamp the cap to the - // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and + // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and // mapping the fill fraction to native width when nativeW < 140. The tile slot makes // midW = full bar width, so the back tiles across all 140px and the front clips to // 140*fraction correctly for any native sprite width (left/right caps unused = 0). @@ -435,7 +435,7 @@ internal static class DatWidgetFactory } else { - Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback."); + Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback."); } return m; @@ -482,11 +482,11 @@ internal static class DatWidgetFactory return (left, tile, right); } - // ── Text ───────────────────────────────────────────────────────────────── + // ── Text ───────────────────────────────────────────────────────────────── /// Type-12 UIElement_Text: an editable field or colored-line text view, /// selected from the canonical property bag. The element's - /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text — + /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text — /// so a Type-12 element that previously rendered via UiDatElement keeps its sprite. Lines /// are bound later by the controller (LinesProvider). An unbound UiText draws nothing /// because defaults to transparent. @@ -497,15 +497,15 @@ internal static class DatWidgetFactory /// that subsequently call / /// on dat-origin elements can be simplified. Controllers that explicitly set those /// properties after still override the build-time - /// defaults — the build-time value is just the starting point, not a lock. + /// defaults — the build-time value is just the starting point, not a lock. /// /// /// The font to seed on the widget. When a font resolver was /// provided and the element's FontDid resolved successfully, this is that element-specific /// font; otherwise it is the shared global fallback. Controllers that call /// and set afterward - /// still override this — the build-time value is just the starting point. - private static UiElement BuildText(ElementInfo info, Func resolve, + /// still override this — the build-time value is just the starting point. + private static UiElement BuildText(ElementInfo info, Func resolve, UiDatFont? elementFont = null, Func? stringResolve = null) { @@ -547,7 +547,7 @@ internal static class DatWidgetFactory // Apply horizontal + vertical justification from the dat at build time. // Controllers that call FindElement and set Centered/RightAligned/VerticalJustify - // afterward will override these — this is only the dat-driven default. + // afterward will override these — this is only the dat-driven default. bool centered = info.HJustify == HJustify.Center; bool rightAligned = info.HJustify == HJustify.Right; var vJustify = info.VJustify switch @@ -580,7 +580,7 @@ internal static class DatWidgetFactory // Font color from dat property 0x1B (ColorBaseProperty). // When present, seed DefaultColor so controllers that read it don't have to hard-code colors. - // Controllers that supply explicit per-line colors via LinesProvider still win — this is only + // Controllers that supply explicit per-line colors via LinesProvider still win — this is only // the build-time default. if (info.FontColor.HasValue) t.DefaultColor = info.FontColor.Value; @@ -593,7 +593,7 @@ internal static class DatWidgetFactory private static UiButton BuildButton( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? elementFont, Func? fontResolve, Func? stringResolve) @@ -663,7 +663,7 @@ internal static class DatWidgetFactory /// private static UiButton BuildCheckbox( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? elementFont, Func? fontResolve, Func? stringResolve) diff --git a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs index 3e750c04..80aeebde 100644 --- a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs @@ -1,4 +1,4 @@ -using DatReaderWriter; +using DatReaderWriter; using AcDream.Content; namespace AcDream.App.UI.Layout; @@ -9,16 +9,16 @@ namespace AcDream.App.UI.Layout; /// UIElement_ListBox::AddItemFromTemplateList for /// EffectInfoRegion. /// -internal sealed class EffectRowTemplateFactory +public sealed class EffectRowTemplateFactory { private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public EffectRowTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -33,7 +33,7 @@ internal sealed class EffectRowTemplateFactory public static EffectRowTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -57,7 +57,7 @@ internal sealed class EffectRowTemplateFactory public EffectRow Create( uint spellId, - GpuTextureSlot iconTexture, + uint iconTexture, string name, string remaining) { @@ -90,7 +90,7 @@ internal sealed class EffectRowTemplateFactory return new EffectRow(slot, duration, remaining); } - internal sealed class EffectRow + public sealed class EffectRow { private readonly UiText _duration; private string _remaining; diff --git a/src/AcDream.App/UI/Layout/EffectsUiController.cs b/src/AcDream.App/UI/Layout/EffectsUiController.cs index 85a9b62f..3ab84179 100644 --- a/src/AcDream.App/UI/Layout/EffectsUiController.cs +++ b/src/AcDream.App/UI/Layout/EffectsUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -7,7 +7,7 @@ using AcDream.Core.Spells; namespace AcDream.App.UI.Layout; /// Retail gmEffectsUI positive/negative instance binding. -internal sealed class EffectsUiController : IRetainedPanelController +public sealed class EffectsUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Bu; // gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc. @@ -27,7 +27,7 @@ internal sealed class EffectsUiController : IRetainedPanelController private readonly Spellbook _spellbook; private readonly bool _positive; private readonly Func _serverTime; - private readonly Func _resolveSpellIcon; + private readonly Func _resolveSpellIcon; private readonly EffectRowTemplateFactory _templates; private readonly string _selectPrompt; private readonly UiItemList _list; @@ -48,7 +48,7 @@ internal sealed class EffectsUiController : IRetainedPanelController Spellbook spellbook, bool positive, Func serverTime, - Func resolveSpellIcon, + Func resolveSpellIcon, EffectRowTemplateFactory templates, string selectPrompt, UiItemList list, @@ -81,8 +81,8 @@ internal sealed class EffectsUiController : IRetainedPanelController Spellbook spellbook, bool positive, Func serverTime, - Func spriteResolve, - Func resolveSpellIcon, + Func spriteResolve, + Func resolveSpellIcon, EffectRowTemplateFactory templates, string selectPrompt, Action? close = null) @@ -144,7 +144,7 @@ internal sealed class EffectsUiController : IRetainedPanelController uint identity = enchantment.Identity; EffectRowTemplateFactory.EffectRow row = _templates.Create( enchantment.SpellId, - metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(enchantment.SpellId), + metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId), metadata?.Name ?? $"Spell {enchantment.SpellId}", FormatRemaining(enchantment, _serverTime())); row.Slot.Clicked = () => Select(enchantment.SpellId); diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs index 0e7f32e7..b4976b42 100644 --- a/src/AcDream.App/UI/Layout/ElementReader.cs +++ b/src/AcDream.App/UI/Layout/ElementReader.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI.Layout; @@ -53,7 +53,7 @@ public sealed class ElementInfo /// /// Raw edge-anchor flag values from the dat (LeftEdge, TopEdge, /// RightEdge, BottomEdge fields of ElementDesc). - /// Values 0–4. Imported elements preserve these in ; + /// Values 0–4. Imported elements preserve these in ; /// is only the compatibility projection /// for older programmatic consumers. /// @@ -80,7 +80,7 @@ public sealed class ElementInfo /// /// Font dat object id inherited from the base element's Properties[0x1A] - /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited. + /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited. /// public uint FontDid; @@ -109,7 +109,7 @@ public sealed class ElementInfo public Vector4? FontColor; /// - /// Sprite per state: state name → (RenderSurface file id, DrawMode int). + /// Sprite per state: state name → (RenderSurface file id, DrawMode int). /// The "" key represents the unnamed DirectState (ElementDesc.StateDesc). /// Named states use the UIStateId.ToString() value as the key /// (e.g. "HideDetail", "ShowDetail"). @@ -220,16 +220,16 @@ public sealed class ElementInfo /// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond /// the bit-flag enum from AcDream.App.UI. /// -internal static class ElementReader +public static class ElementReader { /// Compatibility projection from raw retail modes to the legacy /// flags. This projection cannot represent centered /// mode 3 or proportional mode 4 exactly. Imported DAT widgets therefore use /// ; call this only for legacy/programmatic paths. - /// LeftEdge dat field value (0–4). - /// TopEdge dat field value (0–4). - /// RightEdge dat field value (0–4). - /// BottomEdge dat field value (0–4). + /// LeftEdge dat field value (0–4). + /// TopEdge dat field value (0–4). + /// RightEdge dat field value (0–4). + /// BottomEdge dat field value (0–4). public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom) { var a = AnchorEdges.None; @@ -284,7 +284,7 @@ internal static class ElementReader X = derived.X, Y = derived.Y, // NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This - // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is + // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is // indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes). // If a real zero-size derived element ever needs to override a non-zero base in // switch Width/Height to nullable values and use presence-aware merging. @@ -301,11 +301,11 @@ internal static class ElementReader // HJustify/VJustify: derived wins when it carries an explicit non-Center value // (the dat property was present and read); otherwise inherit the base prototype's value. // Center is the default (= "not set by this element") so Center-derived never overrides - // a non-Center base — matching the FontDid "non-zero wins" convention. + // a non-Center base — matching the FontDid "non-zero wins" convention. HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify, VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify, // FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base. - // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base. + // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base. FontColor = derived.FontColor ?? base_.FontColor, // DefaultStateName: derived wins if set; otherwise inherit the base's default. DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName, diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs index 7d2f1a47..d7bf4460 100644 --- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs +++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs @@ -1,4 +1,4 @@ -using System; +using System; using AcDream.Core.Items; using AcDream.Core.Selection; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// Chests, corpses, and other world containers share this bottom-screen strip; /// it is intentionally independent from the owned backpack window. /// -internal sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController +public sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController { public const uint LayoutId = 0x21000008u; public const uint RootId = 0x10000063u; @@ -31,8 +31,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai private readonly SelectionState _selection; private readonly ItemInteractionController _itemInteraction; private readonly StackSplitQuantityState _stackSplitQuantity; - private readonly Func _resolveIcon; - private readonly Func _resolveDragIcon; + private readonly Func _resolveIcon; + private readonly Func _resolveDragIcon; private readonly Action _sendUse; private readonly Action _sendPutItemInContainer; private readonly Action _sendSplitToContainer; @@ -53,8 +53,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai SelectionState selection, ItemInteractionController itemInteraction, StackSplitQuantityState stackSplitQuantity, - Func resolveIcon, - Func resolveDragIcon, + Func resolveIcon, + Func resolveDragIcon, Action sendUse, Action sendPutItemInContainer, Action sendSplitToContainer, @@ -128,8 +128,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai SelectionState selection, ItemInteractionController itemInteraction, StackSplitQuantityState stackSplitQuantity, - Func resolveIcon, - Func resolveDragIcon, + Func resolveIcon, + Func resolveDragIcon, Action sendUse, Action sendPutItemInContainer, Action sendSplitToContainer, @@ -354,9 +354,9 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai private UiItemSlot CreateCell(UiItemList owner, uint guid, ItemDragSource source) { ClientObject? item = _objects.Get(guid); - GpuTextureSlot icon = item is null ? GpuTextureSlot.Unassigned : _resolveIcon( + uint icon = item is null ? 0u : _resolveIcon( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot dragIcon = item is null ? GpuTextureSlot.Unassigned : _resolveDragIcon( + uint dragIcon = item is null ? 0u : _resolveDragIcon( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); var cell = new UiItemSlot { diff --git a/src/AcDream.App/UI/Layout/IndicatorBarController.cs b/src/AcDream.App/UI/Layout/IndicatorBarController.cs index 9b78e933..60e6b741 100644 --- a/src/AcDream.App/UI/Layout/IndicatorBarController.cs +++ b/src/AcDream.App/UI/Layout/IndicatorBarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using AcDream.Core.Items; using AcDream.Core.Net; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// sprites, and state names; this controller owns only live state selection and /// authored input actions. /// -internal sealed class IndicatorBarController : IRetainedPanelController +public sealed class IndicatorBarController : IRetainedPanelController { public const uint LayoutId = 0x21000071u; @@ -270,7 +270,7 @@ internal sealed class IndicatorBarController : IRetainedPanelController } } -internal sealed record IndicatorBarBindings( +public sealed record IndicatorBarBindings( Spellbook Spellbook, ClientObjectTable Objects, Func PlayerGuid, diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs index 26ab7a52..4030a4b1 100644 --- a/src/AcDream.App/UI/Layout/InventoryController.cs +++ b/src/AcDream.App/UI/Layout/InventoryController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; using AcDream.Core.Items; @@ -10,12 +10,12 @@ namespace AcDream.App.UI.Layout; /// Binds the imported gmInventoryUI tree (LayoutDesc 0x21000023) and populates it from /// . The acdream analogue of retail /// gmInventoryUI/gmBackpackUI/gm3DItemsUI ::PostInit (named-retail decomp 176236/176596/176728). -/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace); +/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace); /// drag-into-bag / wield-drop wire are later sub-phases. /// -internal sealed class InventoryController : IItemListDragHandler, IRetainedPanelController +public sealed class InventoryController : IItemListDragHandler, IRetainedPanelController { - // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds). + // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds). public const uint ContentsGridId = 0x100001C6u; // gm3DItemsUI m_itemList ("Contents of Backpack") public const uint ContainerListId = 0x100001CAu; // gmBackpackUI m_containerList (side-bag selector) public const uint TopContainerId = 0x100001C9u; // gmBackpackUI m_topContainer (main-pack cell) @@ -30,7 +30,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel private const uint PaperdollWindowId = 0x100001CDu; private const uint BackpackWindowId = 0x100001CEu; - // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037). + // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037). private const int ContentsColumns = 6; private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px) private const float BackpackCellPx = 36f; // gmBackpackUI column cells (0x100001C9/CA = 36px) @@ -40,8 +40,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _iconIds; - private readonly Func? _dragIconIds; + private readonly Func _iconIds; + private readonly Func? _dragIconIds; private readonly Func _strength; private readonly Func? _ownerName; @@ -80,8 +80,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, - Func? dragIconIds, + Func iconIds, + Func? dragIconIds, Func strength, SelectionState selection, Func? ownerName, @@ -169,7 +169,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel _topContainer.ExamineItemRequested = ExamineItem; } - // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). + // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4). _burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter; if (_burdenMeter is not null) { @@ -180,7 +180,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel // Captions: drive each host UiText directly with the known string (the caption // elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail - // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) + // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.) AttachCaption(layout.FindElement(TitleTextId), () => "Inventory of " + OwnerName(), datFont); AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont); AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of " + OpenContainerName(), datFont); @@ -228,7 +228,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, + Func iconIds, Func strength, SelectionState selection, UiDatFont? datFont, @@ -244,7 +244,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ItemInteractionController? itemInteraction = null, Action? onClose = null, StackSplitQuantityState? stackSplitQuantity = null, - Func? dragIconIds = null) + Func? dragIconIds = null) => new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection, ownerName, datFont, contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite, @@ -342,7 +342,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel Populate(); } - /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted. + /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted. private bool Concerns(ClientObject o) { uint p = _playerGuid(); @@ -425,14 +425,14 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel while (_containerList.GetNumUIItems() < slots) AddEmptyCell(_containerList); } - // Main-pack cell: the player's own container — clicking it opens/selects the main pack. + // Main-pack cell: the player's own container — clicking it opens/selects the main pack. // Retail draws a CONSTANT backpack icon here, NOT the player's body icon: IconData::RenderIcons // (0x0058d1ee) has an IsThePlayer() branch that draws a fixed backpack with m_itemType = // TYPE_CONTAINER. Compose that backpack base over the Container type-underlay (the player // object's own IconId is the character body, which would render wrong here). The backpack // RenderSurface is 0x0600127E, VISUALLY CONFIRMED at the live gate 2026-06-22 (the earlier // 0x060011F4 from a research dat-dump of GetDIDByEnum(0x10000004,7) was a green tile, not the - // pack — the index value was misreported). Retires AP-51. + // pack — the index value was misreported). Retires AP-51. if (_topContainer is not null) { const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate) @@ -442,7 +442,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u), dragIconTexture: _dragIconIds?.Invoke( - ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u)); + ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u); main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u; main.Clicked = () => OpenContainer(p); main.DoubleClicked = () => _itemInteraction?.ActivateItem(p); @@ -454,10 +454,10 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel RefreshBurden(); } - /// The effective open container — the explicit one, or the player (main pack) by default. + /// The effective open container — the explicit one, or the player (main pack) by default. /// Resolved live (not cached at ctor) so a late-arriving player guid is handled. The default /// sentinel is 0; once the main pack is explicitly opened, _openContainer holds the player - /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent. + /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent. private static bool IsBag(ClientObject item) => item.ContainerTypeHint != 0u || item.Type.HasFlag(ItemType.Container) @@ -468,17 +468,17 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel /// The owned destination retail PlaceInBackpack currently uses. public uint CurrentOpenContainerId => EffectiveOpen(); - /// Add a populated cell wired to its click role: container cell → open+select, - /// item cell → select-only. + /// Add a populated cell wired to its click role: container cell → open+select, + /// item cell → select-only. private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false) { if (list is null) return; var item = _objects.Get(guid); - GpuTextureSlot tex = item is null ? GpuTextureSlot.Unassigned + uint tex = item is null ? 0u : _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot? dragTex = item is null ? null + uint dragTex = item is null ? 0u : _dragIconIds?.Invoke( - item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve }; cell.SetItem(guid, tex, dragIconTexture: dragTex); cell.SetWaitingState(waiting); @@ -520,9 +520,9 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel /// /// Bind the exact ItemList_DragOver state for this destination. A normal contents-grid - /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9). + /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9). /// An occupied container selector uses ItemSlot_DragOver_DropIn - /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400. + /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400. /// private void ConfigureDropFeedback(UiItemList list, UiItemSlot cell) { @@ -532,11 +532,11 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel cell.DragRejectSprite = 0x060011F8u; } - /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay + /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay /// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill = /// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown /// capacity). For a CLOSED side bag the contents aren't indexed until it's opened (ViewContents), - /// so the bar reads empty until then — faithful to retail's known-children count. + /// so the bar reads empty until then — faithful to retail's known-children count. private void SetCapacityBar(UiItemSlot cell, uint containerGuid) { int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0; @@ -545,7 +545,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f); } - // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ────────────── + // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ────────────── /// Retail ItemList_BeginDrag selects an unselected item before enabling its waiting /// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot /// until the server confirms the eventual drop. @@ -778,7 +778,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel ?? dispatch(); /// True only when we KNOW the container is full (capacity known + contents indexed). A - /// closed bag (unknown count) returns false → advisory accept; the server is authoritative. + /// closed bag (unknown count) returns false → advisory accept; the server is authoritative. private bool IsContainerFull(uint container) { int cap = _objects.Get(container)?.ItemsCapacity ?? 0; @@ -815,7 +815,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel if (guid == 0) return; _selection.Select(guid, SelectionChangeSource.Inventory); uint open = EffectiveOpen(); - if (guid == open) { ApplyIndicators(); return; } // already open — just move the square + if (guid == open) { ApplyIndicators(); return; } // already open — just move the square uint p = _playerGuid(); _openContainer = guid; @@ -872,7 +872,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel if (host is null) return; // The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack", - // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). + // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live). // Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas // a nested child UiText did not paint. Set it to a static centered single-line label. if (host is UiText t) @@ -913,7 +913,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel } /// Recompute the burden fill + percent. Port of CACQualities::InqLoad - /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden: + /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden: /// player wire EncumbranceVal (PropertyInt 5) if present, else the carried-Burden sum. private void RefreshBurden() { diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs index ea003944..3997e6ee 100644 --- a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs +++ b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Text; namespace AcDream.App.UI.Layout; @@ -18,7 +18,7 @@ public enum ItemAppraisalFontStyle /// Separator inserted before an appraisal fragment. This is the retained /// equivalent of ItemExamineUI::AddItemInfo's final argument. /// -internal enum ItemAppraisalSeparator +public enum ItemAppraisalSeparator { None, Line, @@ -30,7 +30,7 @@ internal enum ItemAppraisalSeparator /// font-color index. Keeping these separate until shaping preserves style /// when a long fragment wraps. /// -internal readonly record struct ItemAppraisalFragment( +public readonly record struct ItemAppraisalFragment( string Text, ItemAppraisalSeparator Separator, ItemAppraisalFontStyle Style); @@ -38,7 +38,7 @@ internal readonly record struct ItemAppraisalFragment( /// /// Immutable item appraisal report in retail append order. /// -internal sealed class ItemAppraisalReport +public sealed class ItemAppraisalReport { public static ItemAppraisalReport Empty { get; } = new([]); diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs index 7e3d444c..8aaa60fc 100644 --- a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs +++ b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using AcDream.Core.Items; using AcDream.Core.Net.Messages; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// and its Appraisal_Show* helpers. Static spell prose comes from the /// installed portal.dat spell table, like retail's ClientMagicSystem. /// -internal static class ItemAppraisalTextFormatter +public static class ItemAppraisalTextFormatter { private static readonly (uint Requirement, uint Stat, uint Difficulty)[] WieldRequirements = diff --git a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs index cbef9793..76a635c1 100644 --- a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs +++ b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using DatReaderWriter; namespace AcDream.App.UI.Layout; @@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout; /// DAT-authored ten-step radial cooldown art owned by the shared retail /// UIElement_UIItem prototype. /// -internal readonly record struct ItemCooldownAssets(IReadOnlyList Sprites) +public readonly record struct ItemCooldownAssets(IReadOnlyList Sprites) { public const uint CatalogLayoutId = 0x21000037u; public const uint SharedItemPrototypeId = 0x1000033Eu; diff --git a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs index 9e7a4cc2..c4ed53a6 100644 --- a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs +++ b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; using AcDream.Core.Spells; namespace AcDream.App.UI.Layout; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// heartbeat before drawing and gives every current and future /// the same display projection. /// -internal sealed class ItemCooldownUiController +public sealed class ItemCooldownUiController { private readonly Spellbook _spellbook; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs index 651d2a71..ea6a53e5 100644 --- a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs +++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned /// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background. /// -internal static class ItemListCellTemplate +public static class ItemListCellTemplate { /// The shared UIItem cell-template catalog. Hardcoded: retail resolves it via /// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal); @@ -87,14 +87,14 @@ internal static class ItemListCellTemplate // child search alone returns nothing for containers. We deliberately do NOT use the // prototype's DirectState child overlay: on the container prototype that child is the // open/selected-container TRIANGLE indicator (0x06005D9C), which retail draws ONLY on the - // selected container (a deferred container-selection feature) — never as empty-cell art. + // selected container (a deferred container-selection feature) — never as empty-cell art. // (Live visual gate 2026-06-22: frame-first stamped the triangle onto every empty cell.) return FindIconEmpty(catalog, proto, new HashSet()); } - // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ── + // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ── // Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern, - // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection. + // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection. private static uint ReadCellTemplateId(ElementDesc elem) { uint id = ReadIdFromState(elem.StateDesc); @@ -121,7 +121,7 @@ internal static class ItemListCellTemplate return 0; } - // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ── + // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ── // Find the icon child's empty media within `element`'s subtree, following BaseElement edges // within the same catalog (cycle-guarded by `baseSeen`). The 32x32 contents prototype carries // 0x1000033B as a direct child; the 36x36 container prototype reaches it only via an inherited @@ -171,7 +171,7 @@ internal static class ItemListCellTemplate return 0; } - // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ── + // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ── private static ElementDesc? FindDesc(LayoutDesc ld, uint id) { foreach (var kv in ld.Elements) diff --git a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs index 7f72ba2f..ecdbd9b4 100644 --- a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs +++ b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Input; +using AcDream.App.Input; namespace AcDream.App.UI.Layout; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// RecvNotice_FinishPowerbar (0x004DA580), and /// ClientCombatSystem::CommenceJump (0x0056AF90). /// -internal sealed class JumpPowerbarController : IRetainedPanelController +public sealed class JumpPowerbarController : IRetainedPanelController { public const uint LayoutId = 0x21000072u; public const uint MeterId = 0x10000034u; diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 5c6648e4..43418270 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Content; using DatReaderWriter; @@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout; /// The result of importing a retail LayoutDesc: a tree with /// an O(1) lookup table for finding any element by its dat id. /// -internal sealed class ImportedLayout +public sealed class ImportedLayout { /// Root widget of the imported tree. public UiElement Root { get; } @@ -37,7 +37,7 @@ internal sealed class ImportedLayout /// /// Pure layer ( / ): /// converts a pre-resolved tree into a -/// tree via . Testable without dats or OpenGL — all tests +/// tree via . Testable without dats or OpenGL — all tests /// in LayoutImporterTests.cs exercise this layer only. /// /// @@ -55,9 +55,9 @@ internal sealed class ImportedLayout /// Every other element type recurses its children generically. /// /// -internal static class LayoutImporter +public static class LayoutImporter { - // ── Pure layer ──────────────────────────────────────────────────────────── + // ── Pure layer ──────────────────────────────────────────────────────────── /// /// Convenience for tests: attach to @@ -68,7 +68,7 @@ internal static class LayoutImporter public static ImportedLayout BuildFromInfos( ElementInfo rootInfo, IEnumerable children, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) @@ -81,15 +81,15 @@ internal static class LayoutImporter /// Pure builder: produce the widget tree from a fully resolved /// tree (children already attached). /// - /// Optional per-element font resolver — FontDid → + /// Optional per-element font resolver — FontDid → /// (or null if the font can't be loaded). When supplied, /// elements with a non-zero get their own dat /// font at build time instead of the shared fallback. /// Null preserves the original single-font behavior for all callers that don't - /// pass it — no behavior change for the live game path. + /// pass it — no behavior change for the live game path. public static ImportedLayout Build( ElementInfo rootInfo, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null, Func? stringResolve = null) @@ -100,7 +100,7 @@ internal static class LayoutImporter var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId); if (root is null) { - Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback."); + Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback."); root = new UiDatElement(rootInfo, resolve); } return new ImportedLayout(root, byId); @@ -108,20 +108,20 @@ internal static class LayoutImporter private static UiElement? BuildWidget( ElementInfo info, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve, Func? stringResolve, Dictionary byId) { var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve); - if (w is null) return null; // Type-12 style prototype — skip + if (w is null) return null; // Type-12 style prototype — skip if (info.Id != 0) byId[info.Id] = w; // Behavioral widgets that draw their full appearance + reproduce their dat // sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps, - // Button labels, Scrollbar arrows) CONSUME their dat children — building those as + // Button labels, Scrollbar arrows) CONSUME their dat children — building those as // separate widgets double-draws and lets an invisible child steal pointer/focus // from the behavioral widget (e.g. the channel Menu's label child intercepting the // button click). Only generic containers (UiDatElement, panels) recurse. See @@ -152,7 +152,7 @@ internal static class LayoutImporter // double-draw the bar art). All other child types are built normally. // // Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children - // (no text children). This loop finds nothing for them → no change to vitals. + // (no text children). This loop finds nothing for them → no change to vitals. foreach (var child in info.Children) { if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter @@ -171,7 +171,7 @@ internal static class LayoutImporter return w; } - // ── Dat shell ───────────────────────────────────────────────────────────── + // ── Dat shell ───────────────────────────────────────────────────────────── /// /// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the @@ -187,7 +187,7 @@ internal static class LayoutImporter // Collect the set of element ids that are referenced as a BaseElement by ANY // element in THIS layout (where BaseLayoutId == layoutId). Such elements are - // purely inheritance templates ("prototypes") — retail never instantiates them + // purely inheritance templates ("prototypes") — retail never instantiates them // as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc // 0x21000016, which all 18 slot elements inherit from and which has no own media. // @@ -258,7 +258,7 @@ internal static class LayoutImporter /// state an element starts in (e.g., Normal, Minimized). It does /// NOT encode visibility of sibling Group containers. The StateDesc's /// contains X/Y/Width/Height/ - /// ZLevel/PassToChildren — there is no Visible flag. + /// ZLevel/PassToChildren — there is no Visible flag. /// /// /// @@ -276,7 +276,7 @@ internal static class LayoutImporter public static ImportedLayout? Import( IDatReaderWriter dats, uint layoutId, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -291,7 +291,7 @@ internal static class LayoutImporter IDatReaderWriter dats, uint layoutId, uint rootElementId, - Func resolve, + Func resolve, UiDatFont? datFont, Func? fontResolve = null) { @@ -301,7 +301,7 @@ internal static class LayoutImporter return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve); } - // ── Inheritance resolution ──────────────────────────────────────────────── + // ── Inheritance resolution ──────────────────────────────────────────────── /// True when a pure-container inheritor needs the mounted-base Z-layer /// correction. Child inheritance itself is unconditional and follows retail @@ -353,9 +353,9 @@ internal static class LayoutImporter // The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted // sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone // top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own - // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000) - // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's - // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the + // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000) + // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's + // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the // panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be // ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in // FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.) @@ -452,7 +452,7 @@ internal static class LayoutImporter if (d.StateDesc is not null) ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info); - // Named states (e.g. UIStateId.HideDetail → "HideDetail"). + // Named states (e.g. UIStateId.HideDetail → "HideDetail"). foreach (var s in d.States) ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info); @@ -464,7 +464,7 @@ internal static class LayoutImporter /// Read the first from into /// info.StateMedia[name], read any into /// info.StateCursors[name], and extract the font DID from property 0x1A - /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set. + /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set. /// private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info) { @@ -504,7 +504,7 @@ internal static class LayoutImporter info.States[stateId] = state; // Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }. - // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty". + // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty". if (info.FontDid == 0 && sd.Properties is not null && sd.Properties.TryGetValue(0x1Au, out var raw) && raw is ArrayBaseProperty arr && arr.Value.Count > 0 @@ -547,15 +547,15 @@ internal static class LayoutImporter }; } - // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A). + // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A). // Only read when not already set (first dat state wins; Merge propagates from base). if (info.FontColor is null && sd.Properties.TryGetValue(0x1Bu, out var cRaw) && cRaw is ColorBaseProperty cProp) { var c = cProp.Value; - // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4. - // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255). + // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4. + // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255). float a = c.Alpha == 0 ? 1f : c.Alpha / 255f; info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a); } @@ -638,7 +638,7 @@ internal static class LayoutImporter return value; } - // ── Prototype detection helpers ─────────────────────────────────────────── + // ── Prototype detection helpers ─────────────────────────────────────────── /// /// Recursively walks and all its children, adding to @@ -656,7 +656,7 @@ internal static class LayoutImporter } /// - /// Returns true when carries no own state media — i.e. its + /// Returns true when carries no own state media — i.e. its /// StateDesc (DirectState) and States (named states) yield no /// entries with a non-zero file id. /// Such elements are pure inheritance templates with no rendering content. @@ -669,7 +669,7 @@ internal static class LayoutImporter return info.StateMedia.Count == 0; } - // ── Element tree search ─────────────────────────────────────────────────── + // ── Element tree search ─────────────────────────────────────────────────── /// /// Find an by id anywhere in the top-level tree of @@ -696,7 +696,7 @@ internal static class LayoutImporter return null; } - // ── Raw-edge layout provenance ──────────────────────────────────────────── + // ── Raw-edge layout provenance ──────────────────────────────────────────── private static void SetOriginalParentSize(ElementInfo child, float width, float height) { diff --git a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs index 03c3f75a..4d461fc1 100644 --- a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs +++ b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using AcDream.Core.Net; @@ -10,7 +10,7 @@ namespace AcDream.App.UI.Layout; /// chrome; this controller owns its five-second text refresh and 120-second /// ping cadence. /// -internal sealed class LinkStatusUiController : IRetainedPanelController +public sealed class LinkStatusUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Du; public const uint RootId = 0x10000167u; @@ -126,7 +126,7 @@ internal sealed class LinkStatusUiController : IRetainedPanelController } } -internal sealed record LinkStatusStrings( +public sealed record LinkStatusStrings( string Description, string Legend, string DisconnectWarning, diff --git a/src/AcDream.App/UI/Layout/MiniGameUiController.cs b/src/AcDream.App/UI/Layout/MiniGameUiController.cs index b0e2c41c..6fcda3f3 100644 --- a/src/AcDream.App/UI/Layout/MiniGameUiController.cs +++ b/src/AcDream.App/UI/Layout/MiniGameUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI.Layout; @@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout; /// ghosted until a future game-state owner calls SetMiniGameActive; /// mounting the page now keeps its window lifecycle on the shared retail host. /// -internal sealed class MiniGameUiController : IRetainedPanelController +public sealed class MiniGameUiController : IRetainedPanelController { public const uint LayoutId = 0x2100001Eu; public const uint RootId = 0x1000016Au; diff --git a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs index 99de82b3..aed5a3f5 100644 --- a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs +++ b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; using AcDream.Core.Textures; using AcDream.Content; using DatReaderWriter; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// gmPaperDollUI::CreateClickMap @ 0x004A4850 and /// gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920. /// -internal sealed class PaperdollClickMap +public sealed class PaperdollClickMap { public const uint ClickMapEnum = 0x1000000Cu; public const uint InterfaceEnumCategory = 7u; diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs index 48599a29..9d84f10b 100644 --- a/src/AcDream.App/UI/Layout/PaperdollController.cs +++ b/src/AcDream.App/UI/Layout/PaperdollController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.App.UI; using AcDream.Core.Items; @@ -10,19 +10,19 @@ namespace AcDream.App.UI.Layout; /// Binds the 24 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the /// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets. /// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp -/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2). +/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2). /// Unwield is handled by InventoryController (dragging an equipped item onto the pack grid). /// -internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController +public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController { public const uint DollViewportId = 0x100001D5u; public const uint DollDragMaskId = 0x100001D6u; - // ── Slots-toggle public surface ─────────────────────────────────────────────────────────────── + // ── Slots-toggle public surface ─────────────────────────────────────────────────────────────── /// /// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles. /// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp - /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor + /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor /// lists remain visible in both views; the three Aetheria lists are independently unlock-gated. /// public static readonly uint[] ArmorSlotElementIds = @@ -36,7 +36,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel /// Default is doll-view (SlotView == false): the 3-D character is visible, /// the 9 armor slots are hidden. Calling Toggle() alternates between views. /// - internal sealed class PaperdollViewState + public sealed class PaperdollViewState { public bool SlotView { get; private set; } // false = doll-view (default) public bool DollVisible => !SlotView; @@ -46,8 +46,8 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _iconIds; - private readonly Func? _dragIconIds; + private readonly Func _iconIds; + private readonly Func? _dragIconIds; private readonly ItemInteractionController _itemInteraction; private readonly bool _ownsItemInteraction; private readonly SelectionState _selection; @@ -55,7 +55,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private readonly List<(EquipMask Mask, UiItemList List)> _slots = new(); private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new(); - // ── Slots-toggle state ──────────────────────────────────────────────────────────────────────── + // ── Slots-toggle state ──────────────────────────────────────────────────────────────────────── private readonly PaperdollViewState _viewState = new(); private readonly List _armorSlots = new(); private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent @@ -64,11 +64,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel private PaperdollController( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, SelectionState selection, + Func iconIds, SelectionState selection, ItemInteractionController itemInteraction, uint emptySlotSprite, UiDatFont? datFont, PaperdollClickMap? clickMap, - Func? dragIconIds, + Func? dragIconIds, IReadOnlyDictionary? emptySlotSprites, bool ownsItemInteraction) { @@ -97,10 +97,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel if (list.Cell.ItemId != 0) _itemInteraction?.ActivateItem(list.Cell.ItemId); }; - // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot + // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot // silhouette) is the doll viewport, which arrives in Slice 2 with the Slots toggle. // Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring - // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's + // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's // insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList; // no need to re-set them here. _slots.Add((mask, list)); @@ -115,7 +115,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel _objects.Cleared += OnObjectsCleared; _selection.Changed += OnSelectionChanged; - // ── Slots-toggle wiring ─────────────────────────────────────────────────────────────────── + // ── Slots-toggle wiring ─────────────────────────────────────────────────────────────────── foreach (var id in ArmorSlotElementIds) if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor); @@ -166,11 +166,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel public static PaperdollController Bind( ImportedLayout layout, ClientObjectTable objects, Func playerGuid, - Func iconIds, SelectionState selection, + Func iconIds, SelectionState selection, ItemInteractionController itemInteraction, uint emptySlotSprite = 0u, UiDatFont? datFont = null, PaperdollClickMap? clickMap = null, - Func? dragIconIds = null, + Func? dragIconIds = null, IReadOnlyDictionary? emptySlotSprites = null, bool ownsItemInteraction = false) => new PaperdollController( @@ -221,7 +221,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel Populate(); } - /// The object belongs to the player (wielded gear or pack contents) — so a change to it may + /// The object belongs to the player (wielded gear or pack contents) — so a change to it may /// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries /// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always /// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by @@ -251,9 +251,9 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel if ((o.CurrentlyEquippedLocation & mask) != EquipMask.None) { worn = o; break; } if (worn is null) { list.Cell.Clear(); continue; } - GpuTextureSlot tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); - GpuTextureSlot? dragTex = _dragIconIds?.Invoke( - worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); + uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects); + uint dragTex = _dragIconIds?.Invoke( + worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u; list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex); } ApplyAetheriaVisibility(); @@ -287,10 +287,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel return EquipMask.None; } - // ── IItemListDragHandler ────────────────────────────────────────────────────────────────────── + // ── IItemListDragHandler ────────────────────────────────────────────────────────────────────── /// Selects the wielded item before the waiting mesh appears. The item itself stays put /// until the server confirms, like the inventory grid and unlike the toolbar's remove-on-lift. - /// Unwield happens on DROP onto the pack grid — InventoryController. + /// Unwield happens on DROP onto the pack grid — InventoryController. public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0. diff --git a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs index 7994838a..6fef9fdf 100644 --- a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs +++ b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Items; using AcDream.Content; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// Cell creation: UIElement_ItemList::InternalCreateItem @ 0x004E3570. /// Prototype media: live DAT catalog LayoutDesc 0x21000037. /// -internal static class PaperdollSlotBackgrounds +public static class PaperdollSlotBackgrounds { internal readonly record struct Definition( uint Element, diff --git a/src/AcDream.App/UI/Layout/RadarController.cs b/src/AcDream.App/UI/Layout/RadarController.cs index 23ec5f8c..1097691b 100644 --- a/src/AcDream.App/UI/Layout/RadarController.cs +++ b/src/AcDream.App/UI/Layout/RadarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// LayoutDesc 0x21000074 tree. Like the other gm* controllers, this class only /// finds children by retail id and attaches live providers; it does not recreate DAT chrome. /// -internal sealed class RadarController : IRetainedPanelController +public sealed class RadarController : IRetainedPanelController { public const uint LayoutId = 0x21000074u; /// Production layout property 0x1000002D, recovered directly from the retail DAT. diff --git a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs index 995c8875..004a357b 100644 --- a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs +++ b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.World; using AcDream.Core.Items; using AcDream.Core.Net; @@ -15,7 +15,7 @@ namespace AcDream.App.UI.Layout; /// AC-specific classification and math decision, while the retained widget remains a /// backend-only renderer. /// -internal sealed class RadarSnapshotProvider +public sealed class RadarSnapshotProvider { private static readonly Vector2 ProductionCenter = new(60f, 60f); diff --git a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs index fbc79c00..7546525f 100644 --- a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs +++ b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using AcDream.Core.Items; using DatReaderWriter; using DatReaderWriter.DBObjs; @@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout; /// AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0 and /// InqHeritageGroupDisplayName @ 0x005B4710. /// -internal sealed class RetailAppraisalNameResolver +public sealed class RetailAppraisalNameResolver { private const uint MaterialClientEnum = 0x10000001u; private const uint MaterialSubEnum = 1u; diff --git a/src/AcDream.App/UI/Layout/RetailDialogData.cs b/src/AcDream.App/UI/Layout/RetailDialogData.cs index 82d47584..debec420 100644 --- a/src/AcDream.App/UI/Layout/RetailDialogData.cs +++ b/src/AcDream.App/UI/Layout/RetailDialogData.cs @@ -1,10 +1,10 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// Property identifiers consumed by retail's Dialog and /// DialogFactory implementations. /// -internal static class RetailDialogProperty +public static class RetailDialogProperty { public const uint Priority = 0x8Du; public const uint Type = 0x8Eu; @@ -43,7 +43,7 @@ public enum RetailDialogType : uint /// The raw numeric keys remain visible because type-specific presenters and semantic /// callbacks both extend the same collection in retail. /// -internal sealed class RetailDialogData +public sealed class RetailDialogData { private readonly Dictionary _values = new(); diff --git a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs index 56b6e3f1..f4c71c2f 100644 --- a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs +++ b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// Retained-mode port of retail DialogFactory @ 0x004773C0..0x00478470. /// It owns dialog contexts, independent FIFO queue groups, nonqueued dialogs, /// priority preemption, callback delivery, close notices, and fresh catalog roots. /// -internal sealed class RetailDialogFactory : IDisposable +public sealed class RetailDialogFactory : IDisposable { public const uint DefaultQueueKey = 2u; public const uint NonQueuedKey = 1u; diff --git a/src/AcDream.App/UI/Layout/RetailFpsController.cs b/src/AcDream.App/UI/Layout/RetailFpsController.cs index c1cf3fa2..b3a299b2 100644 --- a/src/AcDream.App/UI/Layout/RetailFpsController.cs +++ b/src/AcDream.App/UI/Layout/RetailFpsController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace AcDream.App.UI.Layout; @@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout; /// string 0x0DCFFF73 supplies the two labels FPS: and DEG:; /// retail inserts both floating-point values with two decimal places. /// -internal sealed class RetailFpsController +public sealed class RetailFpsController { public const uint LayoutId = 0x2100000Fu; public const uint DisplayElementId = 0x10000047u; diff --git a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs index 80b84812..c5e9c8d5 100644 --- a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs +++ b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI.Layout; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// deferred child; this owner applies the same lifecycle to registered retained /// windows without making the toolbar authoritative for non-toolbar panels. /// -internal sealed class RetailPanelUiController : IDisposable +public sealed class RetailPanelUiController : IDisposable { public const uint RestorePreviousPropertyId = 0x10000049u; diff --git a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs index f94176fe..d9b22a22 100644 --- a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs +++ b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace AcDream.App.UI.Layout; /// How a production retained window obtains its outer chrome. -internal enum RetailWindowChrome +public enum RetailWindowChrome { /// The imported root already is the complete retail outer frame. Imported, @@ -21,9 +21,9 @@ internal enum RetailWindowChrome /// shared nine-slice wrapper, applies exact resize/opacity/visibility policy, mounts /// the outer frame, and returns its registered typed handle. /// -internal static class RetailWindowFrame +public static class RetailWindowFrame { - internal sealed record Options + public sealed record Options { public required string WindowName { get; init; } public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice; @@ -87,7 +87,7 @@ internal static class RetailWindowFrame public static RetailWindowHandle Mount( UiRoot root, UiElement content, - Func resolveChrome, + Func resolveChrome, Options options) { ArgumentNullException.ThrowIfNull(root); diff --git a/src/AcDream.App/UI/Layout/SelectedObjectController.cs b/src/AcDream.App/UI/Layout/SelectedObjectController.cs index 58544f8c..224f9de8 100644 --- a/src/AcDream.App/UI/Layout/SelectedObjectController.cs +++ b/src/AcDream.App/UI/Layout/SelectedObjectController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; using AcDream.Core.Items; @@ -7,7 +7,7 @@ using AcDream.Core.Selection; namespace AcDream.App.UI.Layout; /// -/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4). +/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4). /// Analogue of retail gmToolbarUI::HandleSelectionChanged /// (docs/research/named-retail/acclient_2013_pseudo_c.txt:198635) + /// RecvNotice_UpdateObjectHealth (:196213) + @@ -18,29 +18,29 @@ namespace AcDream.App.UI.Layout; /// guid is provided it sets the name, flashes the selection overlay briefly, and sends /// either QueryHealth (0x01BF) for health-bearing targets or /// QueryItemMana (0x0263) for owned non-stack items. The Health meter -/// becomes visible only when the server actually reports health for the selected guid — +/// becomes visible only when the server actually reports health for the selected guid — /// either an UpdateHealth (0x01C0) arrives (retail -/// RecvNotice_UpdateObjectHealth → SetVisible(1)) or the value is already +/// RecvNotice_UpdateObjectHealthSetVisible(1)) or the value is already /// cached. So a friendly NPC you have not assessed shows name-only (no bar), and a -/// monster's bar appears after damage / a successful assess — matching retail. +/// monster's bar appears after damage / a successful assess — matching retail. /// /// /// /// Retail element roles (PostInit, :198119): m_pSelObjectField /// is the container 0x1000019E whose SetState(0x1000000b/0c) drives a -/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame. +/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame. /// acdream has no state-cascade / transition-animation system, so this controller drives /// the overlay element 0x100001A0 directly and reverts it after the same /// to reproduce the brief flash. The name element /// 0x1000019F is bumped to the top of the strip's z-order so it draws OVER the -/// overlay frame and the health bar (retail draws the name over the bar — see the +/// overlay frame and the health bar (retail draws the name over the bar — see the /// "Drudge Slinker" reference shot). /// /// /// -internal sealed class SelectedObjectController : IRetainedPanelController +public sealed class SelectedObjectController : IRetainedPanelController { - // ── Element ids (toolbar LayoutDesc 0x21000016) ───────────────────────── + // ── Element ids (toolbar LayoutDesc 0x21000016) ───────────────────────── /// Selected-object container / field element id (retail m_pSelObjectField). public const uint ContainerId = 0x1000019E; /// Selected-object name element id (retail m_pSelObjectName, UIElement_Text). @@ -56,24 +56,24 @@ internal sealed class SelectedObjectController : IRetainedPanelController /// Horizontal stack quantity slider (retail m_pStackSizeSlider). public const uint StackSizeSliderId = 0x100001A4; - /// Selection-overlay flash duration — retail's container ObjectSelected state is a - /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E). + /// Selection-overlay flash duration — retail's container ObjectSelected state is a + /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E). private const double FlashSeconds = 0.25; /// Z-order for the name so it draws OVER the overlay frame + health bar. - /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top. + /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top. private const int NameZOrderOnTop = 1_000_000; - /// Z-order for the selection-flash overlay — above the health meter (so the green + /// Z-order for the selection-flash overlay — above the health meter (so the green /// flash isn't hidden by the bar) but below the name (so the name stays readable). private const int OverlayZOrder = NameZOrderOnTop - 1; /// Height (px) of the black name band at the top of the 31px bar sprite. The name - /// label is constrained to this band (top-aligned) so the health bar shows below it — + /// label is constrained to this band (top-aligned) so the health bar shows below it — /// retail "name on the black, bar below". The bar sprite's colored region starts ~y14. private const float NameBandHeight = 15f; - // ── Found elements (any may be null for partial/test layouts) ─────────── + // ── Found elements (any may be null for partial/test layouts) ─────────── private readonly UiElement? _name; private readonly UiDatElement? _overlay; private readonly UiMeter? _healthMeter; @@ -81,7 +81,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController private readonly UiField? _stackSizeEntry; private readonly UiScrollbar? _stackSizeSlider; - // ── Captured delegates ─────────────────────────────────────────────────── + // ── Captured delegates ─────────────────────────────────────────────────── private readonly Func _isHealthTarget; private readonly Func _isOwnedByPlayer; private readonly Func _resolveName; @@ -97,7 +97,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController private readonly Action> _unsubscribeItemManaChanged; private readonly Action> _unsubscribeObjectUpdated; - // ── Live state (read by closures on the per-frame draw path) ──────────── + // ── Live state (read by closures on the per-frame draw path) ──────────── private uint? _current; private string? _currentName; private double _flashRemaining; // > 0 while the selection overlay is flashing @@ -143,7 +143,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController _unsubscribeItemManaChanged = unsubscribeItemManaChanged; _unsubscribeObjectUpdated = unsubscribeObjectUpdated; - // Find elements — silently skip absent ones (partial/test layouts). + // Find elements — silently skip absent ones (partial/test layouts). _name = layout.FindElement(NameId); _overlay = layout.FindElement(OverlayId) as UiDatElement; _healthMeter = layout.FindElement(HealthMeterId) as UiMeter; @@ -152,7 +152,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController _stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar; // The selection-flash overlay must draw OVER the health meter (which spans the whole - // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e. + // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e. // for players/monsters). Float it just below the name so the name stays readable. if (_overlay is not null) _overlay.ZOrder = OverlayZOrder; @@ -195,7 +195,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController // // The bar sprite (0x0600193E/F, 146x31) carries a ~14px BLACK name band across its // TOP with the colored bar in the lower portion (confirmed from the dat). Retail - // draws the object name in that black band with the health bar BELOW it — so the + // draws the object name in that black band with the health bar BELOW it — so the // label is TOP-aligned by constraining its height to the band, not centered over the // whole 31px strip (which overlapped the bar's middle). if (_name is not null) @@ -240,7 +240,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController /// Imported toolbar layout (LayoutDesc 0x21000016). /// The single Core selected-object owner. /// Called once with - /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility. + /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility. /// Returns true for guids that may show a health meter /// (proxy for retail's IsPlayer() || pet_owner || ObjectIsAttackable()). /// Returns retail's NAME_APPROPRIATE display name for a guid. @@ -296,8 +296,8 @@ internal sealed class SelectedObjectController : IRetainedPanelController _sendQueryItemMana(0); } - // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0) - // + SetVisible(0) on the meters). ────────────────────────────────────── + // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0) + // + SetVisible(0) on the meters). ────────────────────────────────────── if (selectionChanged) { if (_healthMeter is not null) _healthMeter.Visible = false; @@ -319,15 +319,15 @@ internal sealed class SelectedObjectController : IRetainedPanelController uint g = guid.Value; - // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ── + // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ── uint stackSize = _stackSize(g); string? objectName = _resolveName(g); _currentName = stackSize > 1u && !string.IsNullOrEmpty(objectName) ? $"{stackSize} {objectName}" : objectName; - // ── 3. Selection overlay: brief flash (retail container ObjectSelected - // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ────────────── + // ── 3. Selection overlay: brief flash (retail container ObjectSelected + // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ────────────── SetOverlayState(stackSize > 1u ? RetailUiStateIds.StackedItemSelected : RetailUiStateIds.ObjectSelected); @@ -344,9 +344,9 @@ internal sealed class SelectedObjectController : IRetainedPanelController if (_stackSizeSlider is not null) _stackSizeSlider.Visible = true; } - // ── 4. Health: query, and show the meter only if real health is already known. + // ── 4. Health: query, and show the meter only if real health is already known. // Otherwise the meter appears when OnHealthChanged fires for this guid - // (retail RecvNotice_UpdateObjectHealth :196213). ────────────────────────── + // (retail RecvNotice_UpdateObjectHealth :196213). ────────────────────────── if (stackSize <= 1u && _isHealthTarget(g)) { if (selectionChanged) @@ -378,7 +378,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController if (_flashRemaining <= 0) return; _flashRemaining -= deltaSeconds; if (_flashRemaining <= 0) - SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank + SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank } private void SetOverlayState(uint state) diff --git a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs index 768ab17f..9d592114 100644 --- a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs +++ b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs @@ -1,4 +1,4 @@ -using AcDream.Content; +using AcDream.Content; using DatReaderWriter; namespace AcDream.App.UI.Layout; @@ -9,19 +9,19 @@ namespace AcDream.App.UI.Layout; /// the component icon as the template root's own UIRegion image while /// retaining child 0x10000330 as the missing-component overlay. /// -internal sealed class SpellExamineComponentTemplateFactory +public sealed class SpellExamineComponentTemplateFactory { public const uint TemplateId = 0x1000032Eu; public const uint MissingOverlayId = 0x10000330u; private readonly ElementInfo _template; - private readonly Func _resolveSprite; + private readonly Func _resolveSprite; private readonly UiDatFont? _defaultFont; private readonly IReadOnlyDictionary _fonts; public SpellExamineComponentTemplateFactory( ElementInfo template, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, IReadOnlyDictionary? fonts = null) { @@ -33,7 +33,7 @@ internal sealed class SpellExamineComponentTemplateFactory public static SpellExamineComponentTemplateFactory? TryLoad( IDatReaderWriter dats, - Func resolveSprite, + Func resolveSprite, UiDatFont? defaultFont, Func? resolveFont) { @@ -53,7 +53,7 @@ internal sealed class SpellExamineComponentTemplateFactory fonts); } - public UiElement Create(GpuTextureSlot iconTexture, bool owned) + public UiElement Create(uint iconTexture, bool owned) { ImportedLayout content = LayoutImporter.Build( _template, diff --git a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs index 39a9bc6b..668709fa 100644 --- a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs +++ b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.Content; using DatReaderWriter; @@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout; /// list points at this prototype through ItemList attribute 0x1000000E; /// retail clones it for every learned spell. /// -internal readonly record struct SpellbookRowStyle( +public readonly record struct SpellbookRowStyle( float Width, float Height, float IconLeft, diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs index b6461f5e..7a408d8d 100644 --- a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs +++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,7 +10,7 @@ using AcDream.Content; namespace AcDream.App.UI.Layout; -internal enum SpellbookWindowPage { Spells, Components } +public enum SpellbookWindowPage { Spells, Components } /// /// Binds retail's combined spellbook/component-book LayoutDesc 0x21000034. @@ -19,7 +19,7 @@ internal enum SpellbookWindowPage { Spells, Components } /// two authored tabs are stateful UIElement_Text controls, matching /// gmSpellbookUI::PostInit @ 0x0048B2B0 and the resolved retail layout. /// -internal sealed class SpellbookWindowController : IRetainedPanelController +public sealed class SpellbookWindowController : IRetainedPanelController { public const uint LayoutId = 0x21000034u; public const uint RootId = 0x100002A8u; @@ -50,8 +50,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController private readonly Func _playerGuid; private readonly IReadOnlyDictionary _components; private readonly SelectionState _selection; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveComponentIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveComponentIcon; private readonly Func _spellLevel; private readonly Action _selectObject; private readonly Action _addFavorite; @@ -86,8 +86,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController Func playerGuid, IReadOnlyDictionary components, SelectionState selection, - Func resolveSpellIcon, - Func resolveComponentIcon, + Func resolveSpellIcon, + Func resolveComponentIcon, Func spellLevel, Action selectObject, Action addFavorite, @@ -170,8 +170,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController Func playerGuid, IReadOnlyDictionary components, SelectionState selection, - Func resolveSpellIcon, - Func resolveComponentIcon, + Func resolveSpellIcon, + Func resolveComponentIcon, Func spellLevel, Action selectObject, Action addFavorite, diff --git a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs index 90ecf1ee..216338b7 100644 --- a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs +++ b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using AcDream.App.Spells; @@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout; /// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one /// request through . /// -internal sealed class SpellcastingUiController : IRetainedPanelController +public sealed class SpellcastingUiController : IRetainedPanelController { public const uint PageId = 0x10000061u; public const uint SpellNameId = 0x1000048Bu; @@ -42,8 +42,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController private readonly SelectionState _selection; private readonly ClientObjectTable _objects; private readonly Func _playerGuid; - private readonly Func _resolveSpellIcon; - private readonly Func _resolveItemDragIcon; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveItemDragIcon; private readonly Action _useItem; private readonly Action? _examineSpell; private readonly Action? _addFavorite; @@ -73,8 +73,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController RuntimeSpellCastState casting, ClientObjectTable objects, Func playerGuid, - Func resolveSpellIcon, - Func resolveItemDragIcon, + Func resolveSpellIcon, + Func resolveItemDragIcon, Action useItem, Action? examineSpell, SelectionState selection, @@ -162,8 +162,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController RuntimeSpellCastState casting, ClientObjectTable objects, Func playerGuid, - Func resolveSpellIcon, - Func resolveItemDragIcon, + Func resolveSpellIcon, + Func resolveItemDragIcon, Action useItem, SelectionState selection, Action? addFavorite, @@ -351,7 +351,7 @@ internal sealed class SpellcastingUiController : IRetainedPanelController var slot = new UiCatalogSlot { EntryId = id, - CatalogIconTexture = metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(id), + CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(id), Label = metadata?.Name ?? $"Spell {id}", SpriteResolve = list.SpriteResolve, CatalogDragPayload = new SpellFavoriteDragPayload(tab, position, id), @@ -530,9 +530,9 @@ internal sealed class SpellcastingUiController : IRetainedPanelController _endowmentHost.Visible = endowment is not null; _endowmentSlot.EntryId = _endowmentItemId; _endowmentSlot.CatalogIconTexture = _endowmentSpellId == 0u - ? GpuTextureSlot.Unassigned : _resolveSpellIcon(_endowmentSpellId); + ? 0u : _resolveSpellIcon(_endowmentSpellId); _endowmentSlot.CatalogOverlayTexture = endowment is null - ? GpuTextureSlot.Unassigned : _resolveItemDragIcon(endowment); + ? 0u : _resolveItemDragIcon(endowment); string spellName = _spellbook.TryGetMetadata(_endowmentSpellId, out SpellMetadata metadata) ? metadata.Name : $"Spell {_endowmentSpellId}"; _endowmentSlot.Label = endowment is null @@ -603,13 +603,13 @@ internal sealed class SpellcastingUiController : IRetainedPanelController } } -internal sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId); +public sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId); /// /// A learned-spell shortcut carried from the spellbook. Unlike a favorite drag, /// lifting this payload never removes anything from its source collection. /// -internal sealed record SpellbookShortcutDragPayload(uint SpellId); +public sealed record SpellbookShortcutDragPayload(uint SpellId); internal static class FavoriteListExtensions { diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 56d05415..9d880d84 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -8,7 +8,7 @@ using AcDream.Core.Selection; namespace AcDream.App.UI.Layout; /// -/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data — +/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data — /// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id, /// populates them from the persisted PlayerDescription shortcuts /// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject @@ -24,7 +24,7 @@ namespace AcDream.App.UI.Layout; /// CreateObject resolves a formerly-unknown guid. /// /// -internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController +public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController { // Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump). // Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF). @@ -39,7 +39,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo // SelectedObjectController owns the health/mana meters and both stack controls, // including retail's initial-hidden state and selection-driven visibility. - // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time. + // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time. // Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic. // Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669) // SetVisible's exactly one element depending on the incoming mode. @@ -64,9 +64,9 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo private readonly ClientObjectTable _repo; private readonly CombatState? _combatState; private readonly ShortcutStore _store; - private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex - private readonly Func? _dragIconIds; - private readonly Action _useItem; // guid → fire UseObject + private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex + private readonly Func? _dragIconIds; + private readonly Action _useItem; // guid → fire UseObject private readonly Action? _sendAddShortcut; private readonly Action? _sendRemoveShortcut; // (index) private readonly ItemInteractionController? _itemInteraction; @@ -83,8 +83,8 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo // Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps ghosting. // Occupancy branch (decomp 229481): - // occupied → regular 0x10000042 / ghosted 0x10000043 - // empty → background digit 0x1000005e (stance-independent) + // occupied → regular 0x10000042 / ghosted 0x10000043 + // empty → background digit 0x1000005e (stance-independent) private uint[]? _regularDigits; private uint[]? _ghostedDigits; private uint[]? _emptyDigits; @@ -94,7 +94,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo ImportedLayout layout, ClientObjectTable repo, ShortcutStore shortcuts, - Func iconIds, + Func iconIds, Action useItem, CombatState? combatState, uint[]? regularDigits, @@ -110,7 +110,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, - Func? dragIconIds = null) + Func? dragIconIds = null) { _repo = repo; _combatState = combatState; @@ -193,7 +193,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo _examineButton.OnClick = () => _itemInteraction?.ExamineSelectedOrEnterMode(_selectedObjectId()); // Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669): - // exactly one indicator visible at a time. Default to NonCombat (peace) — the player + // exactly one indicator visible at a time. Default to NonCombat (peace) — the player // always spawns in peace mode; retail has not yet called SetVisible when PostInit runs. SetCombatMode(CombatMode.NonCombat); @@ -204,7 +204,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo _selection.Changed += OnSelectionChanged; // D.5.4: the table now holds ALL objects (creatures, NPCs, etc.), so filter - // to our 18 shortcut guids — else every creature spawn in a busy zone + // to our 18 shortcut guids — else every creature spawn in a busy zone // needlessly re-populates the bar (gmToolbarUI::SetDelayedShortcutNum pattern). repo.ObjectAdded += OnRepositoryObjectChanged; repo.ObjectUpdated += OnRepositoryObjectChanged; @@ -310,22 +310,22 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// if the shortcut list is refreshed outside the repo-event path. /// /// Imported toolbar layout (LayoutDesc 0x21000016). - /// Live item repository — must stay alive for the controller's lifetime. + /// Live item repository — must stay alive for the controller's lifetime. /// /// Runtime-owned retail shortcut manager. The toolbar borrows this exact /// mutable owner; it never reconstructs another slot map. /// - /// Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle. + /// Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle. /// Callback fired when a bound slot is clicked; receives the item guid. /// - /// Optional live combat state — when provided, the toolbar subscribes to + /// Optional live combat state — when provided, the toolbar subscribes to /// and updates the four mutually-exclusive /// combat-mode indicator elements accordingly. /// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator). /// /// /// Regular digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element - /// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id. + /// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id. /// Null if the dat lookup failed (no digits drawn). Retail reference: /// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). /// @@ -333,14 +333,14 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Empty-slot background digit DID array (property 0x1000005e, stance-independent). /// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum - /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state). + /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state). /// Null if the dat lookup failed (empty slots draw no digit, which is safe). /// public static ToolbarController Bind( ImportedLayout layout, ClientObjectTable repo, ShortcutStore shortcuts, - Func iconIds, + Func iconIds, Action useItem, CombatState? combatState = null, uint[]? regularDigits = null, @@ -356,7 +356,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, - Func? dragIconIds = null) + Func? dragIconIds = null) { var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState, regularDigits, ghostedDigits, emptyDigits, itemInteraction, @@ -427,15 +427,15 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo if (list is null) continue; var item = _repo.Get(guid); if (item is null) continue; // deferred: ObjectAdded re-calls Populate - GpuTextureSlot tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); - GpuTextureSlot? dragTex = _dragIconIds?.Invoke( - item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects); + uint dragTex = _dragIconIds?.Invoke( + item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; list.Cell.SetItem(guid, tex, entry, dragTex); } // Re-stamp slot number labels after any item change. // Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481): - // occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e. + // occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e. // The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511). RestampShortcutNumbers(); } @@ -450,7 +450,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// public void SetCombatMode(CombatMode mode) { - // Index → mode mapping matches CombatIndicatorIds declaration order: + // Index → mode mapping matches CombatIndicatorIds declaration order: // 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic. bool[] show = { @@ -475,14 +475,14 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Push digit-array references and shortcut-number state into every slot cell. - /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown + /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown /// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite - /// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481). - /// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there. + /// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481). + /// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there. /// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); /// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). - /// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043; - /// empty → background 0x1000005e (decomp 229481/229493). + /// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043; + /// empty → background 0x1000005e (decomp 229481/229493). /// private void RestampShortcutNumbers() { @@ -494,7 +494,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo cell.GhostedDigits = _ghostedDigits; cell.EmptyDigits = _emptyDigits; if (i < 9) - cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown + cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown else cell.ClearShortcutNum(); // bottom row: no slot labels } @@ -503,7 +503,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo /// /// Wire the callback on a slot cell so that /// clicking a bound item fires with the slot's current guid. - /// Mirrors retail's gmToolbarUI click → UseShortcut dispatch. + /// Mirrors retail's gmToolbarUI click → UseShortcut dispatch. /// private void WireClick(UiItemList list) { @@ -662,12 +662,12 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo return true; } - // ── IItemListDragHandler (B.2 live handler) ────────────────────────────── + // ── IItemListDragHandler (B.2 live handler) ────────────────────────────── // Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list. // Retail model (remove-on-lift / place-on-drop / no-restore): - // lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately) - // drop → AddShortcut (0x019C) + optional swap of evicted item into source - // off-bar release → the lift's removal stands (no restore) + // lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately) + // drop → AddShortcut (0x019C) + optional swap of evicted item into source + // off-bar release → the lift's removal stands (no restore) // Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971 /// @@ -678,7 +678,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo if (payload.ObjId != 0 && _selectedObjectId() != payload.ObjId) _selectItem?.Invoke(payload.ObjId); - // Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted + // Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted // shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a // drop onto a slot. Off-bar release leaves it removed. _sendRemoveShortcut?.Invoke((uint)payload.SourceSlot); diff --git a/src/AcDream.App/UI/Layout/ToolbarInputController.cs b/src/AcDream.App/UI/Layout/ToolbarInputController.cs index 964762c9..d298da41 100644 --- a/src/AcDream.App/UI/Layout/ToolbarInputController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarInputController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Selection; +using AcDream.Core.Selection; using AcDream.UI.Abstractions.Input; namespace AcDream.App.UI.Layout; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// dispatcher owns chords/scopes; this class only maps semantic actions to the /// exact gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0 slot intent. /// -internal sealed class ToolbarInputController +public sealed class ToolbarInputController { private readonly ToolbarController _toolbar; private readonly SelectionState _selection; diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs index 88b08f99..b67a6c70 100644 --- a/src/AcDream.App/UI/Layout/UiDatElement.cs +++ b/src/AcDream.App/UI/Layout/UiDatElement.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI.Layout; @@ -10,28 +10,28 @@ namespace AcDream.App.UI.Layout; /// faithful because retail's base element render is exactly "stamp the media per draw-mode". /// /// -/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the +/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the /// sprite shader already alpha-blends, so no per-mode branch is needed here. The named /// constants document the real enum for Plan 2. /// /// /// /// DrawModeType (DatReaderWriter.Enums), stored as int in to -/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6: +/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6: /// Undefined=0, Normal=1, Overlay=2, Alphablend=3. There is no Stretch mode. /// /// /// /// Tiling uses UV-repeat on BOTH axes (Width/tw, Height/th) so vertical -/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too. +/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too. /// sets /// GL_REPEAT on both S and T, so vertical tiling is always active. /// /// -internal sealed class UiDatElement : UiElement, IUiDatStateful +public sealed class UiDatElement : UiElement, IUiDatStateful { // DrawModeType enum values from DatReaderWriter.Enums. - // See docs/research/2026-06-15-layoutdesc-format.md §6. + // See docs/research/2026-06-15-layoutdesc-format.md §6. #pragma warning disable IDE0051 // private constants kept for documentation / Plan 2 private const int DrawUndefined = 0; private const int DrawNormal = 1; @@ -40,7 +40,7 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful #pragma warning restore IDE0051 private readonly ElementInfo _info; - private readonly Func _resolve; + private readonly Func _resolve; /// The dat element id from . Exposed so controllers /// can identify which logical element a UiDatElement represents when walking subtrees @@ -102,9 +102,9 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful } /// Merged for this element. - /// Dat file-id → (GL texture handle, native px width, native px height). + /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. - public UiDatElement(ElementInfo info, Func resolve) + public UiDatElement(ElementInfo info, Func resolve) { _info = info; _resolve = resolve; @@ -172,13 +172,13 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful /// The image remains this element's own media, so authored descendants retain /// their normal foreground relationship. /// - public GpuTextureSlot? RuntimeImageTexture { get; set; } + public uint? RuntimeImageTexture { get; set; } protected override void OnDraw(UiRenderContext ctx) { - if (MediaVisible && RuntimeImageTexture is { } runtimeTexture) + if (MediaVisible && RuntimeImageTexture is uint runtimeTexture) { - if (runtimeTexture.IsAssigned) + if (runtimeTexture != 0u) { ctx.DrawSprite( runtimeTexture, @@ -200,9 +200,9 @@ internal sealed class UiDatElement : UiElement, IUiDatStateful if (MediaVisible && file != 0) { var (tex, tw, th) = _resolve(file); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) { - // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI + // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI // texture), matching ImgTex::TileCSI. Overlay/Alphablend use the same blit (the // sprite shader already alpha-blends). No Stretch mode exists in DrawModeType. ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); diff --git a/src/AcDream.App/UI/Layout/UiPropertyBag.cs b/src/AcDream.App/UI/Layout/UiPropertyBag.cs index 2e5c56e8..de97aa43 100644 --- a/src/AcDream.App/UI/Layout/UiPropertyBag.cs +++ b/src/AcDream.App/UI/Layout/UiPropertyBag.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI.Layout; diff --git a/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs index 961aaf29..97793596 100644 --- a/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs +++ b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI.Layout; +namespace AcDream.App.UI.Layout; /// /// One runtime listbox entry instantiated from an authored LayoutDesc template. /// Retail UIElement_ListBox::AddItemFromTemplateList creates exactly this /// ownership shape: the list owns the entry while the template owns its visuals. /// -internal sealed class UiTemplateListSlot : UiItemSlot +public sealed class UiTemplateListSlot : UiItemSlot { private readonly IUiDatStateful? _statefulRoot; private readonly uint _normalState; diff --git a/src/AcDream.App/UI/Layout/VitaeUiController.cs b/src/AcDream.App/UI/Layout/VitaeUiController.cs index 487c58c7..518205b3 100644 --- a/src/AcDream.App/UI/Layout/VitaeUiController.cs +++ b/src/AcDream.App/UI/Layout/VitaeUiController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout; /// Retained port of retail gmVitaeUI. It presents the active vitae /// modifier and the authoritative experience pool needed to regain one percent. /// -internal sealed class VitaeUiController : IRetainedPanelController +public sealed class VitaeUiController : IRetainedPanelController { public const uint LayoutId = 0x21000020u; public const uint RootId = 0x100001C1u; @@ -151,7 +151,7 @@ internal sealed class VitaeUiController : IRetainedPanelController } } -internal sealed record VitaeStrings( +public sealed record VitaeStrings( string FullStrength, string LostPrefix, string LostSuffix, diff --git a/src/AcDream.App/UI/Layout/VitalsController.cs b/src/AcDream.App/UI/Layout/VitalsController.cs index acb74d2a..9668c479 100644 --- a/src/AcDream.App/UI/Layout/VitalsController.cs +++ b/src/AcDream.App/UI/Layout/VitalsController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI; @@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout; /// Per-window controller for the vitals layout (LayoutDesc 0x2100006C). /// Mirrors retail gmVitalsUI::PostInit: grab the three meter elements /// by their dat element ids and bind live data providers (fill fraction + cur/max -/// text) to each. This is the ONLY per-window code in the whole importer — pure +/// text) to each. This is the ONLY per-window code in the whole importer — pure /// data wiring, not graphics. /// /// The slice sprites + dat font on each are already @@ -16,10 +16,10 @@ namespace AcDream.App.UI.Layout; /// only binds the dynamic vitals data. Do not touch meter rendering fields here. /// /// Element ids confirmed from -/// docs/research/2026-06-15-layoutdesc-format.md §11 +/// docs/research/2026-06-15-layoutdesc-format.md §11 /// (vitals window 0x2100006C dump). /// -internal static class VitalsController +public static class VitalsController { /// Dat element id for the Health meter (0x100000E6). public const uint Health = 0x100000E6; @@ -34,7 +34,7 @@ internal static class VitalsController /// /// Bind live vitals data providers to the Health, Stamina, and Mana meter /// elements found in . Any meter whose id is absent - /// from the layout is silently skipped — partial layouts (e.g. test fakes) + /// from the layout is silently skipped — partial layouts (e.g. test fakes) /// do not cause errors. /// /// Imported vitals layout tree. @@ -58,7 +58,7 @@ internal static class VitalsController BindMeter(layout, Mana, ManaText, manaPct, manaText); } - /// White cur/max numbers — matches the former UiMeter.LabelColor default. + /// White cur/max numbers — matches the former UiMeter.LabelColor default. private static readonly Vector4 NumberColor = new(1f, 1f, 1f, 1f); private static void BindMeter( @@ -66,7 +66,7 @@ internal static class VitalsController Func pct, Func text) { - // Silently skip if the id is absent — missing meters are not an error (partial layouts). + // Silently skip if the id is absent — missing meters are not an error (partial layouts). if (layout.FindElement(id) is not UiMeter m) return; m.Fill = () => pct(); diff --git a/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs b/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs index 56573901..187163e4 100644 --- a/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs +++ b/src/AcDream.App/UI/Layout/VividTargetIndicatorController.cs @@ -1,17 +1,17 @@ -using System.Numerics; +using System.Numerics; using AcDream.Core.Selection; using AcDream.Core.Ui; using AcDream.Content; namespace AcDream.App.UI.Layout; -internal readonly record struct VividTargetInfo( +public readonly record struct VividTargetInfo( Vector3 SelectionSphereCenter, float SelectionSphereRadius, uint ItemType, uint ObjectDescriptionFlags); -internal sealed record VividTargetRuntimeBindings( +public sealed record VividTargetRuntimeBindings( SelectionState Selection, Func PlayerGuid, Func Enabled, @@ -25,7 +25,7 @@ internal sealed record VividTargetRuntimeBindings( /// this presentation from the live object's selection sphere, independently of /// whether the world renderer drew or occluded the object. /// -internal sealed class VividTargetIndicatorController +public sealed class VividTargetIndicatorController { private const uint ClientEnumCategory = 0x10000009u; private const uint FirstSourceImageEnum = 1u; @@ -85,7 +85,7 @@ internal sealed class VividTargetIndicatorController return null; var resolved = assets.ResolveSprite(did); - if (!resolved.Texture.IsAssigned || resolved.Width <= 0 || resolved.Height <= 0) + if (resolved.Texture == 0u || resolved.Width <= 0 || resolved.Height <= 0) return null; sources[i] = new VividTargetSource( resolved.Texture, resolved.Width, resolved.Height); @@ -387,6 +387,6 @@ internal readonly record struct VividTargetProjection( float AngleDegrees); internal readonly record struct VividTargetSource( - GpuTextureSlot Texture, + uint Texture, float Width, float Height); diff --git a/src/AcDream.App/UI/MarkupDocument.cs b/src/AcDream.App/UI/MarkupDocument.cs index ddd9364f..1132479b 100644 --- a/src/AcDream.App/UI/MarkupDocument.cs +++ b/src/AcDream.App/UI/MarkupDocument.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Numerics; using System.Reflection; @@ -10,16 +10,16 @@ namespace AcDream.App.UI; /// Parses our KSML-style panel markup (mirrors retail's ElementDesc fields) /// into a live subtree. {Binding} attribute /// values resolve against a supplied object by property name (reflection). -/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7. +/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7. /// -internal static class MarkupDocument +public static class MarkupDocument { /// Raw XML markup for a single panel. /// Object whose public properties are bound to {PropName} attributes. - /// Surface id → (GL handle, width, height) for chrome sprites. + /// Surface id → (GL handle, width, height) for chrome sprites. /// Optional controls.ini stylesheet for the title color. public static UiNineSlicePanel Build( - string xml, object binding, Func resolve, + string xml, object binding, Func resolve, ControlsIni? style = null) { var root = XDocument.Parse(xml).Root ?? throw new FormatException("empty markup"); @@ -86,7 +86,7 @@ internal static class MarkupDocument CultureInfo.InvariantCulture, out var v) ? v : 0f; /// - /// Parses #AARRGGBB → RGBA (alpha first, matching + /// Parses #AARRGGBB → RGBA (alpha first, matching /// controls.ini convention). Falls back to opaque white on bad input. /// private static Vector4 Color(string? hex) diff --git a/src/AcDream.App/UI/RetailChromeSprites.cs b/src/AcDream.App/UI/RetailChromeSprites.cs index de7aea4d..f2a80fd7 100644 --- a/src/AcDream.App/UI/RetailChromeSprites.cs +++ b/src/AcDream.App/UI/RetailChromeSprites.cs @@ -1,66 +1,66 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Retail window-chrome RenderSurface DataIds, CONFIRMED via the D.2b Step-0 /// prove-out (2026-06-14). These are RenderSurface objects (0x06xxxxxx) decoded /// DIRECTLY (), NOT -/// through the Surface→SurfaceTexture chain. +/// through the Surface→SurfaceTexture chain. /// /// /// The universal floating-window bevel is an 8-piece border (4 corners -/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single +/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single /// 9-slice texture. Decoded sizes are in the comments (from the prove-out). /// /// /// -/// The edge/corner → position mapping below is a reasonable guess pending the +/// The edge/corner → position mapping below is a reasonable guess pending the /// LayoutDesc 0x21000040 parse (sub-project 3) and is confirmed visually in the /// first vitals-panel render. If a corner's bevel highlight looks wrong, swap /// the four corner constants; if top/bottom or left/right look inverted, swap /// those edge pairs. /// /// -internal static class RetailChromeSprites +public static class RetailChromeSprites { - /// Tiled interior fill — the shared panel background (48×48). + /// Tiled interior fill — the shared panel background (48×48). public const uint CenterFill = 0x06004CC2; - /// Horizontal top edge (10×5, tiled across the top span). + /// Horizontal top edge (10×5, tiled across the top span). public const uint TopEdge = 0x060074BF; - /// Horizontal bottom edge (10×5). + /// Horizontal bottom edge (10×5). public const uint BottomEdge = 0x060074C1; - /// Vertical left edge (5×10). + /// Vertical left edge (5×10). public const uint LeftEdge = 0x060074C0; - /// Vertical right edge (5×10). + /// Vertical right edge (5×10). public const uint RightEdge = 0x060074C2; - /// Top-left corner (5×5). + /// Top-left corner (5×5). public const uint CornerTL = 0x060074C3; - /// Top-right corner (5×5). + /// Top-right corner (5×5). public const uint CornerTR = 0x060074C4; - /// Bottom-left corner (5×5). + /// Bottom-left corner (5×5). public const uint CornerBL = 0x060074C5; - /// Bottom-right corner (5×5). + /// Bottom-right corner (5×5). public const uint CornerBR = 0x060074C6; /// Border thickness in pixels = the corner/edge sprite size (5px). public const int Border = 5; - // ── Resize-grip overlay ────────────────────────────────────────────── + // ── Resize-grip overlay ────────────────────────────────────────────── // A second 8-piece layer drawn ON TOP of the bevel above: the gold ridged // accents + square corner studs that frame a resizable retail window. From - // the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each - // corner is the same 5×5 stud (0x06006129); the edges are gold double-line + // the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each + // corner is the same 5×5 stud (0x06006129); the edges are gold double-line // strips tiled along each side. These have transparent gaps, so the bevel - // shows through — both layers are needed. - /// Corner grip stud, all four corners (5×5). + // shows through — both layers are needed. + /// Corner grip stud, all four corners (5×5). public const uint GripCorner = 0x06006129; - /// Top edge grip (10×5, tiled across). + /// Top edge grip (10×5, tiled across). public const uint GripTop = 0x0600612A; - /// Left edge grip (5×10, tiled down). + /// Left edge grip (5×10, tiled down). public const uint GripLeft = 0x0600612B; - /// Bottom edge grip (10×5). + /// Bottom edge grip (10×5). public const uint GripBottom = 0x0600612C; - /// Right edge grip (5×10). + /// Right edge grip (5×10). public const uint GripRight = 0x0600612D; } diff --git a/src/AcDream.App/UI/RetailItemConfirmationController.cs b/src/AcDream.App/UI/RetailItemConfirmationController.cs index 325fb44e..e16c8134 100644 --- a/src/AcDream.App/UI/RetailItemConfirmationController.cs +++ b/src/AcDream.App/UI/RetailItemConfirmationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.UI.Layout; +using AcDream.App.UI.Layout; using AcDream.Core.Items; namespace AcDream.App.UI; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// ClientUISystem::UsageConfirmation_* @ 0x00566420..0x005669EA and /// completion mirrors ClientUISystem::UsageCallback @ 0x00565B20. /// -internal sealed class RetailItemConfirmationController : IDisposable +public sealed class RetailItemConfirmationController : IDisposable { internal const string PlayerKillerMessage = "Using this altar will make you a player killer, able to attack or be attacked by other player killers. Are you sure you want to do this?"; diff --git a/src/AcDream.App/UI/RetailPanelCatalog.cs b/src/AcDream.App/UI/RetailPanelCatalog.cs index 14c4b0e7..95dcae21 100644 --- a/src/AcDream.App/UI/RetailPanelCatalog.cs +++ b/src/AcDream.App/UI/RetailPanelCatalog.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Retail gmPanelUI panel ids carried by DAT property 0x10000029. /// The toolbar exposes only a subset of these ids; Helpful/Harmful effects use /// the authored gmUIElement_EffectsIndicator buttons instead. /// -internal static class RetailPanelCatalog +public static class RetailPanelCatalog { public const uint CharacterInformation = 3u; public const uint PositiveEffects = 4u; diff --git a/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs b/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs index ff5eb963..dbd28ef7 100644 --- a/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs +++ b/src/AcDream.App/UI/RetailSkillTrainingConfirmationController.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using AcDream.App.UI.Layout; namespace AcDream.App.UI; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// Already-trained skill XP raises deliberately bypass this controller, matching /// retail's separate gmSkillUI::RaiseSelection @ 0x0049C8C0 path. /// -internal sealed class RetailSkillTrainingConfirmationController +public sealed class RetailSkillTrainingConfirmationController { public const string MessageFormat = "Are you sure you want to spend {0} credits to train {1}?"; diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 98437aef..4d436089 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AcDream.App.Plugins; using AcDream.App.Combat; using AcDream.App.Input; @@ -24,41 +24,41 @@ using Silk.NET.Input; namespace AcDream.App.UI; -internal sealed record RetailUiAssets( +public sealed record RetailUiAssets( IDatReaderWriter Dats, object DatLock, - Func ResolveSprite, + Func ResolveSprite, Func ResolveFont, UiDatFont? DefaultFont, BitmapFont? DebugFont, ControlsIni Controls, IconComposer Icons); -internal sealed record VitalsRuntimeBindings(VitalsVM ViewModel); +public sealed record VitalsRuntimeBindings(VitalsVM ViewModel); -internal sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); +public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); -internal sealed record RadarRuntimeBindings( +public sealed record RadarRuntimeBindings( Func Snapshot, SelectionState Selection, Action SetUiLocked); -internal sealed record CombatRuntimeBindings( +public sealed record CombatRuntimeBindings( CombatState State, RuntimeCombatAttackState Attacks, Func Gameplay, Action SetGameplay); -internal sealed record MagicRuntimeBindings( +public sealed record MagicRuntimeBindings( Spellbook Spellbook, RuntimeSpellCastState Casting, ClientObjectTable Objects, Func PlayerGuid, IReadOnlyDictionary Components, - Func ResolveIcon, - Func ResolveDragIcon, - Func ResolveSpellIcon, - Func ResolveComponentIcon, + Func ResolveIcon, + Func ResolveDragIcon, + Func ResolveSpellIcon, + Func ResolveComponentIcon, SelectionState Selection, Func SpellLevel, Func> SpellComponents, @@ -72,14 +72,14 @@ internal sealed record MagicRuntimeBindings( Action SetDesiredComponent, Func ServerTime); -internal sealed record JumpPowerbarRuntimeBindings(Func Snapshot); +public sealed record JumpPowerbarRuntimeBindings(Func Snapshot); -internal sealed record FpsRuntimeBindings( +public sealed record FpsRuntimeBindings( Func FramesPerSecond, Func DegradeMultiplier, Func IsVisible); -internal sealed record IndicatorRuntimeBindings( +public sealed record IndicatorRuntimeBindings( Spellbook Spellbook, ClientObjectTable Objects, Func PlayerGuid, @@ -89,11 +89,11 @@ internal sealed record IndicatorRuntimeBindings( Action RequestLinkStatusPing, Action EndCharacterSession); -internal sealed record ToolbarRuntimeBindings( +public sealed record ToolbarRuntimeBindings( ClientObjectTable Objects, ShortcutStore Shortcuts, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, Action UseItem, CombatState Combat, ItemManaState ItemMana, @@ -114,13 +114,13 @@ internal sealed record ToolbarRuntimeBindings( Func PlayerGuid, Action? SendPutItemInContainer); -internal sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); +public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); -internal sealed record InventoryRuntimeBindings( +public sealed record InventoryRuntimeBindings( ClientObjectTable Objects, Func PlayerGuid, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, Func Strength, Action? SendUse, Action? SendPutItemInContainer, @@ -129,11 +129,11 @@ internal sealed record InventoryRuntimeBindings( ItemInteractionController ItemInteraction, SelectionState Selection); -internal sealed record ExternalContainerRuntimeBindings( +public sealed record ExternalContainerRuntimeBindings( ExternalContainerState State, ClientObjectTable Objects, - Func ResolveIcon, - Func ResolveDragIcon, + Func ResolveIcon, + Func ResolveDragIcon, ItemInteractionController ItemInteraction, SelectionState Selection, Action SendUse, @@ -141,12 +141,12 @@ internal sealed record ExternalContainerRuntimeBindings( Action SendStackableSplitToContainer, Func IsWithinUseRange); -internal sealed record RetailUiPersistenceBindings( +public sealed record RetailUiPersistenceBindings( SettingsStore Store, Func CharacterKey, Func<(int Width, int Height)> ScreenSize); -internal sealed record RetailUiProbeBindings( +public sealed record RetailUiProbeBindings( bool Enabled, string? ScriptPath, bool DumpOnStart, @@ -155,19 +155,19 @@ internal sealed record RetailUiProbeBindings( Func SetInputHeld, Testing.IRetailUiAutomationRuntime? Runtime = null); -internal sealed record RetailUiCursorBindings( +public sealed record RetailUiCursorBindings( CursorFeedbackController Feedback, RetailCursorManager Manager); -internal sealed record ConfirmationRuntimeBindings( +public sealed record ConfirmationRuntimeBindings( Action SendResponse); -internal sealed record AppraisalRuntimeBindings( +public sealed record AppraisalRuntimeBindings( Func PlayerName, Action SendSetInscription, Action DisplaySystemMessage); -internal sealed record RetailUiRuntimeBindings( +public sealed record RetailUiRuntimeBindings( UiHost Host, RetailUiAssets Assets, VitalsRuntimeBindings Vitals, @@ -196,7 +196,7 @@ internal sealed record RetailUiRuntimeBindings( /// state/action delegates and GL/DAT resolvers; this runtime imports layouts, binds /// controllers, mounts windows, owns persistence, and presents one tick/draw/dispose seam. /// -internal sealed class RetailUiRuntime : IDisposable +public sealed class RetailUiRuntime : IDisposable { private readonly RetailUiRuntimeBindings _bindings; private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity; @@ -566,7 +566,7 @@ internal sealed class RetailUiRuntime : IDisposable ImportedLayout? layout = Import(0x2100006Cu); if (layout is null) { - Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); + Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); return; } @@ -590,7 +590,7 @@ internal sealed class RetailUiRuntime : IDisposable MinWidth = 40f, ContentClickThrough = false, }); - Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); + Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); } private void MountRadar() diff --git a/src/AcDream.App/UI/RetailWindowHandle.cs b/src/AcDream.App/UI/RetailWindowHandle.cs index e021e6e0..fd7d12e6 100644 --- a/src/AcDream.App/UI/RetailWindowHandle.cs +++ b/src/AcDream.App/UI/RetailWindowHandle.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -7,7 +7,7 @@ namespace AcDream.App.UI; /// mutations flow through this handle so lifecycle observers see the same state /// regardless of whether a change came from input, restore, or controller logic. /// -internal sealed class RetailWindowHandle +public sealed class RetailWindowHandle { private readonly RetailWindowManager _owner; private bool _notifiedVisible; diff --git a/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs b/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs index 5d0ac356..9022c780 100644 --- a/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs +++ b/src/AcDream.App/UI/RetailWindowLayoutPersistence.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using AcDream.UI.Abstractions.Panels.Settings; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// default character key so startup layout cannot overwrite a real /// character's state. /// -internal sealed class RetailWindowLayoutPersistence : IDisposable +public sealed class RetailWindowLayoutPersistence : IDisposable { private readonly RetailWindowManager _manager; private readonly SettingsStore _store; diff --git a/src/AcDream.App/UI/RetailWindowManager.cs b/src/AcDream.App/UI/RetailWindowManager.cs index d38021a5..8dd9d228 100644 --- a/src/AcDream.App/UI/RetailWindowManager.cs +++ b/src/AcDream.App/UI/RetailWindowManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ namespace AcDream.App.UI; /// and controller teardown. remains responsible for raw /// input ownership and reports completed interactions to this manager. /// -internal sealed class RetailWindowManager : IDisposable +public sealed class RetailWindowManager : IDisposable { private readonly UiRoot _root; private readonly Dictionary _byName = diff --git a/src/AcDream.App/UI/RetainedPanelControllerGroup.cs b/src/AcDream.App/UI/RetainedPanelControllerGroup.cs index 161b9592..890fdba9 100644 --- a/src/AcDream.App/UI/RetainedPanelControllerGroup.cs +++ b/src/AcDream.App/UI/RetainedPanelControllerGroup.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -8,7 +8,7 @@ namespace AcDream.App.UI; /// inventory+paperdoll). Teardown runs in reverse construction order and is /// idempotent. /// -internal sealed class RetainedPanelControllerGroup : IRetainedPanelController +public sealed class RetainedPanelControllerGroup : IRetainedPanelController { private readonly IRetainedPanelController[] _controllers; private bool _disposed; diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs index a11bf09a..42218789 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationProbe.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -10,7 +10,7 @@ namespace AcDream.App.UI.Testing; /// Snapshot row for the live retained-mode retail UI tree. Coordinates are /// absolute root/screen pixels. /// -internal sealed record RetailUiProbeElement( +public sealed record RetailUiProbeElement( int Index, int Depth, string Path, @@ -32,14 +32,14 @@ internal sealed record RetailUiProbeElement( public int CenterY => (int)MathF.Round(Y + Height * 0.5f); } -internal sealed record RetailUiProbeAssertion(bool Success, string Message); +public sealed record RetailUiProbeAssertion(bool Success, string Message); /// /// Test/diagnostic harness for the retail-style UI. It drives /// mouse events, never panel controllers directly, so automated checks exercise /// the same click, double-click, and drag-drop path as a player. /// -internal sealed class RetailUiAutomationProbe +public sealed class RetailUiAutomationProbe { private readonly UiRoot _root; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs index c3f61a64..4fc0e1f7 100644 --- a/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs +++ b/src/AcDream.App/UI/Testing/RetailUiAutomationScriptRunner.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -15,7 +15,7 @@ public enum RetailUiAutomationCheckpointStatus Cancelled, } -internal interface IRetailUiAutomationCheckpoint +public interface IRetailUiAutomationCheckpoint { int Sequence { get; } string Name { get; } @@ -28,7 +28,7 @@ internal interface IRetailUiAutomationCheckpoint /// diagnostics. Implementations run on the same update/render thread as the /// script; they must not mutate gameplay state or call GL from another thread. /// -internal interface IRetailUiAutomationRuntime +public interface IRetailUiAutomationRuntime { bool IsWorldReady { get; } bool IsWorldViewportVisible { get; } @@ -48,7 +48,7 @@ internal interface IRetailUiAutomationRuntime /// execute on render ticks. Pointer commands use the same /// path as physical mouse input; semantic input uses the production dispatcher. /// -internal sealed class RetailUiAutomationScriptRunner : IDisposable +public sealed class RetailUiAutomationScriptRunner : IDisposable { private readonly RetailUiAutomationProbe _probe; private readonly Action _log; diff --git a/src/AcDream.App/UI/UiButton.cs b/src/AcDream.App/UI/UiButton.cs index b2a02244..e642ccc4 100644 --- a/src/AcDream.App/UI/UiButton.cs +++ b/src/AcDream.App/UI/UiButton.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Numerics; using AcDream.App.UI.Layout; namespace AcDream.App.UI; /// -/// Generic dat-widget button — the production replacement for any dat element of +/// Generic dat-widget button — the production replacement for any dat element of /// Type 1 (UIElement_Button, registered via RegisterElementClass(1, UIElement_Button::Create) /// @ acclient_2013_pseudo_c.txt:125828). /// @@ -21,7 +21,7 @@ namespace AcDream.App.UI; /// /// State selection: picks if set, then /// "Normal" if the element has a Normal state sprite, then falls back to the unnamed -/// DirectState ("" key) — identical to . +/// DirectState ("" key) — identical to . /// /// /// @@ -30,12 +30,12 @@ namespace AcDream.App.UI; /// earlier dev-scaffold widget with no dat sprites. /// /// -internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful +public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful { private readonly ElementInfo _info; private readonly ElementInfo _mediaInfo; private readonly FaceSegment[] _faceSegments; - private readonly Func _resolve; + private readonly Func _resolve; private readonly HashSet _availableStates = new(); private bool _pressed; private bool _pointerOver; @@ -103,7 +103,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center; /// Label horizontal alignment options. - internal enum LabelAlignment { Center, Left } + public enum LabelAlignment { Center, Left } public bool ToggleBehavior { get; } public bool RolloverEnabled { get; } @@ -123,7 +123,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu } /// - /// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized). + /// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized). /// Matches . /// public string ActiveState { get; set; } = ""; @@ -198,11 +198,11 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu } /// Merged for this element. - /// Dat file-id → (GL texture handle, native px width, native px height). + /// Dat file-id → (GL texture handle, native px width, native px height). /// Returns (0,0,0) when the texture is not yet uploaded. public UiButton( ElementInfo info, - Func resolve, + Func resolve, ElementInfo? mediaInfo = null, IReadOnlyList? faceSegments = null) { @@ -212,7 +212,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu ? [] : faceSegments.Select(static segment => new FaceSegment(segment)).ToArray(); _resolve = resolve; - ClickThrough = false; // buttons are interactive — opt OUT of click-through + ClickThrough = false; // buttons are interactive — opt OUT of click-through // Visual transitions can select only states with an actual button face. // Retail layouts commonly declare an empty Normal_pressed descriptor while @@ -254,7 +254,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu /// procedurally, so the importer must not build the button's children as widgets. public override bool ConsumesDatChildren => true; - /// A button is interactive — it must receive its Click even inside a whole-window-Draggable + /// A button is interactive — it must receive its Click even inside a whole-window-Draggable /// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the /// IA-12 whole-window-drag that would otherwise swallow the press. public override bool HandlesClick => true; @@ -282,9 +282,9 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (file != 0) { var (tex, tw, th) = _resolve(file); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) { - // Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped + // Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped // UI texture). Matches ImgTex::TileCSI; no Stretch mode exists. float faceWidth = FaceWidth > 0f ? FaceWidth : Width; float faceHeight = FaceHeight > 0f ? FaceHeight : Height; @@ -312,7 +312,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (dragSprite != 0) { var (tex, _, _) = _resolve(dragSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } } @@ -322,7 +322,7 @@ internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStatefu if (file == 0 || rect.Width <= 0 || rect.Height <= 0) return; var (texture, textureWidth, textureHeight) = _resolve(file); - if (!texture.IsAssigned || textureWidth == 0 || textureHeight == 0) + if (texture == 0 || textureWidth == 0 || textureHeight == 0) return; // Same tiled/cropped media path as UiDatElement. Segment geometry is diff --git a/src/AcDream.App/UI/UiButtonStateMachine.cs b/src/AcDream.App/UI/UiButtonStateMachine.cs index 71826fbb..4238df9d 100644 --- a/src/AcDream.App/UI/UiButtonStateMachine.cs +++ b/src/AcDream.App/UI/UiButtonStateMachine.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Inputs to retail UIElement_Button::UpdateState_. public readonly record struct UiButtonVisualInput( @@ -12,7 +12,7 @@ public readonly record struct UiButtonVisualInput( /// GL-free retail button visual-state policy. State ids are the numeric /// UIStateId values from the DAT. /// -internal static class UiButtonStateMachine +public static class UiButtonStateMachine { public const uint Normal = 1u; public const uint NormalRollover = 2u; diff --git a/src/AcDream.App/UI/UiCatalogSlot.cs b/src/AcDream.App/UI/UiCatalogSlot.cs index 9a6aaae3..a6d36372 100644 --- a/src/AcDream.App/UI/UiCatalogSlot.cs +++ b/src/AcDream.App/UI/UiCatalogSlot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using AcDream.App.Rendering; @@ -9,12 +9,12 @@ namespace AcDream.App.UI; /// It reuses the retail UIItemList geometry without pretending a spell id is an /// object GUID; catalog drags stay distinct from physical item drag/drop. /// -internal sealed class UiCatalogSlot : UiItemSlot +public sealed class UiCatalogSlot : UiItemSlot { public uint EntryId { get; set; } - public GpuTextureSlot CatalogIconTexture { get; set; } = GpuTextureSlot.Unassigned; + public uint CatalogIconTexture { get; set; } /// Optional second 32x32 layer (retail spell endowment item icon). - public GpuTextureSlot CatalogOverlayTexture { get; set; } = GpuTextureSlot.Unassigned; + public uint CatalogOverlayTexture { get; set; } public string Label { get; set; } = string.Empty; public string? Detail { get; set; } public bool ShowLabel { get; init; } @@ -43,8 +43,8 @@ internal sealed class UiCatalogSlot : UiItemSlot public override bool IsDragSource => CatalogDragPayload is not null; public override object? GetDragPayload() => CatalogDragPayload; - public override (GpuTextureSlot tex, int w, int h)? GetDragGhost() => - CatalogDragPayload is not null && CatalogIconTexture.IsAssigned + public override (uint tex, int w, int h)? GetDragGhost() => + CatalogDragPayload is not null && CatalogIconTexture != 0u ? (CatalogIconTexture, 32, 32) : null; @@ -92,7 +92,7 @@ internal sealed class UiCatalogSlot : UiItemSlot if (BackgroundSprite != 0u && SpriteResolve is not null) { var (texture, _, _) = SpriteResolve(BackgroundSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } @@ -101,16 +101,16 @@ internal sealed class UiCatalogSlot : UiItemSlot float iconWidth = IconWidth > 0f ? IconWidth : ShowLabel ? MathF.Min(32f, Height) : Width; float iconHeight = IconHeight > 0f ? IconHeight : ShowLabel ? MathF.Min(32f, Height) : Height; - if (CatalogIconTexture.IsAssigned) + if (CatalogIconTexture != 0) ctx.DrawSprite(CatalogIconTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); else if (SpriteResolve is not null && EmptySprite != 0) { var (texture, _, _) = SpriteResolve(EmptySprite); - if (texture.IsAssigned) + if (texture != 0) ctx.DrawSprite(texture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); } - if (CatalogOverlayTexture.IsAssigned) + if (CatalogOverlayTexture != 0) ctx.DrawSprite(CatalogOverlayTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One); // Spell favorites are UIElement_UIItems too. Retail's @@ -147,7 +147,7 @@ internal sealed class UiCatalogSlot : UiItemSlot { if (!Selected || SpriteResolve is null || SelectedSprite == 0u) return; var (texture, _, _) = SpriteResolve(SelectedSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiCollapsibleFrame.cs b/src/AcDream.App/UI/UiCollapsibleFrame.cs index 2e6a4fac..97899243 100644 --- a/src/AcDream.App/UI/UiCollapsibleFrame.cs +++ b/src/AcDream.App/UI/UiCollapsibleFrame.cs @@ -1,18 +1,18 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI; /// -/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and -/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the +/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and +/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the /// bottom edge (the mount sets = Bottom); each tick it resolves -/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a -/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17. +/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a +/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17. /// -internal sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController +public sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController { - public UiCollapsibleFrame(Func resolve) : base(resolve) { } + public UiCollapsibleFrame(Func resolve) : base(resolve) { } public float CollapsedHeight { get; set; } public float ExpandedHeight { get; set; } @@ -25,13 +25,13 @@ internal sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStat protected override void OnTick(double deltaSeconds) { base.OnTick(deltaSeconds); - if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap + if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap bool expanded = IsExpanded; Height = expanded ? ExpandedHeight : CollapsedHeight; // snap the dragged height to a stop for (int i = 0; i < SecondRow.Count; i++) SecondRow[i].Visible = expanded; } - /// Test hook — OnTick is protected. Drives one snap+visibility reconcile. + /// Test hook — OnTick is protected. Drives one snap+visibility reconcile. internal void TickForTest(double dt) => OnTick(dt); public RetainedWindowState CaptureWindowState() diff --git a/src/AcDream.App/UI/UiDatFont.cs b/src/AcDream.App/UI/UiDatFont.cs index ff17f89e..176cb3b0 100644 --- a/src/AcDream.App/UI/UiDatFont.cs +++ b/src/AcDream.App/UI/UiDatFont.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; @@ -17,7 +17,7 @@ namespace AcDream.App.UI; /// as two textured quads exactly the way the retail client does. /// /// -/// Retail render model — SurfaceWindow::DrawCharacter +/// Retail render model — SurfaceWindow::DrawCharacter /// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits): for /// each glyph it copies the BACKGROUND atlas sub-rect first, tinted with the /// outline color (black), then the FOREGROUND atlas sub-rect, tinted with the @@ -30,27 +30,27 @@ namespace AcDream.App.UI; /// /// /// Atlas format: the foreground atlas (0x06005EE5 for Font 0x40000000) is -/// PFID_A8 — alpha-only. Our SurfaceDecoder expands A8 to RGBA as +/// PFID_A8 — alpha-only. Our SurfaceDecoder expands A8 to RGBA as /// (255,255,255, alpha). The UI sprite shader path (ui_text.frag, /// uUseTexture==2) MULTIPLIES the sampled texel by the per-vertex tint /// (texture(uTex,vUv) * vColor), so tinting a white+alpha glyph by a -/// color gives that color with the glyph's alpha — black for the outline pass, +/// color gives that color with the glyph's alpha — black for the outline pass, /// text color for the fill pass. No shader change was needed. /// /// -internal sealed class UiDatFont +public sealed class UiDatFont { /// Retail UI font id (Latin-1, 16x16 max, with outline atlas). public const uint DefaultFontId = 0x40000000u; - /// Foreground (glyph pixels) texture-table slot + atlas pixel size. - public GpuTextureSlot ForegroundTexture { get; } + /// Foreground (glyph pixels) GL texture handle + atlas pixel size. + public uint ForegroundTexture { get; } public int ForegroundWidth { get; } public int ForegroundHeight { get; } - /// Background (outline/shadow) texture-table slot + atlas pixel size. - /// Unassigned when the font has no background atlas (then the outline pass is skipped). - public GpuTextureSlot BackgroundTexture { get; } + /// Background (outline/shadow) GL texture handle + atlas pixel size. + /// 0 when the font has no background atlas (then the outline pass is skipped). + public uint BackgroundTexture { get; } public int BackgroundWidth { get; } public int BackgroundHeight { get; } @@ -63,8 +63,8 @@ internal sealed class UiDatFont private readonly Dictionary _glyphs; internal UiDatFont( - GpuTextureSlot fgTex, int fgW, int fgH, - GpuTextureSlot bgTex, int bgW, int bgH, + uint fgTex, int fgW, int fgH, + uint bgTex, int bgW, int bgH, float lineHeight, float baselineOffset, Dictionary glyphs) { @@ -78,7 +78,7 @@ internal sealed class UiDatFont /// True if this font carries a separate outline/shadow atlas /// (retail's m_pBackgroundSurface). When false the outline pass is /// skipped and only the foreground (fill) glyphs are drawn. - public bool HasBackground => BackgroundTexture.IsAssigned; + public bool HasBackground => BackgroundTexture != 0; /// Look up a glyph descriptor for a character. Returns false for /// characters not present in the font's table (callers skip them). @@ -88,7 +88,7 @@ internal sealed class UiDatFont /// Load Font from the dat collection and upload /// both atlases through the texture cache (the same direct-RenderSurface /// path the D.2b chrome sprites use). Returns null if the Font DBObj is - /// missing — callers fall back to the debug bitmap font. + /// missing — callers fall back to the debug bitmap font. /// public static UiDatFont? Load(IDatReaderWriter dats, TextureCache cache, uint fontId = DefaultFontId) { @@ -104,9 +104,9 @@ internal sealed class UiDatFont // Point-sample the glyph atlases (nearest) so small UI text stays pixel-crisp; // bilinear softens the dat font noticeably (the chat menu/button text "blur"). - GpuTextureSlot fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true); + uint fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true); - GpuTextureSlot bgTex = GpuTextureSlot.Unassigned; int bgW = 0, bgH = 0; + uint bgTex = 0; int bgW = 0, bgH = 0; if (font.BackgroundSurfaceDataId != 0) bgTex = cache.GetOrUploadRenderSurface(font.BackgroundSurfaceDataId, out bgW, out bgH, nearest: true); @@ -147,7 +147,7 @@ internal sealed class UiDatFont /// /// Pure pen-advance summation seam: total width of /// given a that maps each char to its descriptor - /// (null = not in the font → contributes nothing). Lets the advance math be + /// (null = not in the font → contributes nothing). Lets the advance math be /// unit-tested with synthetic glyphs, with no GL or dat dependency. /// public static float MeasureWidth(string? text, Func lookup) diff --git a/src/AcDream.App/UI/UiDialogRoot.cs b/src/AcDream.App/UI/UiDialogRoot.cs index 81a5fb47..3915b497 100644 --- a/src/AcDream.App/UI/UiDialogRoot.cs +++ b/src/AcDream.App/UI/UiDialogRoot.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -6,7 +6,7 @@ namespace AcDream.App.UI; /// Full-device modal root for retail dialog element classes. The authored child with id /// 0x3D supplies the visible popup art; this root only owns modality and input capture. /// -internal sealed class UiDialogRoot : UiPanel +public sealed class UiDialogRoot : UiPanel { public Action? Cancel { get; set; } diff --git a/src/AcDream.App/UI/UiElement.cs b/src/AcDream.App/UI/UiElement.cs index 7c1d9d51..ea484b1e 100644 --- a/src/AcDream.App/UI/UiElement.cs +++ b/src/AcDream.App/UI/UiElement.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; namespace AcDream.App.UI; /// Which parent edges a child keeps a fixed margin to on resize. -/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches. +/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches. [System.Flags] -internal enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 } +public enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 } /// Retail dat cursor media attached to a UI state. public readonly record struct UiCursorMedia(uint File, int HotspotX, int HotspotY) @@ -21,7 +21,7 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot /// Design notes: /// - Retail AC delegates widget semantics to the external /// keystone.dll library (see -/// docs/research/retail-ui/02-class-hierarchy.md — there is no +/// docs/research/retail-ui/02-class-hierarchy.md — there is no /// widget hierarchy inside acclient.exe itself). We implement /// our own retained-mode toolkit here, matching the behavior /// described in the decompile without trying to byte-match Keystone's @@ -35,13 +35,13 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot /// - Coordinates are in screen pixels, origin top-left. /// is in the parent's local coordinate space. /// -internal abstract class UiElement +public abstract class UiElement { - // ── Identity ───────────────────────────────────────────────────────── + // ── Identity ───────────────────────────────────────────────────────── /// /// Unique 32-bit event ID. Retail uses the range 0x10000000+ /// for custom app events (see - /// docs/research/retail-ui/04-input-events.md §3). Assigned + /// docs/research/retail-ui/04-input-events.md §3). Assigned /// by when the element is added to the tree. /// public uint EventId { get; internal set; } @@ -96,7 +96,7 @@ internal abstract class UiElement return default; } - // ── Geometry ──────────────────────────────────────────────────────── + // ── Geometry ──────────────────────────────────────────────────────── /// X in the parent's local pixel space. public float Left { get; set; } public float Top { get; set; } @@ -119,7 +119,7 @@ internal abstract class UiElement } } - // ── State flags ───────────────────────────────────────────────────── + // ── State flags ───────────────────────────────────────────────────── private bool _visible = true; public bool Visible { @@ -152,7 +152,7 @@ internal abstract class UiElement } /// - /// If true, skips this element — the event + /// If true, skips this element — the event /// passes through to whatever is behind. Used by decoration widgets /// (portrait frames, ornamental dividers). /// @@ -212,13 +212,13 @@ internal abstract class UiElement /// If true, a left-drag starting on this element is delivered to the /// element (e.g. text selection) instead of moving/resizing an ancestor window. - /// Edge resize on a resizable ancestor still wins — only the interior move / + /// Edge resize on a resizable ancestor still wins — only the interior move / /// drag-drop candidacy is suppressed in favour of the element's own handling. public bool CapturesPointerDrag { get; set; } /// If true, a left-press-and-move on this element starts a DRAG-DROP /// ( promotes to BeginDrag) rather than moving a Draggable - /// ancestor window — so an item cell inside the toolbar frame drags the item, not + /// ancestor window — so an item cell inside the toolbar frame drags the item, not /// the window. Distinct from (a self-driven /// interior drag like text selection, which does NOT promote to BeginDrag). Default /// false; overridden by drag sources (e.g. an occupied ). @@ -227,7 +227,7 @@ internal abstract class UiElement /// If true, a left-press on this element is handled BY the element (it receives the Click /// on release) instead of being captured as a whole-window move on a Draggable ancestor. Set by /// interactive leaf widgets (e.g. ) so they stay clickable inside a - /// whole-window-Draggable frame like the inventory window — where, without this, the IA-12 + /// whole-window-Draggable frame like the inventory window — where, without this, the IA-12 /// whole-window-drag swallows the press and the Click is never emitted. Distinct from /// (starts a drag-drop) and (a /// self-driven interior drag such as text selection). Default false. @@ -260,7 +260,7 @@ internal abstract class UiElement ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Top | ResizeEdges.Bottom; /// Edges this element anchors to in its parent. Default Left|Top - /// (pinned top-left, fixed size — no reflow). Left|Right stretches width. + /// (pinned top-left, fixed size — no reflow). Left|Right stretches width. private AnchorEdges _anchors = AnchorEdges.Left | AnchorEdges.Top; /// Edges this programmatic element anchors to in its parent. Assigning @@ -284,7 +284,7 @@ internal abstract class UiElement /// public UiLayoutPolicy? LayoutPolicy { get; set; } - // ── Tree structure ────────────────────────────────────────────────── + // ── Tree structure ────────────────────────────────────────────────── public UiElement? Parent { get; private set; } private readonly List _children = new(); @@ -355,7 +355,7 @@ internal abstract class UiElement /// /// True if this widget draws its full appearance itself and REPRODUCES its dat /// sub-elements procedurally (3-slice caps, button labels, scroll arrows, popup - /// rows…) — so the must NOT build + /// rows…) — so the must NOT build /// those dat child elements as separate widgets (they would double-draw and, worse, /// steal pointer/focus from the behavioral widget). All registered behavioral widgets /// (Meter/Menu/Button/Scrollbar/Text/Field) return true; the generic container @@ -365,7 +365,7 @@ internal abstract class UiElement /// public virtual bool ConsumesDatChildren => false; - // ── Virtual overrides ─────────────────────────────────────────────── + // ── Virtual overrides ─────────────────────────────────────────────── /// /// Draw THIS element (not its children). Children are composited by @@ -384,7 +384,7 @@ internal abstract class UiElement /// /// Draw content that must sit ON TOP of the ENTIRE UI, regardless of this - /// element's position in the tree — open menus, dropdowns, tooltips. Called in + /// element's position in the tree — open menus, dropdowns, tooltips. Called in /// a SECOND traversal after the whole tree's pass, with the /// same accumulated transform/alpha this element had during its normal draw. /// Retail spawns popups as ROOT elements (UIElement_Menu::MakePopup) for exactly @@ -425,9 +425,9 @@ internal abstract class UiElement public virtual object? GetDragPayload() => null; /// The texture paints at the cursor while this element - /// is the drag source: (texture-table slot, width, height). Null = no ghost. Keeps + /// is the drag source: (GL handle, width, height). Null = no ghost. Keeps /// item-agnostic. Retail analog: m_dragIcon (decomp 229738). - public virtual (GpuTextureSlot tex, int w, int h)? GetDragGhost() => null; + public virtual (uint tex, int w, int h)? GetDragGhost() => null; /// /// Notifies the source widget when the root starts or finishes carrying its drag payload. @@ -444,7 +444,7 @@ internal abstract class UiElement /// public virtual string? GetTooltipText() => null; - // ── Framework entry points (internal, called by UiRoot) ───────────── + // ── Framework entry points (internal, called by UiRoot) ───────────── internal void DrawSelfAndChildren(UiRenderContext ctx) { @@ -551,7 +551,7 @@ internal abstract class UiElement return null; // Children first, in reverse Z-order (topmost first). ClickThrough means - // THIS element is transparent to the pointer — but its children are NOT. + // THIS element is transparent to the pointer — but its children are NOT. // A ClickThrough container (e.g. a UiDatElement panel that hosts the chat // input / transcript) must still let the pointer reach its behavioral // children, so the ClickThrough check happens AFTER the child walk, gating @@ -571,7 +571,7 @@ internal abstract class UiElement return OnHitTest(localX, localY) ? this : null; } - // ── Anchor layout ──────────────────────────────────────────────────── + // ── Anchor layout ──────────────────────────────────────────────────── private bool _anchorCaptured; private float _amL, _amT, _amR, _amB, _aw0, _ah0; @@ -659,7 +659,7 @@ internal abstract class UiElement } /// Walk up to the owning (the top of the tree), or null - /// if this element is not attached. Lets a widget reach focus/capture services — e.g. + /// if this element is not attached. Lets a widget reach focus/capture services — e.g. /// a chat input blurring itself (exiting write mode) after submit. internal UiRoot? FindRoot() { @@ -668,8 +668,8 @@ internal abstract class UiElement return e as UiRoot; } - /// Compute an anchored child rect. Left&Right ⇒ stretch width - /// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise + /// Compute an anchored child rect. Left&Right ⇒ stretch width + /// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise /// pin left at fixed width. Same logic vertically. public static (float x, float y, float w, float h) ComputeAnchoredRect( AnchorEdges a, float mL, float mT, float mR, float mB, diff --git a/src/AcDream.App/UI/UiEvent.cs b/src/AcDream.App/UI/UiEvent.cs index 073936b5..044026b6 100644 --- a/src/AcDream.App/UI/UiEvent.cs +++ b/src/AcDream.App/UI/UiEvent.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -10,16 +10,16 @@ namespace AcDream.App.UI; /// Layout from decompiled chunk_004A0000.c paperdoll handler /// FUN_004A5FA0: /// -/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source) +/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source) /// void* target_widget; // param_2[1] -/// int event_type; // param_2[2] — see UiEventType +/// int event_type; // param_2[2] — see UiEventType /// int data0; // param_2[3] -/// int data1; // param_2[4] — typically x in local coords -/// int data2; // param_2[5] — typically y +/// int data1; // param_2[4] — typically x in local coords +/// int data2; // param_2[5] — typically y /// int data3; // param_2[6] /// /// -internal readonly record struct UiEvent( +public readonly record struct UiEvent( uint SourceId, UiElement? Target, int Type, // see @@ -33,22 +33,22 @@ internal readonly record struct UiEvent( /// Retail AC UI event-type constants. Each value matches the decompiled /// switch-case in widgets' OnEvent handlers (e.g. 0x01 click, 0x15 drag /// begin, 0x3E drop released). Win32 WM_* numbers are reused for raw -/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches +/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches /// retail where internal event codes collide deliberately with WM_*. /// /// Evidence from decompile: -/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270 -/// - 0x05/0x06 hover — chunk_00460000.c ~6253 -/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the +/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270 +/// - 0x05/0x06 hover — chunk_00460000.c ~6253 +/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the /// hovered element's tooltip deadline before global time) -/// - 0x0A scroll — chunk_00470000.c ~11210 -/// - 0x0E right-click— chunk_004A0000.c ~2674 -/// - 0x15 drag begin — chunk_004A0000.c ~2707 -/// - 0x1C drag-over — chunk_004A0000.c ~2723 -/// - 0x21 drag-enter — chunk_004A0000.c ~2714 -/// - 0x3E drop-released — chunk_004A0000.c ~2754 +/// - 0x0A scroll — chunk_00470000.c ~11210 +/// - 0x0E right-click— chunk_004A0000.c ~2674 +/// - 0x15 drag begin — chunk_004A0000.c ~2707 +/// - 0x1C drag-over — chunk_004A0000.c ~2723 +/// - 0x21 drag-enter — chunk_004A0000.c ~2714 +/// - 0x3E drop-released — chunk_004A0000.c ~2754 /// -internal static class UiEventType +public static class UiEventType { public const int Click = 0x01; public const int HoverEnter = 0x05; @@ -82,7 +82,7 @@ internal static class UiEventType /// /// Mouse button enum matching retail's 1/2/3 encoding. /// -internal enum UiMouseButton +public enum UiMouseButton { Left = 1, Right = 2, diff --git a/src/AcDream.App/UI/UiField.cs b/src/AcDream.App/UI/UiField.cs index f802751a..8cb0af79 100644 --- a/src/AcDream.App/UI/UiField.cs +++ b/src/AcDream.App/UI/UiField.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -11,16 +11,16 @@ namespace AcDream.App.UI; /// as stubs for future item-window use. /// /// -/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the +/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the /// caret. Supports mouse + Shift-arrow SELECTION, clipboard cut/copy/paste, and /// held-key auto-repeat (hold Backspace deletes continuously). Submit (Enter / Send) /// fires , clears, and pushes history (100-entry cap, -/// sentinel 0xFFFFFFFF — port of ChatInterface::ProcessCommand @0x4f5100). +/// sentinel 0xFFFFFFFF — port of ChatInterface::ProcessCommand @0x4f5100). /// /// /// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40. /// -internal sealed class UiField : UiElement +public sealed class UiField : UiElement { private readonly record struct WrappedLine(int Start, int Length, string Text); @@ -71,9 +71,9 @@ internal sealed class UiField : UiElement /// Wired by the host from . public Silk.NET.Input.IKeyboard? Keyboard { get; set; } - /// Dat sprite resolver (id → GL texture + size) for the focused-field + /// Dat sprite resolver (id → GL texture + size) for the focused-field /// background. Null = fall back to the flat rect. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// Unfocused/default state sprite imported from the DAT. public uint BackgroundSprite { get; set; } /// Gold "lit" field background drawn when focused (retail Normal_focussed @@ -120,7 +120,7 @@ internal sealed class UiField : UiElement /// are reproduced procedurally, so the importer must not build them as widgets. public override bool ConsumesDatChildren => true; - // ── Editing primitives ────────────────────────────────────────────── + // ── Editing primitives ────────────────────────────────────────────── public void InsertChar(char c) { @@ -169,7 +169,7 @@ internal sealed class UiField : UiElement private void MoveCaret(int delta, bool shift) => MoveCaretTo(_caret + delta, shift); - // ── Selection ──────────────────────────────────────────────────────── + // ── Selection ──────────────────────────────────────────────────────── private (int lo, int hi) SelSpan() { @@ -268,7 +268,7 @@ internal sealed class UiField : UiElement _historyIndex = -1; } - // ── Submit + history ───────────────────────────────────────────────── + // ── Submit + history ───────────────────────────────────────────────── public void Submit() { @@ -315,9 +315,9 @@ internal sealed class UiField : UiElement _selAnchor = null; } - // ── Geometry ───────────────────────────────────────────────────────── + // ── Geometry ───────────────────────────────────────────────────────── - /// Pixel-X of the caret (Σ glyph advances to ). + /// Pixel-X of the caret (Σ glyph advances to ). private float MeasureTo(int i) { if (i <= 0) return 0f; @@ -328,7 +328,7 @@ internal sealed class UiField : UiElement public float CaretPixelX() => MeasureTo(_caret); - /// Map a local X (click) to the nearest caret index — retail + /// Map a local X (click) to the nearest caret index — retail /// FindPixelsFromPos inverse. Accounts for the horizontal scroll offset. private int HitCharX(float localX) { @@ -344,25 +344,25 @@ internal sealed class UiField : UiElement return best; } - // ── Draw ───────────────────────────────────────────────────────────── + // ── Draw ───────────────────────────────────────────────────────────── protected override void OnDraw(UiRenderContext ctx) { // Focused = "write mode": draw the gold lit field sprite (retail Normal_focussed). // Unfocused: draw the imported default state, then the flat translucent fallback. // Both go through the sprite bucket - // (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top. + // (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top. bool lit = _focused && SpriteResolve is not null && FocusFieldSprite != 0; if (lit) { var (tex, tw, th) = SpriteResolve!(FocusFieldSprite); - if (tex.IsAssigned && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); + if (tex != 0 && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); else lit = false; } if (!lit && SpriteResolve is not null && BackgroundSprite != 0) { var (tex, tw, th) = SpriteResolve(BackgroundSprite); - if (tex.IsAssigned && tw > 0 && th > 0) + if (tex != 0 && tw > 0 && th > 0) { ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, Width / tw, Height / th, Vector4.One); @@ -382,7 +382,7 @@ internal sealed class UiField : UiElement float visibleW = MathF.Max(1f, Width - 2f * Padding); // Horizontal scroll: keep the caret inside the field; clamp so we never scroll past - // the text. Then draw only the glyph window that lands inside the field — a single- + // the text. Then draw only the glyph window that lands inside the field — a single- // line text box clips + scrolls (retail UIElement_Text) rather than overflowing the // field (which previously spilled the text out into the 3D world). float caretX = MeasureTo(_caret); @@ -417,7 +417,7 @@ internal sealed class UiField : UiElement if (_focused) { - // Caret on TOP of the text → submitted after the text in the same bucket. + // Caret on TOP of the text → submitted after the text in the same bucket. float cx = Padding + alignX + (caretX - _scrollX); if (cx >= Padding - 1f && cx <= Width - Padding + 1f) ctx.DrawFill(cx, ty, 1f, lh, TextColor); @@ -620,7 +620,7 @@ internal sealed class UiField : UiElement return line.Start + best; } - // ── Auto-repeat ────────────────────────────────────────────────────── + // ── Auto-repeat ────────────────────────────────────────────────────── protected override void OnTick(double deltaSeconds) { @@ -649,7 +649,7 @@ internal sealed class UiField : UiElement && (Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftLeft) || Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftRight)); - // ── Events ─────────────────────────────────────────────────────────── + // ── Events ─────────────────────────────────────────────────────────── public override bool OnEvent(in UiEvent e) { diff --git a/src/AcDream.App/UI/UiHost.cs b/src/AcDream.App/UI/UiHost.cs index 41bae50a..6fb21eb6 100644 --- a/src/AcDream.App/UI/UiHost.cs +++ b/src/AcDream.App/UI/UiHost.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using Silk.NET.Input; +using Silk.NET.OpenGL; namespace AcDream.App.UI; @@ -14,7 +14,7 @@ namespace AcDream.App.UI; /// /// Usage (from GameWindow.OnLoad): /// -/// _uiHost = new UiHost(_gpuDevice, () => _gpuFrameLifetime.Current, _debugFont); +/// _uiHost = new UiHost(_gl, shadersDir, _debugFont); /// _uiHost.Root.WorldMouseFallThrough += (btn, x, y, f) => HandleWorldClick(btn, x, y); /// _uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk); /// @@ -35,7 +35,7 @@ namespace AcDream.App.UI; /// (Keystone root, widget tree). We fuse them into a single host class /// because we're not linking to Keystone. /// -internal sealed class UiHost : System.IDisposable +public sealed class UiHost : System.IDisposable { public UiRoot Root { get; } = new(); public RetailWindowManager WindowManager => Root.WindowManager; @@ -44,13 +44,12 @@ internal sealed class UiHost : System.IDisposable /// The last wired keyboard. Exposed so widgets that need clipboard /// access () or modifier-key state - /// () — e.g. 's - /// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins. + /// () — e.g. 's + /// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins. public IKeyboard? Keyboard { get; private set; } private long _startTicks = System.Environment.TickCount64; private readonly HostQuiescenceGate _quiescence; - private readonly Func _currentFrame; private readonly List _inputBindings = new(); private ResourceShutdownTransaction? _inputShutdown; private ResourceShutdownTransaction? _shutdown; @@ -59,24 +58,23 @@ internal sealed class UiHost : System.IDisposable internal bool IsDisposalComplete => _disposed; - public UiHost(IGpuDevice device, Func currentFrame, BitmapFont? defaultFont = null) - : this(device, currentFrame, defaultFont, new HostQuiescenceGate()) + public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null) + : this(gl, shaderDir, defaultFont, new HostQuiescenceGate()) { } internal UiHost( - IGpuDevice device, - Func currentFrame, + GL gl, + string shaderDir, BitmapFont? defaultFont, HostQuiescenceGate quiescence) { _quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence)); - _currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame)); - TextRenderer = new TextRenderer(device); + TextRenderer = new TextRenderer(gl, shaderDir); DefaultFont = defaultFont; } - // ── Per-frame ────────────────────────────────────────────────────── + // ── Per-frame ────────────────────────────────────────────────────── public void Tick(double deltaSeconds) { @@ -92,10 +90,10 @@ internal sealed class UiHost : System.IDisposable var ctx = new UiRenderContext(TextRenderer, screenSize, DefaultFont); TextRenderer.Begin(screenSize); Root.Draw(ctx); - TextRenderer.Flush(DefaultFont, _currentFrame()); + TextRenderer.Flush(DefaultFont); } - // ── Input wiring helpers ─────────────────────────────────────────── + // ── Input wiring helpers ─────────────────────────────────────────── public void WireMouse(IMouse mouse) { @@ -185,7 +183,7 @@ internal sealed class UiHost : System.IDisposable } } - // ── Window manager forwarders (delegate to UiRoot) ───────────────── + // ── Window manager forwarders (delegate to UiRoot) ───────────────── /// Register a top-level window for Show/Hide/Toggle. See . public RetailWindowHandle RegisterWindow( diff --git a/src/AcDream.App/UI/UiItemList.cs b/src/AcDream.App/UI/UiItemList.cs index abdf9d86..d96b3654 100644 --- a/src/AcDream.App/UI/UiItemList.cs +++ b/src/AcDream.App/UI/UiItemList.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; namespace AcDream.App.UI; /// Cell order for a multi-column . -internal enum UiItemListFlow +public enum UiItemListFlow { /// Retail listbox bit 0 set: index advances across columns, then down rows. RowMajor, @@ -19,7 +19,7 @@ internal enum UiItemListFlow /// LayoutImporter must NOT build dat children. The toolbar uses single-cell /// instances (one slot); the inventory phase will grow this to an N-cell grid. /// -internal sealed class UiItemList : UiElement +public sealed class UiItemList : UiElement { private readonly List _cells = new(); private int _layoutDeferralDepth; @@ -29,7 +29,7 @@ internal sealed class UiItemList : UiElement public UiScrollable Scroll { get; } public UiItemList( - Func? spriteResolve = null, + Func? spriteResolve = null, UiScrollable? scroll = null) { Scroll = scroll ?? new UiScrollable(); @@ -40,7 +40,7 @@ internal sealed class UiItemList : UiElement public override bool ConsumesDatChildren => true; - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } private IReadOnlyList? _cooldownSprites; public IReadOnlyList? CooldownSprites @@ -165,7 +165,7 @@ internal sealed class UiItemList : UiElement cell.CooldownStepProvider ??= _cooldownStepProvider; if (_cellEmptySprite != 0) cell.EmptySprite = _cellEmptySprite; // The list lays cells out procedurally (grid offset + scroll clip), so cells must be - // EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor + // EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor // on every child after OnDraw, which would otherwise reset the scroll offset to each // cell's captured base position (the escaping-grid bug). Anchors=None makes LayoutCells // the sole authority over cell rects. @@ -197,7 +197,7 @@ internal sealed class UiItemList : UiElement /// public UiItemListFlow Flow { get; set; } = UiItemListFlow.RowMajor; - /// Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes + /// Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes /// to the whole list (the toolbar single-slot legacy). Set >0 (with CellHeight) for a grid. public float CellWidth { get; set; } /// Fixed cell height in grid mode (pairs with CellWidth). diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index faf2b0a2..898c14af 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -1,24 +1,16 @@ -using System; +using System; using System.Numerics; using AcDream.Core.Items; namespace AcDream.App.UI; -/// -/// Accept/reject overlay state while a drag hovers an item slot cell. Top-level -/// (not nested in , which is internal) because -/// CursorFeedbackSnapshot is a public test-visible type that carries this -/// value. -/// -public enum DragAcceptState { None, Accept, Reject } - /// /// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032). /// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a /// pre-composited icon texture (set by the controller). Holds the bound weenie /// guid (retail UIElement_UIItem::itemID, +0x5FC). /// -internal class UiItemSlot : UiElement +public class UiItemSlot : UiElement { public UiItemSlot() { ClickThrough = false; } @@ -27,15 +19,15 @@ internal class UiItemSlot : UiElement /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID. public uint ItemId { get; private set; } - /// Pre-composited icon texture-table slot for the bound item (Unassigned = none). - public GpuTextureSlot IconTexture { get; private set; } = GpuTextureSlot.Unassigned; + /// Pre-composited icon GL texture for the bound item (0 = none). + public uint IconTexture { get; private set; } /// /// Underlay-free cursor graphic for the bound item (retail IconData::m_pDragIcon). /// Falls back to only for callers that have not supplied the /// dedicated composite. /// - public GpuTextureSlot DragIconTexture { get; private set; } = GpuTextureSlot.Unassigned; + public uint DragIconTexture { get; private set; } /// /// Lossless shortcut record when this cell belongs to the toolbar. Physical item @@ -44,7 +36,7 @@ internal class UiItemSlot : UiElement public ShortcutEntry? Shortcut { get; private set; } /// This cell's own index within its panel (0..17 toolbar; container slot - /// for inventory). Distinct from (the 1–9 label, -1 on the + /// for inventory). Distinct from (the 1–9 label, -1 on the /// bottom row). Set by the controller; used as the drag payload's SourceSlot and to /// identify the drop TARGET slot. public int SlotIndex { get; set; } = -1; @@ -62,24 +54,24 @@ internal class UiItemSlot : UiElement /// True when this cell is the OPEN container (its contents fill the grid). Draws the /// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator - /// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId. + /// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId. public bool IsOpenContainer { get; set; } /// Open-container triangle sprite (element 0x10000450 on the container prototype /// 0x1000033F). Configurable; guard id != 0 before resolving. public uint OpenContainerSprite { get; set; } = 0x06005D9Cu; /// True when this cell is the SELECTED item. Draws the green/yellow selection square. - /// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState + /// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState /// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells. public bool Selected { get; set; } - /// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype + /// Selected-item square sprite (element 0x10000342 on the 32×32 item prototype /// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders - /// on the 36×36 container cell too (whose prototype lacks the square child). + /// on the 36×36 container cell too (whose prototype lacks the square child). public uint SelectedSprite { get; set; } = 0x06004D21u; /// Container fullness [0..1], or -1 = hidden (the cell is not a container, or its /// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a - /// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when + /// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when /// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69). public float CapacityFill { get; set; } = -1f; /// Capacity-bar track sprite (meter 0x10000347 DirectState). @@ -87,8 +79,10 @@ internal class UiItemSlot : UiElement /// Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState). public uint CapacityFrontSprite { get; set; } = 0x06004D23u; + /// Accept/reject overlay state while a drag hovers this cell. + public enum DragAcceptState { None, Accept, Reject } private DragAcceptState _dragAccept = DragAcceptState.None; - /// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo). + /// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo). internal DragAcceptState DragAcceptVisual => _dragAccept; /// Empty-slot sprite. Default = the generic toolbar empty-slot border @@ -108,7 +102,7 @@ internal class UiItemSlot : UiElement private bool _primaryPressConsumed; /// RenderSurface id -> (GL texture, w, h). Set by the factory/controller. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// /// DAT-authored 10%..100% radial cooldown overlays from element ids @@ -124,21 +118,21 @@ internal class UiItemSlot : UiElement public void SetItem( uint itemId, - GpuTextureSlot iconTexture, + uint iconTexture, ShortcutEntry? shortcut = null, - GpuTextureSlot? dragIconTexture = null) + uint dragIconTexture = 0) { ItemId = itemId; IconTexture = iconTexture; - DragIconTexture = dragIconTexture ?? GpuTextureSlot.Unassigned; + DragIconTexture = dragIconTexture; Shortcut = shortcut; } public void Clear() { ItemId = 0; - IconTexture = GpuTextureSlot.Unassigned; - DragIconTexture = GpuTextureSlot.Unassigned; + IconTexture = 0; + DragIconTexture = 0; Shortcut = null; _waiting = false; _primaryPressConsumed = false; @@ -151,13 +145,13 @@ internal class UiItemSlot : UiElement : null; /// - public override (GpuTextureSlot tex, int w, int h)? GetDragGhost() + public override (uint tex, int w, int h)? GetDragGhost() { if (ItemId == 0) return null; - // RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its - // containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16). - if (DragIconTexture.IsAssigned) return (DragIconTexture, 32, 32); - return IconTexture.IsAssigned ? (IconTexture, (int)Width, (int)Height) : null; + // RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its + // containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16). + if (DragIconTexture != 0) return (DragIconTexture, 32, 32); + return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null; } /// @@ -177,7 +171,7 @@ internal class UiItemSlot : UiElement internal void SetWaitingState(bool waiting) => _waiting = waiting && ItemId != 0; - /// An OCCUPIED slot is a drag source — a press-and-move picks up the item + /// An OCCUPIED slot is a drag source — a press-and-move picks up the item /// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a /// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar /// movable by its empty cells / chrome. Drives 's mousedown @@ -193,23 +187,23 @@ internal class UiItemSlot : UiElement return null; } - // ── Shortcut number (slot label) ───────────────────────────────────────── + // ── Shortcut number (slot label) ───────────────────────────────────────── // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Retail draws the digit on the cell's ShortcutNum sub-element, picking the // digit image from a DID-array property: 0x10000042 (regular) / 0x10000043 (ghosted), - // indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface + // indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface // with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend. /// Slot position in the shortcut bar (0-indexed). -1 = no number (retail - /// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1. + /// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1. public int ShortcutNum { get; private set; } = -1; /// True when retail marks the physical-item shortcut as ghosted. public bool ShortcutGhosted { get; private set; } - /// Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id. + /// Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id. /// Injected by the controller after reading LayoutDesc 0x21000037. - /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks + /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks /// property 0x10000042 when the shortcut is not ghosted. public uint[]? RegularDigits { get; set; } @@ -219,7 +213,7 @@ internal class UiItemSlot : UiElement /// Empty-slot digit DID array (property 0x1000005e, stance-independent). /// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum - /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty). + /// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty). public uint[]? EmptyDigits { get; set; } /// @@ -247,8 +241,8 @@ internal class UiItemSlot : UiElement /// /// Returns the digit DID array that OnDraw will use, following the retail occupancy /// branch in UIElement_UIItem::SetShortcutNum (decomp 229481): - /// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42) - /// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent) + /// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42) + /// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent) /// Exposed as an internal method so unit tests can assert array selection without /// needing a real render context. /// @@ -259,7 +253,7 @@ internal class UiItemSlot : UiElement : EmptyDigits; } - // ── Events / draw ───────────────────────────────────────────────────────── + // ── Events / draw ───────────────────────────────────────────────────────── /// Invoked by when a completed left click lands /// on a bound slot. Selection already occurred on MouseDown through the owning @@ -275,7 +269,7 @@ internal class UiItemSlot : UiElement { switch (e.Type) { - // Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a + // Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a // drag (press + >3px move) does NOT also use the item. UiRoot suppresses the // post-drag Click (UiRoot.cs FinishDrag returns before the Click emit). case UiEventType.MouseDown: @@ -302,13 +296,13 @@ internal class UiItemSlot : UiElement return true; case UiEventType.DragBegin: - // Notify the source list's handler so it can lift (remove + wire) — retail - // RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first. + // Notify the source list's handler so it can lift (remove + wire) — retail + // RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first. if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp) lh.OnDragLift(liftList, this, lp); return true; - case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler + case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler _dragAccept = FindList() is { DragHandler: { } h } list && e.Payload is ItemDragPayload p ? h.OnDragOver(list, this, p) switch @@ -320,7 +314,7 @@ internal class UiItemSlot : UiElement : DragAcceptState.Reject; return true; - case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral + case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral _dragAccept = DragAcceptState.None; return true; @@ -337,43 +331,43 @@ internal class UiItemSlot : UiElement { // Draw the icon (filled slot) or the empty-slot border. Both paths fall through // to the digit draw below; the slot label always shows on top-row slots. - if (ItemId != 0 && IconTexture.IsAssigned) + if (ItemId != 0 && IconTexture != 0) { ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } else if (SpriteResolve is not null && EmptySprite != 0) { var (tex, _, _) = SpriteResolve(EmptySprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } // Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Occupancy branch (decomp 229481): - // occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43 - // empty (ItemId == 0) → background digit set 0x1000005e, stance-independent + // occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43 + // empty (ItemId == 0) → background digit set 0x1000005e, stance-independent // Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn // full-cell Alphablend so the transparent region is invisible. DrawShortcutOverlay(ctx); - // Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical - // UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells + // Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical + // UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells // (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the - // "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first. + // "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first. if (CapacityFill >= 0f && SpriteResolve is not null) { - const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1) + const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1) float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge) if (CapacityBackSprite != 0) { var (bt, _, _) = SpriteResolve(CapacityBackSprite); - if (bt.IsAssigned) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One); + if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One); } float f = Math.Clamp(CapacityFill, 0f, 1f); if (f > 0f && CapacityFrontSprite != 0) { var (ft, _, _) = SpriteResolve(CapacityFrontSprite); - if (ft.IsAssigned) + if (ft != 0) { // Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching // bottom slice of the front sprite (UV v from 1-f to 1). @@ -383,7 +377,7 @@ internal class UiItemSlot : UiElement } } - // Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0) + // Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0) // reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0) // sets it for physical inventory/equipment cells while leaving their icon in place. // Draw it before the persistent selected/open indicators: retail keeps selection visible @@ -391,7 +385,7 @@ internal class UiItemSlot : UiElement if (_waiting && SpriteResolve is not null && WaitingSprite != 0) { var (tex, _, _) = SpriteResolve(WaitingSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } @@ -402,18 +396,18 @@ internal class UiItemSlot : UiElement if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0) { var (tex, _, _) = SpriteResolve(OpenContainerSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } if (Selected && SpriteResolve is not null && SelectedSprite != 0) { var (tex, _, _) = SpriteResolve(SelectedSprite); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } // Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40). - // Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder + // Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder // with a non-zero GL handle (feedback_ui_resolve_zero_magenta). if (_dragAccept != DragAcceptState.None && SpriteResolve is not null) { @@ -421,7 +415,7 @@ internal class UiItemSlot : UiElement if (id != 0) { var (tex, _, _) = SpriteResolve(id); - if (tex.IsAssigned) + if (tex != 0) ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } } @@ -434,7 +428,7 @@ internal class UiItemSlot : UiElement if (cooldownSprite != 0u && SpriteResolve is not null) { var (texture, _, _) = SpriteResolve(cooldownSprite); - if (texture.IsAssigned) + if (texture != 0u) ctx.DrawSprite( texture, 0f, @@ -466,7 +460,7 @@ internal class UiItemSlot : UiElement return; var (texture, _, _) = SpriteResolve(did); - if (texture.IsAssigned) + if (texture != 0) ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiLayoutPolicy.cs b/src/AcDream.App/UI/UiLayoutPolicy.cs index 5d5f2269..bd1af24e 100644 --- a/src/AcDream.App/UI/UiLayoutPolicy.cs +++ b/src/AcDream.App/UI/UiLayoutPolicy.cs @@ -1,7 +1,7 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// An inclusive integer pixel box, matching retail Box2D. -internal readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) +public readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) { public int Width => X1 - X0 + 1; public int Height => Y1 - Y0 + 1; @@ -16,7 +16,7 @@ internal readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1) /// retain all four independent modes; programmatic widgets continue to use /// . /// -internal sealed class UiLayoutPolicy +public sealed class UiLayoutPolicy { public uint LeftMode { get; } public uint TopMode { get; } diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs index b84f5f35..c10bd419 100644 --- a/src/AcDream.App/UI/UiMenu.cs +++ b/src/AcDream.App/UI/UiMenu.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; @@ -13,10 +13,10 @@ namespace AcDream.App.UI; /// knowledge are populated by the controller, not baked into this widget. Built /// by for Type-6 elements. /// -internal sealed class UiMenu : UiElement +public sealed class UiMenu : UiElement { /// One menu row: its label + an opaque payload the controller maps back. - internal readonly record struct MenuItem(string Label, object? Payload); + public readonly record struct MenuItem(string Label, object? Payload); /// The rows, populated by the controller. Laid out column-major: /// rows 0..RowsPerColumn-1 in column 0, then the next group in column 1, etc. @@ -28,10 +28,10 @@ internal sealed class UiMenu : UiElement /// Fired with the picked item's payload when a row is chosen. public Action? OnSelect { get; set; } - /// Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled. + /// Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled. public Func? EnabledProvider { get; set; } - /// Button-face caption (the active target). Null ⇒ blank face. + /// Button-face caption (the active target). Null ⇒ blank face. public Func? ButtonLabelProvider { get; set; } public int RowsPerColumn { get; set; } = 7; // items per column (dat item template) @@ -43,29 +43,29 @@ internal sealed class UiMenu : UiElement // square; the label starts just past it (box width + small gap) so text aligns with // the box instead of overlapping it. private const float TextIndent = 19f; - // The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its - // left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't + // The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its + // left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't // render over the LED. private const float ButtonTextIndent = 20f; public UiDatFont? DatFont { get; set; } public AcDream.App.Rendering.BitmapFont? Font { get; set; } - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } // Button face sprites (dat menu element 0x10000014). public uint NormalSprite { get; set; } public uint PressedSprite { get; set; } // Popup chrome sprites (dat menu popup template, layout 0x21000006). - public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles) - public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17) - public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row + public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles) + public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17) + public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f); - /// Available item text — retail white #FFFFFF (gmMainChatUI talk-focus + /// Available item text — retail white #FFFFFF (gmMainChatUI talk-focus /// enabled state). Confirmed via decomp: enabled items render white. public Vector4 TextColorAvailable { get; set; } = new(1f, 1f, 1f, 1f); - /// Disabled/unavailable item text — retail GREYS these (UIElement state 0xd - /// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that + /// Disabled/unavailable item text — retail GREYS these (UIElement state 0xd + /// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that /// belongs to the chat-MESSAGE palette and was misapplied. Exact float lives in the dat /// StateDesc (not a code symbol); ~0.5 neutral grey here pending a live cdb dump. public Vector4 TextColorGhosted { get; set; } = new(0.5f, 0.5f, 0.5f, 1f); @@ -92,7 +92,7 @@ internal sealed class UiMenu : UiElement if (resolve is not null) { var (tex, tw, _) = resolve(_open ? PressedSprite : NormalSprite); - if (tex.IsAssigned && tw > 0) DrawButtonFace(ctx, tex, tw); + if (tex != 0 && tw > 0) DrawButtonFace(ctx, tex, tw); } DrawLabel(ctx, ButtonLabelProvider?.Invoke() ?? "", ButtonTextIndent, (Height - LineH()) * 0.5f, TextColor); } @@ -102,7 +102,7 @@ internal sealed class UiMenu : UiElement // point. Slicing keeps the LED + arrow undistorted when the button widens to its label. private const float FaceCapL = 20f, FaceCapR = 12f; - private void DrawButtonFace(UiRenderContext ctx, GpuTextureSlot tex, float tw) + private void DrawButtonFace(UiRenderContext ctx, uint tex, float tw) { float uL = FaceCapL / tw, uR = (tw - FaceCapR) / tw; float midDest = Width - FaceCapL - FaceCapR; @@ -112,7 +112,7 @@ internal sealed class UiMenu : UiElement ctx.DrawSprite(tex, Width - FaceCapR, 0f, FaceCapR, Height, uR, 0f, 1f, 1f, Vector4.One); // arrow cap } - /// The button width that fits "LED cap + channel label + arrow cap" — retail + /// The button width that fits "LED cap + channel label + arrow cap" — retail /// sizes the talk-focus button to its selected label. The controller widens the button /// to this and reflows the input field to start after it. public float NaturalButtonWidth() @@ -123,7 +123,7 @@ internal sealed class UiMenu : UiElement } /// The open popup draws in the OVERLAY pass so it sits on top of the whole - /// UI — otherwise the translucent chat panel (drawn after this element in the main + /// UI — otherwise the translucent chat panel (drawn after this element in the main /// pass) greys out the part of the popup that overlaps it. protected override void OnDrawOverlay(UiRenderContext ctx) { @@ -133,7 +133,7 @@ internal sealed class UiMenu : UiElement // Column-major popup opening UPWARD from the button, wrapped in the universal // 8-piece window bevel (retail UIElement_Menu::MakePopup spawns the popup as a // bevelled floating window). Force OPAQUE (a menu reads solid even though the - // chat window is translucent). Draw bevel → panel fill → row sprites → labels, + // chat window is translucent). Draw bevel → panel fill → row sprites → labels, // all through the sprite bucket in submission order so labels land on top. ctx.PushAlphaAbsolute(1f); try @@ -170,7 +170,7 @@ internal sealed class UiMenu : UiElement /// ,). Reuses the same geometry + /// ids as ; no resize /// grips (a menu popup is not resizable). - private void DrawBevel(UiRenderContext ctx, Func resolve, + private void DrawBevel(UiRenderContext ctx, Func resolve, float x, float y, float w, float h) { var r = UiNineSlicePanel.ComputeFrameRects(w, h, Border); @@ -188,13 +188,13 @@ internal sealed class UiMenu : UiElement private float LineH() => DatFont?.LineHeight ?? Font?.LineHeight ?? 14f; - private void DrawSprite(UiRenderContext ctx, Func resolve, + private void DrawSprite(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; - // Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1). + if (tex == 0 || tw == 0 || th == 0) return; + // Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1). ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One); } @@ -230,7 +230,7 @@ internal sealed class UiMenu : UiElement // The widget REPORTS the pick; the controller owns Selected (it sets // Selected only for payloads it acts on). This mirrors retail // UIElement_Menu::NewSelection delegating to the owner rather than - // self-selecting — so a deferred/no-op item (e.g. the Squelch / + // self-selecting — so a deferred/no-op item (e.g. the Squelch / // Tell-to-Selected specials, null payload) leaves the current // selection + highlight unchanged when the controller ignores it. OnSelect?.Invoke(Items[idx].Payload); diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs index 82813761..d4aac0cc 100644 --- a/src/AcDream.App/UI/UiMeter.cs +++ b/src/AcDream.App/UI/UiMeter.cs @@ -1,20 +1,20 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; /// /// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a /// partial-width solid fill, and an optional centered "current/max" numeric -/// overlay. returns 0..1 (null = no data → empty bar); +/// overlay. returns 0..1 (null = no data → empty bar); /// returns the overlay text (null = no number). /// /// /// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite -/// (glassy center highlight) and the retail dat font are a later polish pass — +/// (glassy center highlight) and the retail dat font are a later polish pass — /// retail's vitals are bars exactly like this, just sprited. /// /// -internal sealed class UiMeter : UiElement, IUiDatStateful +public sealed class UiMeter : UiElement, IUiDatStateful { private readonly Dictionary _stateFillSprites = new(); @@ -37,13 +37,13 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// Resolver from a RenderSurface DataId to (GL handle, w, h). When set /// with the 9-slice ids below, the bar draws the retail sprites instead of solid color. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } // Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap, - // a TILED gradient middle (the "fill-tile" repeats at native width — it does not + // a TILED gradient middle (the "fill-tile" repeats at native width — it does not // stretch), and a fixed-width right-cap. The "back" slice is the empty track // (drawn full width); the "front" slice is the coloured fill (drawn full-geometry - // but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's + // but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's // shows through when partial). Ids come from the stacked vitals LayoutDesc // (0x2100006C) via the dump-vitals-layout CLI; 0 = none. /// Empty-track left-cap RenderSurface id. @@ -110,8 +110,8 @@ internal sealed class UiMeter : UiElement, IUiDatStateful } /// Clamp to [0,1] and return the vertical fill rect - /// (local px). true → the fill occupies the bottom - /// h*pct px (retail direction 4); false → the top (direction 2). + /// (local px). true → the fill occupies the bottom + /// h*pct px (retail direction 4); false → the top (direction 2). public static (float x, float y, float w, float h) ComputeVFillRect( float pct, float w, float h, bool fromBottom) { @@ -142,7 +142,7 @@ internal sealed class UiMeter : UiElement, IUiDatStateful // empty track, drawn full width; the FRONT 3-slice is the coloured fill, // drawn at FULL width too but horizontally CLIPPED to the fill fraction. // The front carries its own right-cap (shown at 100%); clipping below 100% - // removes it and reveals the back track's right-cap — retail's scissor-fill. + // removes it and reveals the back track's right-cap — retail's scissor-fill. DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width); if (pct is not null && p > 0f) DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p); @@ -191,7 +191,7 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// A 0 id skips that slice. /// private void DrawHBar( - UiRenderContext ctx, Func resolve, + UiRenderContext ctx, Func resolve, uint leftId, uint midId, uint rightId, float clipW) { if (clipW <= 0f) return; @@ -201,18 +201,18 @@ internal sealed class UiMeter : UiElement, IUiDatStateful // testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar // selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets // all six ids. Guard on the id, not the resolved handle. - var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (GpuTextureSlot.Unassigned, 0, 0); - var (mt, mw, _) = midId != 0 ? resolve(midId) : (GpuTextureSlot.Unassigned, 0, 0); - var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (GpuTextureSlot.Unassigned, 0, 0); + var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0); + var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0); + var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0); - float capL = lt.IsAssigned ? MathF.Min(lw, w) : 0f; - float capR = rt.IsAssigned ? MathF.Min(rw, w - capL) : 0f; + float capL = lt != 0 ? MathF.Min(lw, w) : 0f; + float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f; float midW = w - capL - capR; // Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI - // texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their - // own native width → a single 1:1 copy. The wide middle spans many native - // widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather + // texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their + // own native width → a single 1:1 copy. The wide middle spans many native + // widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather // than stretching one copy. (Same UV-repeat the chrome border already uses.) DrawPiece(ctx, lt, 0f, capL, lw, h, clipW); DrawPiece(ctx, mt, capL, midW, mw, h, clipW); @@ -224,12 +224,12 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// bottom () or top, UV-cropped to that fraction of the /// sprite so the fill reveals the matching part of the art (retail /// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op. - private void DrawVBar(UiRenderContext ctx, Func resolve, + private void DrawVBar(UiRenderContext ctx, Func resolve, uint tileId, float visibleH, bool fromBottom, bool isFill) { if (tileId == 0 || visibleH <= 0f) return; var (tex, _, _) = resolve(tileId); - if (!tex.IsAssigned) return; + if (tex == 0) return; float w = Width, h = Height; if (visibleH > h) visibleH = h; float frac = h > 0f ? visibleH / h : 0f; @@ -243,17 +243,17 @@ internal sealed class UiMeter : UiElement, IUiDatStateful /// Draw a slice over local [, /// pieceX+], with the texture repeating every - /// px (UV-repeat — the UI texture is GL_REPEAT-wrapped). + /// px (UV-repeat — the UI texture is GL_REPEAT-wrapped). /// Clipped so nothing past shows. For a cap (span == native) /// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is /// UV-cropped. private static void DrawPiece( - UiRenderContext ctx, GpuTextureSlot tex, float pieceX, float pieceW, float nativeW, float h, float clipW) + UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW) { - if (!tex.IsAssigned || pieceW <= 0f || nativeW <= 0f) return; + if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return; float visibleW = MathF.Min(pieceW, clipW - pieceX); if (visibleW <= 0f) return; - float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy + float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One); } } diff --git a/src/AcDream.App/UI/UiNineSlicePanel.cs b/src/AcDream.App/UI/UiNineSlicePanel.cs index 68a91e48..a87ce024 100644 --- a/src/AcDream.App/UI/UiNineSlicePanel.cs +++ b/src/AcDream.App/UI/UiNineSlicePanel.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; @@ -10,17 +10,17 @@ namespace AcDream.App.UI; /// the widget is testable without GL. In production: /// id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }. /// -internal class UiNineSlicePanel : UiPanel +public class UiNineSlicePanel : UiPanel { /// A placed chrome piece: destination rect in local pixel space. - internal readonly record struct Rect(float X, float Y, float W, float H); + public readonly record struct Rect(float X, float Y, float W, float H); /// The nine destination rects for an 8-piece border + center. - internal readonly record struct FrameRects( + public readonly record struct FrameRects( Rect Center, Rect Top, Rect Bottom, Rect Left, Rect Right, Rect TL, Rect TR, Rect BL, Rect BR); - private readonly System.Func _resolve; + private readonly System.Func _resolve; /// /// Whether this wrapper paints the shared center surface. Some retail child @@ -29,7 +29,7 @@ internal class UiNineSlicePanel : UiPanel /// public bool DrawCenterFill { get; set; } = true; - public UiNineSlicePanel(System.Func resolve) + public UiNineSlicePanel(System.Func resolve) { _resolve = resolve; BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill @@ -46,7 +46,7 @@ internal class UiNineSlicePanel : UiPanel /// /// Destination rects (local px) for a frame of (, /// ) with border thickness : - /// b×b corners, top/bottom edges spanning the interior width at height b, + /// b×b corners, top/bottom edges spanning the interior width at height b, /// left/right edges spanning the interior height at width b, center fills /// the interior. /// @@ -68,7 +68,7 @@ internal class UiNineSlicePanel : UiPanel protected override void OnDraw(UiRenderContext ctx) { - // Center fill is the window BACKGROUND — it must sit UNDER the content, so it + // Center fill is the window BACKGROUND — it must sit UNDER the content, so it // draws here (before children). The bevel border + grip is the OUTERMOST layer // and draws in OnDrawAfterChildren (over the content's edges) so content can // never poke through the frame (e.g. the toolbar's 2px bottom-right cap overhang). @@ -91,7 +91,7 @@ internal class UiNineSlicePanel : UiPanel DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR); // Resize-grip overlay (gold ridged edges + square corner studs) on top of the - // bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642). + // bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642). DrawTiled(ctx, RetailChromeSprites.GripTop, r.Top); DrawTiled(ctx, RetailChromeSprites.GripBottom, r.Bottom); DrawTiled(ctx, RetailChromeSprites.GripLeft, r.Left); @@ -106,7 +106,7 @@ internal class UiNineSlicePanel : UiPanel { if (d.W <= 0 || d.H <= 0) return; var (tex, tw, th) = _resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, d.W / tw, d.H / th, Vector4.One); } @@ -114,7 +114,7 @@ internal class UiNineSlicePanel : UiPanel { if (d.W <= 0 || d.H <= 0) return; var (tex, _, _) = _resolve(id); - if (!tex.IsAssigned) return; + if (tex == 0) return; ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, 1, 1, Vector4.One); } } diff --git a/src/AcDream.App/UI/UiPanel.cs b/src/AcDream.App/UI/UiPanel.cs index 159ea2e4..71bf0c8b 100644 --- a/src/AcDream.App/UI/UiPanel.cs +++ b/src/AcDream.App/UI/UiPanel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -14,7 +14,7 @@ namespace AcDream.App.UI; /// AcFont/UiSpriteBatch consumes those directly, we draw a /// simple translucent rectangle so panels are visible during development. /// -internal class UiPanel : UiElement +public class UiPanel : UiElement { /// Background fill color. Set to skip. public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f); @@ -32,14 +32,14 @@ internal class UiPanel : UiElement /// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height). /// Required when is non-zero. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } protected override void OnDraw(UiRenderContext ctx) { if (BackgroundSprite != 0 && SpriteResolve is { } sr) { var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } else if (BackgroundColor.W > 0f) @@ -64,7 +64,7 @@ internal class UiPanel : UiElement /// FUN_0040b8f0 then drawn by the widget's draw method through /// FUN_00698330. /// -internal class UiLabel : UiElement +public class UiLabel : UiElement { public string Text { get; set; } = string.Empty; public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f); @@ -81,10 +81,10 @@ internal class UiLabel : UiElement /// a StateDesc per UIStateId (normal / hot / pressed / /// disabled) from the panel layout. /// Note: the dat-widget button (Type 1 / UIElement_Button) is -/// in UiButton.cs — that is the production widget used by D.2b panels. +/// in UiButton.cs — that is the production widget used by D.2b panels. /// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites). /// -internal class UiSimpleButton : UiPanel +public class UiSimpleButton : UiPanel { public string Text { get; set; } = string.Empty; public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f); @@ -120,7 +120,7 @@ internal class UiSimpleButton : UiPanel /// /// A that fires an callback when the user -/// left-clicks it. Used for the attribute-list rows in the Character window — each row +/// left-clicks it. Used for the attribute-list rows in the Character window — each row /// is a transparent container that needs to respond to pointer hits while its children /// (icon, name, value) are ClickThrough decorations. /// @@ -136,7 +136,7 @@ internal class UiSimpleButton : UiPanel /// with NO left/right end-caps. Bar height is pixels /// (default 3px). /// -internal class UiClickablePanel : UiPanel +public class UiClickablePanel : UiPanel { /// Called when the user releases the left mouse button over this panel. public Action? OnClick { get; set; } @@ -144,7 +144,7 @@ internal class UiClickablePanel : UiPanel /// When true and is non-zero, draws /// the sprite as a thin horizontal bar at the top AND bottom edges of the panel, /// NOT as a full-height stretched fill. Matches retail's selected-row highlight - /// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill). + /// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill). /// Default false (preserves legacy full-stretch behavior). public bool UseSelectionBars { get; set; } @@ -154,7 +154,7 @@ internal class UiClickablePanel : UiPanel public UiClickablePanel() { - // Rows must receive pointer events — override the UiPanel default (ClickThrough=false, + // Rows must receive pointer events — override the UiPanel default (ClickThrough=false, // which is the UiElement base default). Explicit for clarity. ClickThrough = false; } @@ -180,18 +180,18 @@ internal class UiClickablePanel : UiPanel if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr) { // Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row. - // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at + // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at // native height (SelectionBarHeight), stretched to full panel width (UV tile // horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat). var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw > 0 && th > 0) + if (tex != 0 && tw > 0 && th > 0) { float barH = SelectionBarHeight; float uTile = tw > 0 ? Width / tw : 1f; - // Top bar: shows the top barH px of the sprite (v = 0 → barH/th). + // Top bar: shows the top barH px of the sprite (v = 0 → barH/th). float vBot = th > 0 ? barH / th : 1f; ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One); - // Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1). + // Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1). float vTop2 = th > 0 ? 1f - barH / th : 0f; ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One); } diff --git a/src/AcDream.App/UI/UiRadar.cs b/src/AcDream.App/UI/UiRadar.cs index 8d6d72bc..77d718fa 100644 --- a/src/AcDream.App/UI/UiRadar.cs +++ b/src/AcDream.App/UI/UiRadar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Ui; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// are local pixels in the 120x120 radar disc. World-to-player /// projection stays in the Core retail port; this record is the backend-facing draw seam. /// -internal readonly record struct UiRadarBlip( +public readonly record struct UiRadarBlip( uint ObjectId, string Name, float PixelX, @@ -25,7 +25,7 @@ internal readonly record struct UiRadarBlip( /// CoordinatesOnRadar is disabled). mirrors /// ClientUISystem::m_bRadarBlank: static chrome and the player marker remain visible. /// -internal sealed record UiRadarSnapshot( +public sealed record UiRadarSnapshot( float PlayerHeadingDegrees, IReadOnlyList Blips, string? CoordinatesText, @@ -50,7 +50,7 @@ internal sealed record UiRadarSnapshot( /// gmRadarUI::UseTime 0x004D98A0 (25 ms refresh cadence). /// /// -internal sealed class UiRadar : UiElement +public sealed class UiRadar : UiElement { public const uint RetailClassId = 0x10000010u; public const float RetailRefreshSeconds = RetailRadar.UpdateIntervalSeconds; diff --git a/src/AcDream.App/UI/UiRenderContext.cs b/src/AcDream.App/UI/UiRenderContext.cs index 1ba6eb63..9c191d40 100644 --- a/src/AcDream.App/UI/UiRenderContext.cs +++ b/src/AcDream.App/UI/UiRenderContext.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; @@ -33,19 +33,19 @@ internal readonly record struct UiClipRect(float Left, float Top, float Right, f /// when iterating the UI tree. Our version is explicit so it plugs /// cleanly into Silk.NET. /// -internal sealed class UiRenderContext +public sealed class UiRenderContext { public TextRenderer TextRenderer { get; } public BitmapFont? DefaultFont { get; set; } public Vector2 ScreenSize { get; } - // Transform stack — simple 2D translate (no rotation/scale for UI). + // Transform stack — simple 2D translate (no rotation/scale for UI). private readonly System.Collections.Generic.List _stack = new(); private Vector2 _current; private readonly System.Collections.Generic.List _clipStack = new(); private UiClipRect? _clip; - // Alpha (opacity) stack — a window pushes its Opacity so its background/sprite + // Alpha (opacity) stack — a window pushes its Opacity so its background/sprite // draws fade (retail's translucent-chat effect). Text draws bypass this (they go // straight to TextRenderer), so text stays sharp over a translucent background. private readonly System.Collections.Generic.List _alphaStack = new(); @@ -57,7 +57,7 @@ internal sealed class UiRenderContext /// Multiply into the running opacity. Pair with . public void PushAlpha(float a) { _alphaStack.Add(_alpha); _alpha *= a; } - /// Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays + /// Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays /// that must stay opaque even inside a translucent window. Pair with . public void PushAlphaAbsolute(float a) { _alphaStack.Add(_alpha); _alpha = a; } @@ -117,7 +117,7 @@ internal sealed class UiRenderContext public void BeginOverlayLayer() => TextRenderer.OverlayMode = true; public void EndOverlayLayer() => TextRenderer.OverlayMode = false; - // ── Pass-through draw helpers (add current translate) ────────────── + // ── Pass-through draw helpers (add current translate) ────────────── public void DrawRect(float x, float y, float w, float h, Vector4 color) { @@ -129,7 +129,7 @@ internal sealed class UiRenderContext /// Solid-colour fill drawn in the SPRITE bucket (painter order with text), for /// a panel BACKGROUND that text draws on top of. composites after - /// all sprites and would cover the text — use this for backgrounds, that for foreground + /// all sprites and would cover the text — use this for backgrounds, that for foreground /// fills (carets, vital bars). public void DrawFill(float x, float y, float w, float h, Vector4 color) { @@ -149,7 +149,7 @@ internal sealed class UiRenderContext DrawRect(x + w - t, y + t, t, h - 2f * t, color); } - public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h, + public void DrawSprite(uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint) { x += _current.X; @@ -158,7 +158,7 @@ internal sealed class UiRenderContext } private void DrawSpriteAbsolute( - GpuTextureSlot texture, float x, float y, float w, float h, + uint texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 tint, bool applyAlpha) { if (_clip is { } clip @@ -213,7 +213,7 @@ internal sealed class UiRenderContext /// /// gates the black outline pass. Retail decides /// this PER text element: UIElement_Text::DrawSelf (acclient 0x00467aa0) - /// runs the outline pass only when m_bitField & 0x10 is set — i.e. the + /// runs the outline pass only when m_bitField & 0x10 is set — i.e. the /// element called SetOutline(true) (LayoutDesc property 0xd). The DEFAULT /// is OFF (one fill-only pass): the talk-focus menu items set no outline, so an /// always-on outline shows as a grey halo over the solid menu panel. Pass @@ -232,10 +232,10 @@ internal sealed class UiRenderContext // Snap the LINE baseline to a whole pixel ONCE. Retail's // SurfaceWindow::DrawCharacter (acclient 0x00442bd0) takes an int32 pen Y - // (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every + // (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every // glyph on a line shares one integer baseline. If we instead round EACH glyph's // Y independently and the caller passes a fractional line Y (e.g. a channel-menu - // item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round + // item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round // to different rows and the line looks crooked ("letters dip down"). The vitals // digits never showed it because their bar baseline lands on an integer; chat text // does. Snapping the baseline once, then adding the integer offset, keeps the whole @@ -251,7 +251,7 @@ internal sealed class UiRenderContext // Horizontal: snap each glyph's dest X to a whole pixel (the pen keeps its // true fractional advance). Vertical: integer baseline + integer per-glyph - // offset — never an independent per-glyph round (see baseY note above). + // offset — never an independent per-glyph round (see baseY note above). float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore); float gy = baseY + g.VerticalOffsetBefore; float gw = g.Width; @@ -259,10 +259,10 @@ internal sealed class UiRenderContext if (gw > 0f && gh > 0f) { - // Background (outline) atlas pass, tinted black — drawn behind. Gated by + // Background (outline) atlas pass, tinted black — drawn behind. Gated by // `outline` (retail's per-element m_bitField & 0x10); off by default so UI // text is crisp fill-only and free of the grey halo over solid panels. - if (outline && font.BackgroundTexture.IsAssigned) + if (outline && font.BackgroundTexture != 0) { var (bu0, bv0, bu1, bv1) = AtlasUv( g.OffsetX, g.OffsetY, g.Width, g.Height, diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs index 25db83c9..24c88b65 100644 --- a/src/AcDream.App/UI/UiRoot.cs +++ b/src/AcDream.App/UI/UiRoot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -22,7 +22,7 @@ public enum ResizeEdges { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 } /// or event fires so the game world /// (camera, player controller) still receives input. /// -internal sealed class UiRoot : UiElement +public sealed class UiRoot : UiElement { public UiRoot() { @@ -32,7 +32,7 @@ internal sealed class UiRoot : UiElement /// Single owner for named retained-window lifecycle and raise policy. public RetailWindowManager WindowManager { get; } - // ── Device-level state ─────────────────────────────────────────────── + // ── Device-level state ─────────────────────────────────────────────── public int MouseX { get; private set; } public int MouseY { get; private set; } public bool LeftButtonDown { get; private set; } @@ -42,7 +42,7 @@ internal sealed class UiRoot : UiElement /// Widget currently receiving keyboard events. public UiElement? KeyboardFocus { get; private set; } - /// The edit control activated by Tab/Enter when nothing is focused — retail's + /// The edit control activated by Tab/Enter when nothing is focused — retail's /// chat input "write mode" toggle. Set by the host once the chat window is built. public UiElement? DefaultTextInput { get; set; } @@ -59,7 +59,7 @@ internal sealed class UiRoot : UiElement /// True when the pointer is over a widget OR a widget holds mouse capture. /// The host ORs this into the InputDispatcher's WantCaptureMouse gate so game /// actions (movement, world-pick) are suppressed while the user interacts with - /// a retail window — mirrors ImGui's WantCaptureMouse. + /// a retail window — mirrors ImGui's WantCaptureMouse. /// public bool WantsMouse => Captured is not null || HitTestTopDown(MouseX, MouseY).element is not null; @@ -111,9 +111,9 @@ internal sealed class UiRoot : UiElement && !target.HandlesClick; } } - private (GpuTextureSlot tex, int w, int h)? _dragGhost; + private (uint tex, int w, int h)? _dragGhost; /// Snapshotted drag-ghost (tex,w,h), exposed for tests. See BeginDrag. - internal (GpuTextureSlot tex, int w, int h)? DragGhostForTest => _dragGhost; + internal (uint tex, int w, int h)? DragGhostForTest => _dragGhost; private UiElement? _lastDragHoverTarget; private int _pressX, _pressY; private bool _dragCandidate; @@ -272,7 +272,7 @@ internal sealed class UiRoot : UiElement return false; } - // ── Per-frame pumping ──────────────────────────────────────────────── + // ── Per-frame pumping ──────────────────────────────────────────────── public void Tick(double dt, long nowMs) { @@ -303,7 +303,7 @@ internal sealed class UiRoot : UiElement public void Draw(UiRenderContext ctx) { - // Render children (panels) sorted by z-order — modal last so it + // Render children (panels) sorted by z-order — modal last so it // sits on top. DrawSelfAndChildren(ctx); // Second pass: open popups/menus draw ON TOP of the whole tree (so e.g. the @@ -324,12 +324,12 @@ internal sealed class UiRoot : UiElement /// never intercepts hit-tests. private void DrawDragGhost(UiRenderContext ctx) { - if (_dragGhost is not { } g || !g.tex.IsAssigned) return; + if (_dragGhost is not { } g || g.tex == 0) return; ctx.DrawSprite(g.tex, MouseX - g.w / 2f, MouseY - g.h / 2f, g.w, g.h, 0f, 0f, 1f, 1f, new Vector4(1f, 1f, 1f, GhostAlpha)); } - // ── Input entry points (called from GameWindow's Silk.NET handlers) ── + // ── Input entry points (called from GameWindow's Silk.NET handlers) ── public void OnMouseMove(int x, int y) { @@ -426,7 +426,7 @@ internal sealed class UiRoot : UiElement if (target is null) { // Clicking the 3D world exits write mode (no submit) and returns control to - // the character — retail blurs the chat input on an outside click. + // the character — retail blurs the chat input on an outside click. if (btn == UiMouseButton.Left) SetKeyboardFocus(null); WorldMouseFallThrough?.Invoke(btn, x, y, flags); return; @@ -473,7 +473,7 @@ internal sealed class UiRoot : UiElement } else if (target.CapturesPointerDrag || target.HandlesClick) { - // The pressed widget owns its pointer interaction — either an interior drag (e.g. text + // The pressed widget owns its pointer interaction — either an interior drag (e.g. text // selection, CapturesPointerDrag) or a click it must receive (e.g. a UiButton, // HandlesClick). Either way do NOT move the ancestor window. The already-dispatched // MouseDown + SetCapture(target) let the target handle it; on release OnMouseUp emits @@ -513,7 +513,7 @@ internal sealed class UiRoot : UiElement // Deliver TARGET-LOCAL coords (consistent with MouseMove/MouseUp, which use // target.ScreenPosition). HitTestTopDown's lx/ly are relative to the TOP-LEVEL // child, so for a nested target (e.g. the chat view inset inside its window) - // they'd be offset by the child's position — which mis-anchored drag-select. + // they'd be offset by the child's position — which mis-anchored drag-select. var sp = target.ScreenPosition; var e = new UiEvent(target.EventId, target, rawType, Data0: (int)flags, Data1: (int)(x - sp.X), Data2: (int)(y - sp.Y)); @@ -615,7 +615,7 @@ internal sealed class UiRoot : UiElement return; } - // No capture — give the world a chance. + // No capture — give the world a chance. WorldMouseFallThrough?.Invoke(btn, x, y, flags); } @@ -687,7 +687,7 @@ internal sealed class UiRoot : UiElement BubbleEvent(KeyboardFocus, in e); } - // ── Focus + capture ───────────────────────────────────────────────── + // ── Focus + capture ───────────────────────────────────────────────── public void SetKeyboardFocus(UiElement? e) { @@ -726,12 +726,12 @@ internal sealed class UiRoot : UiElement PointerCaptureChanged?.Invoke(previous, null); } - // ── Window manager (named top-level windows: Show / Hide / Toggle) ─── + // ── Window manager (named top-level windows: Show / Hide / Toggle) ─── // Registry state lives in RetailWindowManager; methods below are compatibility forwarders. /// Register a top-level window under a name for Show/Hide/Toggle. - /// Does NOT add it to the tree — the caller mounts via AddChild and controls + /// Does NOT add it to the tree — the caller mounts via AddChild and controls /// initial Visible. Idempotent registration returns the existing typed handle; /// replacement performs full lifecycle teardown of the prior registration. public RetailWindowHandle RegisterWindow( @@ -788,7 +788,7 @@ internal sealed class UiRoot : UiElement WindowResized?.Invoke(handle.Name, window); } - // ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ──────── + // ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ──────── private void BeginDrag(UiElement source) { @@ -796,7 +796,7 @@ internal sealed class UiRoot : UiElement if (payload is null) { _dragCandidate = false; return; } DragSource = source; DragPayload = payload; - _dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell + _dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload); source.OnEvent(in e); // Retail UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects an @@ -843,7 +843,7 @@ internal sealed class UiRoot : UiElement var (t, lx, ly) = HitTestTopDown(x, y); if (t is not null) { - // Dropped on a real element — deliver DropReleased; the hit cell's handler places. + // Dropped on a real element — deliver DropReleased; the hit cell's handler places. // A non-item target's OnEvent ignores it, so an off-bar drop leaves the lift's removal. var e = new UiEvent(source!.EventId, t, UiEventType.DropReleased, Data1: (int)lx, Data2: (int)ly, Payload: payload); @@ -859,7 +859,7 @@ internal sealed class UiRoot : UiElement _lastDragHoverTarget = null; } - // ── Hover / tooltip ───────────────────────────────────────────────── + // ── Hover / tooltip ───────────────────────────────────────────────── private void UpdateHover(int x, int y) { @@ -892,7 +892,7 @@ internal sealed class UiRoot : UiElement } } - // ── Helpers ───────────────────────────────────────────────────────── + // ── Helpers ───────────────────────────────────────────────────────── public void FireEvent(int type, UiElement target, object? payload = null) { diff --git a/src/AcDream.App/UI/UiScrollable.cs b/src/AcDream.App/UI/UiScrollable.cs index 779264b0..cc12eaee 100644 --- a/src/AcDream.App/UI/UiScrollable.cs +++ b/src/AcDream.App/UI/UiScrollable.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AcDream.App.UI; @@ -11,7 +11,7 @@ namespace AcDream.App.UI; /// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0, /// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0. /// -internal sealed class UiScrollable +public sealed class UiScrollable { /// Total wrapped content height in px (m_iScrollableHeight). public int ContentHeight { get; set; } @@ -30,7 +30,7 @@ internal sealed class UiScrollable /// True when content exceeds the view (a scrollbar is warranted). public bool HasOverflow => ContentHeight > ViewHeight; - /// True when the offset is at (or past) the bottom — used for bottom-pin. + /// True when the offset is at (or past) the bottom — used for bottom-pin. public bool AtEnd => _scrollY >= MaxScroll; /// @@ -64,7 +64,7 @@ internal sealed class UiScrollable /// Position ratio = scroll/(content-view) in [0,1] (UpdateScrollbarPosition_). public float PositionRatio => MaxScroll <= 0 ? 0f : (float)_scrollY / MaxScroll; - /// Inverse of PositionRatio — used when the user drags the thumb. + /// Inverse of PositionRatio — used when the user drags the thumb. public void SetPositionRatio(float ratio) => SetScrollY((int)MathF.Round(Math.Clamp(ratio, 0f, 1f) * MaxScroll)); diff --git a/src/AcDream.App/UI/UiScrollablePanel.cs b/src/AcDream.App/UI/UiScrollablePanel.cs index 245b99fc..ea993c13 100644 --- a/src/AcDream.App/UI/UiScrollablePanel.cs +++ b/src/AcDream.App/UI/UiScrollablePanel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// pixel scroll model as chat text and item grids, and clips whole rows because /// the UI renderer does not have a scissor stack yet. /// -internal sealed class UiScrollablePanel : UiPanel +public sealed class UiScrollablePanel : UiPanel { private readonly Dictionary _baseTops = new(ReferenceEqualityComparer.Instance); diff --git a/src/AcDream.App/UI/UiScrollbar.cs b/src/AcDream.App/UI/UiScrollbar.cs index 320e31f9..25c04acc 100644 --- a/src/AcDream.App/UI/UiScrollbar.cs +++ b/src/AcDream.App/UI/UiScrollbar.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace AcDream.App.UI; @@ -6,20 +6,20 @@ namespace AcDream.App.UI; /// /// Generic scrollbar. Ports retail UIElement_Scrollbar /// (RegisterElementClass(0xb) @ acclient_2013_pseudo_c.txt:124137); -/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line. +/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line. /// /// /// Dat element ids (chat LayoutDesc 0x21000006): track 0x10000012 (X=474 Y=6 W=16 H=68), /// thumb 0x1000048C. The track is instanced from base layout 0x2100003E which contains /// the full scrollbar widget with distinct up/down button children: -/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69. -/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C. +/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69. +/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C. /// Track body sprite: 0x06004C5F (48px tall in the base template; stretched to H=68 in chat). /// Thumb is a 3-slice: top cap 0x06004C60, middle 0x06004C63, bottom cap 0x06004C66. /// The widget reproduces referenced button children procedurally and uses their /// authored extent for drawing and hit comparison (16px in this base layout). /// -internal sealed class UiScrollbar : UiElement +public sealed class UiScrollbar : UiElement { public override bool ReceivesHoverMouseMove => true; @@ -64,8 +64,8 @@ internal sealed class UiScrollbar : UiElement public void SetScalarPosition(float position) => ScalarPosition = Math.Clamp(position, 0f, 1f); - /// RenderSurface id → (GL tex, w, h). 0 id = skip. - public Func? SpriteResolve { get; set; } + /// RenderSurface id → (GL tex, w, h). 0 id = skip. + public Func? SpriteResolve { get; set; } /// Track background sprite id (0x06004C5F from layout 0x2100003E element 0x10000455). public uint TrackSprite { get; set; } @@ -212,8 +212,8 @@ internal sealed class UiScrollbar : UiElement if (Model is not { } m) return; - // Track background — TILED vertically (retail DrawMode=Normal). The native track - // sprite (~16×32) repeats to fill the element height instead of stretch-distorting. + // Track background — TILED vertically (retail DrawMode=Normal). The native track + // sprite (~16×32) repeats to fill the element height instead of stretch-distorting. DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height); float decrementExtent = AxisExtent(DecrementButtonExtent, Height); @@ -224,7 +224,7 @@ internal sealed class UiScrollbar : UiElement DrawSprite(ctx, resolve, ActiveEndSprite, 0f, Height - incrementExtent, Width, incrementExtent); - // Thumb — only when content overflows the view. Retail 3-slice: top cap + + // Thumb — only when content overflows the view. Retail 3-slice: top cap + // tiled middle + bottom cap (base layout 0x2100003E thumb sub-elements // 0x10000364/65/66). Falls back to a single tiled middle if the caps are unset // or the thumb is too short to hold both caps. @@ -248,7 +248,7 @@ internal sealed class UiScrollbar : UiElement private void DrawHorizontalModel( UiRenderContext ctx, - Func resolve, + Func resolve, UiScrollable model) { float decrementExtent = AxisExtent(DecrementButtonExtent, Width); @@ -275,23 +275,23 @@ internal sealed class UiScrollbar : UiElement } /// Draw a sprite stretched 1:1 to the dest rect. - private void DrawSprite(UiRenderContext ctx, Func resolve, + private void DrawSprite(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, _, _) = resolve(id); - if (!tex.IsAssigned) return; + if (tex == 0) return; ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, 1f, 1f, Vector4.One); } /// Draw a sprite TILED to fill the dest rect (UV-repeat at native size on - /// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1. - private void DrawTiled(UiRenderContext ctx, Func resolve, + /// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1. + private void DrawTiled(UiRenderContext ctx, Func resolve, uint id, float x, float y, float w, float h) { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One); } @@ -301,7 +301,7 @@ internal sealed class UiScrollbar : UiElement /// private void DrawTiledClipped( UiRenderContext ctx, - Func resolve, + Func resolve, uint id, float rangeLeft, float x, @@ -310,7 +310,7 @@ internal sealed class UiScrollbar : UiElement { if (id == 0 || w <= 0f || h <= 0f) return; var (tex, tw, th) = resolve(id); - if (!tex.IsAssigned || tw == 0 || th == 0) return; + if (tex == 0 || tw == 0 || th == 0) return; float u0 = (x - rangeLeft) / tw; float u1 = u0 + w / tw; ctx.DrawSprite(tex, x, 0f, w, h, u0, 0f, u1, h / th, Vector4.One); @@ -390,13 +390,13 @@ internal sealed class UiScrollbar : UiElement if (ly >= ty && ly <= ty + th) { - // Clicked inside the thumb — begin drag with offset from thumb top. + // Clicked inside the thumb — begin drag with offset from thumb top. _draggingThumb = true; _dragOffsetY = ly - ty; } else { - // Clicked above or below thumb — page scroll (HandleButtonClick page case). + // Clicked above or below thumb — page scroll (HandleButtonClick page case). m.ScrollByPage(ly < ty ? -1 : 1); } return true; @@ -520,7 +520,7 @@ internal sealed class UiScrollbar : UiElement return false; } - private float ScalarThumbWidth(Func? resolve) + private float ScalarThumbWidth(Func? resolve) { if (resolve is not null && ThumbSprite != 0) { diff --git a/src/AcDream.App/UI/UiShortcutDigitGraphics.cs b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs index 5cf3102f..0e8aafbf 100644 --- a/src/AcDream.App/UI/UiShortcutDigitGraphics.cs +++ b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// /// Shared DAT digit overlays used by UIItem shortcut cells. Both gmToolbarUI and @@ -6,7 +6,7 @@ /// share these number arrays. The underlying ItemSlot_Empty surface remains a /// per-ItemList asset selected by that list's cell prototype. /// -internal sealed record UiShortcutDigitGraphics( +public sealed record UiShortcutDigitGraphics( uint[]? RegularDigits, uint[]? GhostedDigits, uint[]? EmptyDigits); diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index 54ba161a..73d57334 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Text; @@ -20,7 +20,7 @@ namespace AcDream.App.UI; /// Display-only text remains click-through and cannot steal focus or window drag. /// /// -internal sealed class UiText : UiElement, IUiDatStateful +public sealed class UiText : UiElement, IUiDatStateful { /// Optional base-element click notice used by authored text tabs. public Action? OnClick { get; set; } @@ -30,11 +30,11 @@ internal sealed class UiText : UiElement, IUiDatStateful public uint ElementId { get; set; } /// One display line: pre-formatted text + its colour. - internal readonly record struct Line(string Text, Vector4 Color); + public readonly record struct Line(string Text, Vector4 Color); /// A caret position: a line index into the cached line list plus a /// character index (0..line.Text.Length, i.e. a caret slot between glyphs). - internal readonly record struct Pos(int Line, int Col); + public readonly record struct Pos(int Line, int Col); /// Provider of the lines to show, oldest-first. Polled each frame. public Func> LinesProvider { get; set; } = static () => Array.Empty(); @@ -58,7 +58,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// otherwise white (). /// /// Controllers that supply a per-line color via - /// (e.g. new UiText.Line(text, explicitColor)) are unaffected — they always + /// (e.g. new UiText.Line(text, explicitColor)) are unaffected — they always /// win over this default. This property is only a convenience starting point for /// controllers that want to read the dat color rather than hard-code it. /// @@ -84,7 +84,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height). /// Required when is non-zero. - public Func? SpriteResolve { get; set; } + public Func? SpriteResolve { get; set; } /// Highlight colour painted behind a selected character span. public Vector4 SelectionColor { get; set; } = new(0.25f, 0.45f, 0.85f, 0.5f); @@ -138,7 +138,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Static right-aligned single-line mode: draws the FIRST line right-justified /// within the element rect, vertically centered, with NO scroll/selection machinery. /// Used for value labels in attribute/skill rows where the number must hug the right edge. - /// Mutually exclusive with — if both are true, Centered takes + /// Mutually exclusive with — if both are true, Centered takes /// precedence. Pair with ClickThrough = true for non-interactive labels. public bool RightAligned { get; set; } @@ -146,20 +146,20 @@ internal sealed class UiText : UiElement, IUiDatStateful /// Vertical position of the text within the element rect in single-line mode /// ( or ). /// - /// Center (default) — vertically centered, matching the original + /// Center (default) — vertically centered, matching the original /// behavior of the centered/right-aligned paths. - /// Top — text is placed at y = Padding (top of the content + /// Top — text is placed at y = Padding (top of the content /// area), so the text sits at the top of the element rather than centering in it. /// Used for footer title elements whose dat box is the full footer height (55 px) but /// the text should render near the top. - /// Bottom — text is placed at y = Height - lineHeight - Padding. + /// Bottom — text is placed at y = Height - lineHeight - Padding. /// /// Only meaningful when or is true. /// Has no effect on the scrollable multi-line path. /// public VJustify VerticalJustify { get; set; } = VJustify.Center; - /// The scroll model — also read by the linked UiScrollbar. + /// The scroll model — also read by the linked UiScrollbar. public UiScrollable Scroll { get; } = new(); /// @@ -179,7 +179,7 @@ internal sealed class UiText : UiElement, IUiDatStateful private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch) - // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── + // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── private IReadOnlyList _lastLines = Array.Empty(); private BitmapFont? _lastFont; private UiDatFont? _lastDatFont; @@ -193,7 +193,7 @@ internal sealed class UiText : UiElement, IUiDatStateful private bool _drawTextAfterChildren; private bool _honorDatVerticalJustification; - // ── Selection state ────────────────────────────────────────────────── + // ── Selection state ────────────────────────────────────────────────── private Pos? _selAnchor; // where the drag started private Pos? _selCaret; // where the drag currently is private bool _selecting; @@ -325,7 +325,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// /// Clamp a scroll offset to [0, max] where max = content-height - view-height - /// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests. + /// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests. /// public static float ClampScroll(float scroll, float contentHeight, float viewHeight) { @@ -340,14 +340,14 @@ internal sealed class UiText : UiElement, IUiDatStateful if (BackgroundSprite != 0 && SpriteResolve is { } sr) { var (tex, tw, th) = sr(BackgroundSprite); - if (tex.IsAssigned && tw != 0 && th != 0) + if (tex != 0 && tw != 0 && th != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } // Background must draw UNDER the transcript text. DrawStringDat emits into the // sprite bucket which flushes BEFORE rects, so a DrawRect background would wash // over the text. DrawFill routes the background through the sprite bucket too, - // submitted first → text on top. + // submitted first → text on top. ctx.DrawFill(0, 0, Width, Height, BackgroundColor); if (!_drawTextAfterChildren) @@ -520,7 +520,7 @@ internal sealed class UiText : UiElement, IUiDatStateful hx = lineX + bitmapFont!.MeasureWidth(text.Substring(0, c0)); hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0)); } - // Highlight sits BEHIND the line's text → sprite bucket, submitted + // Highlight sits BEHIND the line's text → sprite bucket, submitted // before this line's DrawStringDat. ctx.DrawFill(hx, y, hw, lh, SelectionColor); } @@ -572,7 +572,7 @@ internal sealed class UiText : UiElement, IUiDatStateful if (!Selectable && !WheelScrollEnabled) return false; // Silk wheel +Y = scroll up = reveal older = toward the TOP = decrease ScrollY. // ScrollByLines sign: +down/newer, -up/older. - // e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines. + // e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines. Scroll.ScrollByLines((int)(-e.Data0 * WheelLines)); return true; } @@ -616,7 +616,7 @@ internal sealed class UiText : UiElement, IUiDatStateful || Keyboard.IsKeyPressed(Silk.NET.Input.Key.ControlRight)); if (ctrl && key == Silk.NET.Input.Key.C) { - // Only touch the clipboard when there's a selection — an empty + // Only touch the clipboard when there's a selection — an empty // copy must NOT clobber what the user previously copied. if (Keyboard is not null) { @@ -636,7 +636,7 @@ internal sealed class UiText : UiElement, IUiDatStateful return false; } - // ── Selection helpers ──────────────────────────────────────────────── + // ── Selection helpers ──────────────────────────────────────────────── /// Select the entire cached transcript (Ctrl+A). private void SelectAll() @@ -670,7 +670,7 @@ internal sealed class UiText : UiElement, IUiDatStateful return SelectedText(_lastLines, start, end); } - // ── Pure, testable logic (no GL / no font texture) ─────────────────── + // ── Pure, testable logic (no GL / no font texture) ─────────────────── /// /// Compute the Y offset (local space) for a single line in the Centered/RightAligned @@ -731,7 +731,7 @@ internal sealed class UiText : UiElement, IUiDatStateful /// (inclusive of start.Col, exclusive of end.Col) from /// . Multi-line selections are joined with "\n": /// the first line from start.Col to its end, whole middle lines, and the last - /// line up to end.Col. Pure — unit-testable without GL. + /// line up to end.Col. Pure — unit-testable without GL. /// public static string SelectedText(IReadOnlyList lines, Pos start, Pos end) { @@ -870,8 +870,8 @@ internal sealed class UiText : UiElement, IUiDatStateful /// The caret column for a horizontal position (already /// adjusted for the left padding, so x=0 is the start of the text). Walks the /// string accumulating each glyph's advance and snaps the caret to whichever - /// side of the glyph midpoint falls on — natural - /// Windows-like caret placement. Pure — unit-testable with a synthetic advance. + /// side of the glyph midpoint falls on — natural + /// Windows-like caret placement. Pure — unit-testable with a synthetic advance. /// /// The line text. /// Per-character advance (pixels) lookup. @@ -888,6 +888,6 @@ internal sealed class UiText : UiElement, IUiDatStateful if (x < mid) return i; // caret sits before this glyph cursor += adv; } - return text.Length; // past the last glyph → end caret + return text.Length; // past the last glyph → end caret } } diff --git a/src/AcDream.App/UI/UiTextureElement.cs b/src/AcDream.App/UI/UiTextureElement.cs index 19ac4921..e061ed87 100644 --- a/src/AcDream.App/UI/UiTextureElement.cs +++ b/src/AcDream.App/UI/UiTextureElement.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using AcDream.App.Rendering; namespace AcDream.App.UI; @@ -8,16 +8,16 @@ namespace AcDream.App.UI; /// still owns the region's geometry and chrome; controllers supply only the live /// texture handle (for example a composed item icon). /// -internal sealed class UiTextureElement : UiElement +public sealed class UiTextureElement : UiElement { public UiTextureElement() => ClickThrough = true; - public GpuTextureSlot Texture { get; set; } = GpuTextureSlot.Unassigned; + public uint Texture { get; set; } public Vector4 Tint { get; set; } = Vector4.One; protected override void OnDraw(UiRenderContext ctx) { - if (Texture.IsAssigned) + if (Texture != 0u) ctx.DrawSprite(Texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Tint); } } diff --git a/src/AcDream.App/UI/UiViewport.cs b/src/AcDream.App/UI/UiViewport.cs index c7f77669..f5a50323 100644 --- a/src/AcDream.App/UI/UiViewport.cs +++ b/src/AcDream.App/UI/UiViewport.cs @@ -1,11 +1,11 @@ -using System.Numerics; +using System.Numerics; namespace AcDream.App.UI; /// Leaf widget for dat Type 0xD (UIElement_Viewport). Blits the texture produced by its /// IUiViewportRenderer (run in the pre-UI hook) as a single sprite at its own rect. The 3-D render /// does NOT happen here (OnDraw only has a 2-D context). -internal sealed class UiViewport : UiElement +public sealed class UiViewport : UiElement { public override bool ConsumesDatChildren => true; @@ -34,12 +34,12 @@ internal sealed class UiViewport : UiElement /// Renderer that produces the off-screen texture. Set by GameWindow wiring (later task). public IUiViewportRenderer? Renderer { get; set; } - /// Last texture-table slot produced by the pre-UI hook. Unassigned = nothing to blit. - public GpuTextureSlot TextureHandle { get; set; } = GpuTextureSlot.Unassigned; + /// Last GL color-texture handle produced by the pre-UI hook. 0 = nothing to blit. + public uint TextureHandle { get; set; } protected override void OnDraw(UiRenderContext ctx) { - if (!Visible || !TextureHandle.IsAssigned) return; + if (!Visible || TextureHandle == 0) return; // Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren). // V is FLIPPED (v0=1, v1=0): TextureHandle is an off-screen FBO color texture, whose origin is // bottom-left (GL), while the UI sprite convention is top-left. Without the flip the doll renders diff --git a/src/AcDream.App/UI/WindowNames.cs b/src/AcDream.App/UI/WindowNames.cs index 7bfcf748..9be64cc8 100644 --- a/src/AcDream.App/UI/WindowNames.cs +++ b/src/AcDream.App/UI/WindowNames.cs @@ -1,8 +1,8 @@ -namespace AcDream.App.UI; +namespace AcDream.App.UI; /// Canonical registry names for top-level retail-UI windows, so the /// mount, the window registry, and the toggle keybind all agree on one literal. -internal static class WindowNames +public static class WindowNames { public const string Vitals = "vitals"; public const string Toolbar = "toolbar"; diff --git a/src/AcDream.App/UI/WorldDropDispatch.cs b/src/AcDream.App/UI/WorldDropDispatch.cs index f97477b7..598c18f6 100644 --- a/src/AcDream.App/UI/WorldDropDispatch.cs +++ b/src/AcDream.App/UI/WorldDropDispatch.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.UI; @@ -6,6 +6,6 @@ namespace AcDream.App.UI; /// One successfully dispatched inventory-to-world request together with the /// quantity selected by the retained UI. /// -internal readonly record struct WorldDropDispatch( +public readonly record struct WorldDropDispatch( PendingInventoryRequest Request, uint Amount); diff --git a/src/AcDream.App/World/ExternalContainerLifecycleController.cs b/src/AcDream.App/World/ExternalContainerLifecycleController.cs index 3545fab9..fdb06675 100644 --- a/src/AcDream.App/World/ExternalContainerLifecycleController.cs +++ b/src/AcDream.App/World/ExternalContainerLifecycleController.cs @@ -1,4 +1,4 @@ -using AcDream.Core.Items; +using AcDream.Core.Items; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// Presentation controllers observe the same state but do not own session /// lifecycle messages. /// -internal sealed class ExternalContainerLifecycleController : IDisposable +public sealed class ExternalContainerLifecycleController : IDisposable { private readonly ExternalContainerState _state; private readonly ClientObjectTable _objects; diff --git a/src/AcDream.App/World/ILiveEntityRadarSource.cs b/src/AcDream.App/World/ILiveEntityRadarSource.cs index 8aa9d84b..bb43a239 100644 --- a/src/AcDream.App/World/ILiveEntityRadarSource.cs +++ b/src/AcDream.App/World/ILiveEntityRadarSource.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// expose current presentation state without publishing a second GUID identity /// map. /// -internal interface ILiveEntityRadarSource +public interface ILiveEntityRadarSource { bool TryGetMaterialized(uint serverGuid, out WorldEntity entity); bool TryGetVisible(uint serverGuid, out WorldEntity entity); diff --git a/src/AcDream.App/World/ILiveEntitySpatialQuery.cs b/src/AcDream.App/World/ILiveEntitySpatialQuery.cs index 6fd83a68..1b587267 100644 --- a/src/AcDream.App/World/ILiveEntitySpatialQuery.cs +++ b/src/AcDream.App/World/ILiveEntitySpatialQuery.cs @@ -1,4 +1,4 @@ -using AcDream.Core.World; +using AcDream.Core.World; namespace AcDream.App.World; @@ -7,7 +7,7 @@ namespace AcDream.App.World; /// Consumers resolve this owner late because GameWindow replaces its /// empty bootstrap world state after retained UI construction. /// -internal interface ILiveEntitySpatialQuery +public interface ILiveEntitySpatialQuery { void CopyLiveEntitiesNearLandblock( uint centerCellOrLandblockId, diff --git a/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs b/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs index c05a6886..b7ede09a 100644 --- a/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs +++ b/src/AcDream.App/World/LiveEntityPartArrayEnterWorldPort.cs @@ -1,4 +1,4 @@ -namespace AcDream.App.World; +namespace AcDream.App.World; /// /// Strongly typed retail PartArray world-entry boundary. @@ -9,7 +9,7 @@ /// This dedicated port prevents that mandatory lifecycle callback from being /// confused with neighboring presentation callbacks. /// -internal sealed class LiveEntityPartArrayEnterWorldPort +public sealed class LiveEntityPartArrayEnterWorldPort { private readonly Action _handleEnterWorld; diff --git a/src/AcDream.App/World/LiveEntityPresentationController.cs b/src/AcDream.App/World/LiveEntityPresentationController.cs index ef1eba75..79653261 100644 --- a/src/AcDream.App/World/LiveEntityPresentationController.cs +++ b/src/AcDream.App/World/LiveEntityPresentationController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Physics; +using AcDream.App.Physics; using AcDream.Core.Physics; using AcDream.Runtime.Entities; @@ -16,7 +16,7 @@ namespace AcDream.App.World; /// (0x00514C60). Typed script ids are retail /// PS_UnHide=0x75 and PS_Hidden=0x76 from acclient.h. /// -internal sealed class LiveEntityPresentationController : IDisposable +public sealed class LiveEntityPresentationController : IDisposable { public const uint UnHideScriptType = 0x75u; public const uint HiddenScriptType = 0x76u; diff --git a/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs b/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs index bd1fefeb..14d13aa4 100644 --- a/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs +++ b/src/AcDream.App/World/LiveEntityProjectionWithdrawalController.cs @@ -1,4 +1,4 @@ -using AcDream.App.Physics; +using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.Plugins; @@ -163,13 +163,13 @@ internal sealed class LiveEntityProjectionWithdrawalController } } -internal enum ExactProjectionWithdrawalDisposition +public enum ExactProjectionWithdrawalDisposition { Completed, Pending, Superseded, } -internal readonly record struct ExactProjectionWithdrawalOutcome( +public readonly record struct ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition Disposition, Exception? Failure); diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs index 07ab3b80..378164ef 100644 --- a/src/AcDream.App/World/LiveEntityRuntime.cs +++ b/src/AcDream.App/World/LiveEntityRuntime.cs @@ -1,4 +1,4 @@ -using AcDream.App.Streaming; +using AcDream.App.Streaming; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -14,16 +14,16 @@ namespace AcDream.App.World; /// Animation state owned by a live object. The concrete App animation runtime /// implements this seam so identity storage does not depend on a render backend. /// -internal interface ILiveEntityAnimationRuntime +public interface ILiveEntityAnimationRuntime { WorldEntity Entity { get; } uint CurrentMotion { get; } } /// Marker for the DAT-driven effect profile added by the effect phase. -internal interface ILiveEntityEffectProfile { } +public interface ILiveEntityEffectProfile { } -internal enum LiveEntityProjectionKind +public enum LiveEntityProjectionKind { World, Attached, @@ -34,7 +34,7 @@ internal enum LiveEntityProjectionKind /// Spatial bucketing is deliberately absent: registering or removing meshes, /// scripts, and effects is a logical-lifetime operation, not a landblock move. /// -internal interface ILiveEntityResourceLifecycle +public interface ILiveEntityResourceLifecycle { void Register(WorldEntity entity); void Unregister(WorldEntity entity); @@ -127,7 +127,7 @@ internal sealed class DeferredLiveEntityRuntimeComponentLifecycle : } /// Delegate adapter used by the App composition root. -internal sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle +public sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceLifecycle { private readonly Action _register; private readonly Action _unregister; @@ -150,7 +150,7 @@ internal sealed class DelegateLiveEntityResourceLifecycle : ILiveEntityResourceL /// temporary loss of its render bucket; this sidecar retains only graphical /// components, visibility, hydration, and teardown progress. /// -internal sealed class LiveEntityRecord +public sealed class LiveEntityRecord { private readonly RuntimeEntityDirectory _directory; @@ -339,7 +339,7 @@ internal sealed class LiveEntityRecord _directory.RefreshSnapshot(Canonical, Snapshot, refreshPosition); } -internal readonly record struct LiveEntityRegistrationResult( +public readonly record struct LiveEntityRegistrationResult( InboundCreateResult Inbound, RuntimeEntityRecord? Canonical, LiveEntityRecord? Projection, @@ -366,7 +366,7 @@ internal readonly record struct LiveEntityRegistrationResult( /// CPhysicsObj::exit_world (0x00514E60). /// /// -internal sealed class LiveEntityRuntime : ILiveEntityRadarSource +public sealed class LiveEntityRuntime : ILiveEntityRadarSource { public const uint FirstLiveEntityId = RuntimeEntityDirectory.FirstLocalEntityId; public const uint LastLiveEntityId = RuntimeEntityDirectory.LastLocalEntityId; diff --git a/src/AcDream.App/World/LiveEntityRuntimeViews.cs b/src/AcDream.App/World/LiveEntityRuntimeViews.cs index c6c824cc..79b9bd70 100644 --- a/src/AcDream.App/World/LiveEntityRuntimeViews.cs +++ b/src/AcDream.App/World/LiveEntityRuntimeViews.cs @@ -1,11 +1,11 @@ -namespace AcDream.App.World; +namespace AcDream.App.World; /// /// Strongly typed view over animation components owned by a /// . It owns only reusable traversal scratch; /// canonical component identity remains in the runtime. /// -internal sealed class LiveEntityAnimationRuntimeView +public sealed class LiveEntityAnimationRuntimeView : IEnumerable> where TAnimation : class, ILiveEntityAnimationRuntime { @@ -90,7 +90,7 @@ internal sealed class LiveEntityAnimationRuntimeView /// invalidating this traversal. Each entry is identity-checked immediately /// before it is yielded, so GUID reuse cannot advance the displaced owner. /// - internal struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private readonly LiveEntityRuntime? _runtime; private readonly List> _snapshot; diff --git a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs index 63727c77..7b081a55 100644 --- a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs @@ -159,7 +159,6 @@ public sealed class HostInputCameraCompositionTests { public GpuFrameFlightController? GpuFrames { get; private set; } public IGpuDevice? GpuDevice { get; private set; } - public GpuDeviceFrameLifetime? GpuFrameLifetime { get; private set; } public SilkKeyboardSource? Keyboard { get; private set; } public SilkMouseSource? Mouse { get; private set; } public IMouseLookCursor? Cursor { get; private set; } @@ -173,9 +172,6 @@ public sealed class HostInputCameraCompositionTests public void PublishGpuDevice(IGpuDevice value) => GpuDevice = PublishOnce(GpuDevice, value); - public void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value) => - GpuFrameLifetime = PublishOnce(GpuFrameLifetime, value); - public void PublishKeyboardSource(SilkKeyboardSource value) => Keyboard = PublishOnce(Keyboard, value); diff --git a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs index cd65d48a..9ea7893e 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs @@ -189,10 +189,9 @@ public sealed class InteractionRetainedUiCompositionTests Dependencies = new InteractionRetainedUiDependencies( Options: options, Gl: null!, - GpuDevice: null!, - CurrentGpuFrame: null!, Window: null!, Input: null!, + ShadersDirectory: "shaders", Dats: null!, DatLock: new object(), TextureCache: null!, diff --git a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs index a4bfedc1..d9d3db6c 100644 --- a/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SettingsDevToolsCompositionTests.cs @@ -218,7 +218,6 @@ public sealed class SettingsDevToolsCompositionTests null!, null!, null!, - null!, null, null, null, diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index a7b522ee..4a1aab29 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -2,10 +2,8 @@ using System.Collections.Concurrent; using System.Runtime.CompilerServices; using AcDream.App.Composition; using AcDream.App.Rendering; -using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Residency; -using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Physics; @@ -161,7 +159,6 @@ public sealed class WorldRenderCompositionTests Factory = new Factory(hasFont); Publication = new Publication(publicationFailure); Lifetime = new RenderLifetime(Factory.Atlas); - GpuDevice = new RecordingGpuDevice(); var content = (ContentEffectsAudioResult) RuntimeHelpers.GetUninitializedObject(typeof(ContentEffectsAudioResult)); Content = content; @@ -170,7 +167,6 @@ public sealed class WorldRenderCompositionTests public Factory Factory { get; } public Publication Publication { get; } public RenderLifetime Lifetime { get; } - public RecordingGpuDevice GpuDevice { get; } public List Points { get; } = []; public ContentEffectsAudioResult Content { get; } @@ -180,7 +176,6 @@ public sealed class WorldRenderCompositionTests new WorldEnvironmentController(), Lifetime, ImmediateGpuResourceRetirementQueue.Instance, - GpuDevice, _budgets, 0xA9B4FFFFu, Path.Combine(Path.GetTempPath(), "acdream-tests"), @@ -253,15 +248,15 @@ public sealed class WorldRenderCompositionTests public SceneLightingUboBinding CreateSceneLighting(GL gl) => Resource("scene lighting"); - public DebugLineRenderer CreateDebugLines(IGpuDevice device) => + public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) => Resource("debug lines"); public byte[]? TryLoadDebugFont() => hasFont ? [1] : null; - public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) => + public BitmapFont CreateDebugFont(GL gl, byte[] bytes) => Resource("debug font"); - public TextRenderer CreateTextRenderer(IGpuDevice device) => + public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) => Resource("text renderer"); public TerrainModernRenderer CreateTerrain( @@ -300,7 +295,6 @@ public sealed class WorldRenderCompositionTests public TextureCache CreateTextureCache( GL gl, - IGpuDevice device, IDatReaderWriter dats, BindlessSupport bindless, IGpuResourceRetirementQueue retirement, diff --git a/tests/AcDream.App.Tests/GlobalUsings.cs b/tests/AcDream.App.Tests/GlobalUsings.cs index 9b259dd4..5213b6cc 100644 --- a/tests/AcDream.App.Tests/GlobalUsings.cs +++ b/tests/AcDream.App.Tests/GlobalUsings.cs @@ -2,4 +2,3 @@ global using AcDream.Runtime.Gameplay; global using AcDream.Runtime.Physics; global using ILocalPlayerMotionSource = AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource; -global using AcDream.App.Rendering.Gpu; diff --git a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs index 6b0cfdae..2edc666f 100644 --- a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs @@ -104,13 +104,8 @@ public sealed class GlTextureOwnershipTests root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs")); string terrain = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs")); - // Campaign V slice V4a ported TextRenderer/BitmapFont off raw GL onto - // IGpuDevice: neither file creates a GL texture name directly anymore. - // The checked-commit-boundary invariant this test enforces now lives in - // the RHI's GlGpuTexture, which every ported (and legacy) texture path - // shares. - string gpuTexture = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "Gpu", "Gl", "GlGpuTexture.cs")); + string text = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "TextRenderer.cs")); string bindless = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "Wb", "BindlessSupport.cs")); @@ -121,8 +116,8 @@ public sealed class GlTextureOwnershipTests Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal); Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal); Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.CreateName", gpuTexture, StringComparison.Ordinal); - Assert.Contains("GLHelpers.ThrowOnResourceError", gpuTexture, StringComparison.Ordinal); + Assert.Contains("GlResourceCommand.CreateTexture", text, StringComparison.Ordinal); + Assert.Contains("GlResourceCommand.Execute", text, StringComparison.Ordinal); Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal); Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs index 078ebc0d..d4f32c6d 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs @@ -14,8 +14,7 @@ public sealed class GlRenderStateCacheTests GpuCullMode.Back, GpuFrontFace.CounterClockwise, AlphaToCoverage: false, - ColorWrite: true, - Multisample: false); + ColorWrite: true); [Fact] public void FirstApplyReportsEveryDimensionChanged() diff --git a/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs b/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs index 6383c778..4ac71907 100644 --- a/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RenderFrameResourceControllerTests.cs @@ -49,12 +49,6 @@ public sealed class RenderFrameResourceControllerTests { string source = ResourceSource(); - // TextRenderer (_worldText/_uiText) moved onto the IGpuDevice/IGpuFrame - // RHI in Campaign V slice V4a: it no longer has a per-slot BeginFrame - // step, so those two needles are retired from this ordering check. - // The remaining GL-only renderers (WbDrawDispatcher, EnvCellRenderer, - // terrain, sky/lighting) are untouched by V4a and keep their slot-based - // BeginFrame(gpuSlot) order. AssertAppearsInOrder( source, "_textures?.BeginCompositeTextureFrame();", @@ -62,6 +56,8 @@ public sealed class RenderFrameResourceControllerTests "_dispatcher?.BeginFrame(gpuSlot);", "_environmentCells?.BeginFrame(gpuSlot);", "_portalDepth?.BeginFrame(gpuSlot);", + "_worldText?.BeginFrame(gpuSlot);", + "_uiText?.BeginFrame(gpuSlot);", "_clip?.BeginFrame(gpuSlot);", "_terrain?.BeginFrame(gpuSlot);", "_lighting?.BeginFrame(gpuSlot);"); diff --git a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs index d7199d01..0efce3e4 100644 --- a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs @@ -130,13 +130,40 @@ public sealed class ResourceCleanupGroupTests StringComparison.Ordinal); } - // TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork was retired - // by Campaign V slice V4a: TextRenderer no longer owns a Shader, per-flight - // FrameBufferSet array, white-texture fallback, or tracked VAO/VBO pair — - // it now borrows a shared IGpuDevice and owns exactly one IGpuPipeline - // created through it, so there is no multi-resource construction order left - // to assert. The equivalent GL-only invariant now lives in GlTextureOwnershipTests - // (GlGpuTexture's checked-commit texture creation). + [Fact] + public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork() + { + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "TextRenderer.cs")); + + AssertAppearsInOrder( + source, + "shader = new Shader(gl,", + "resources.Add(\"text shader\", shader.Dispose);", + "frameBuffers[i] = CreateFrameBufferSet(resources);", + "whiteTexture = GlResourceCommand.CreateTexture(", + "resources.Add(", + "\"white texture\"", + "GlResourceCommand.Execute(", + "initialize TextRenderer white texture", + "resources.RetryCleanup();", + "_resources = resources;"); + AssertAppearsInOrder( + source, + "private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)", + "TrackedGlResource.CreateVertexArray(", + "resources.Add(\"frame VAO\", vaoRelease.Run);", + "TrackedGlResource.CreateBuffer(", + "resources.Add(", + "\"frame VBO\""); + Assert.Contains("_resources.RetryCleanup();", source, StringComparison.Ordinal); + Assert.DoesNotContain("private FrameBufferSet CreateFrameBufferSet()", source, + StringComparison.Ordinal); + } [Fact] public void ReturnedNameIsRetainedWhenPostconditionAndFirstRollbackFail() diff --git a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs new file mode 100644 index 00000000..77154717 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs @@ -0,0 +1,217 @@ +using System.Reflection; +using AcDream.App.Rendering; +using Silk.NET.OpenGL; + +namespace AcDream.App.Tests.Rendering; + +public sealed class TextRendererFailureSafetyTests +{ + [Fact] + public void Flush_CompilesCompleteGlStateScopeAsFinallyAroundBothDrawLayers() + { + MethodInfo flush = typeof(TextRenderer).GetMethod(nameof(TextRenderer.Flush))!; + MethodBody body = flush.GetMethodBody()!; + string source = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Rendering", + "TextRenderer.cs")); + + Assert.Contains( + body.ExceptionHandlingClauses, + clause => clause.Flags == ExceptionHandlingClauseOptions.Finally); + AssertAppearsInOrder( + source, + "using var stateScope = new TextRenderGlStateScope(_glState);", + "_gl.Disable(EnableCap.Multisample);", + "DrawLayer(_spriteSegs,", + "DrawLayer(_overlaySpriteSegs,"); + } + + [Fact] + public void FailedDraw_RestoresEveryGlValueMutatedByTheTextPass() + { + var gl = new RecordingGlState + { + DepthWrite = false, + BlendSourceRgb = BlendingFactor.DstAlpha, + BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha, + BlendSourceAlpha = BlendingFactor.One, + BlendDestinationAlpha = BlendingFactor.Zero, + Program = 17, + VertexArray = 23, + ArrayBuffer = 31, + ActiveUnit = TextureUnit.Texture2, + }; + gl.SetCapability(EnableCap.DepthTest, enabled: true); + gl.SetCapability(EnableCap.Blend, enabled: false); + gl.SetCapability(EnableCap.CullFace, enabled: true); + gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: true); + gl.SetCapability(EnableCap.Multisample, enabled: false); + gl.TextureBindings[TextureUnit.Texture0] = 41; + gl.TextureBindings[TextureUnit.Texture2] = 43; + StateSnapshot expected = gl.Capture(); + + Action failedDraw = () => + { + using var stateScope = new TextRenderGlStateScope(gl); + gl.SetCapability(EnableCap.DepthTest, enabled: false); + gl.SetCapability(EnableCap.Blend, enabled: true); + gl.SetCapability(EnableCap.CullFace, enabled: false); + gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false); + gl.SetCapability(EnableCap.Multisample, enabled: true); + gl.DepthMask(true); + gl.BlendFuncSeparate( + BlendingFactor.SrcAlpha, + BlendingFactor.OneMinusSrcAlpha, + BlendingFactor.SrcAlpha, + BlendingFactor.OneMinusSrcAlpha); + gl.UseProgram(101); + gl.BindVertexArray(103); + gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107); + gl.ActiveTexture(TextureUnit.Texture0); + gl.BindTexture(TextureTarget.Texture2D, 109); + throw new InvalidOperationException("draw upload"); + }; + + Assert.Throws(failedDraw); + + Assert.Equal(expected, gl.Capture()); + } + + private readonly record struct StateSnapshot( + bool DepthTest, + bool Blend, + bool Cull, + bool AlphaToCoverage, + bool Multisample, + bool DepthWrite, + BlendingFactor BlendSourceRgb, + BlendingFactor BlendDestinationRgb, + BlendingFactor BlendSourceAlpha, + BlendingFactor BlendDestinationAlpha, + uint Program, + uint VertexArray, + uint ArrayBuffer, + TextureUnit ActiveUnit, + uint Texture0, + uint Texture2); + + private sealed class RecordingGlState : ITextRenderGlStateApi + { + private readonly Dictionary _capabilities = []; + + public bool DepthWrite { get; set; } + public BlendingFactor BlendSourceRgb { get; set; } + public BlendingFactor BlendDestinationRgb { get; set; } + public BlendingFactor BlendSourceAlpha { get; set; } + public BlendingFactor BlendDestinationAlpha { get; set; } + public uint Program { get; set; } + public uint VertexArray { get; set; } + public uint ArrayBuffer { get; set; } + public TextureUnit ActiveUnit { get; set; } + public Dictionary TextureBindings { get; } = []; + + public StateSnapshot Capture() => new( + IsEnabled(EnableCap.DepthTest), + IsEnabled(EnableCap.Blend), + IsEnabled(EnableCap.CullFace), + IsEnabled(EnableCap.SampleAlphaToCoverage), + IsEnabled(EnableCap.Multisample), + DepthWrite, + BlendSourceRgb, + BlendDestinationRgb, + BlendSourceAlpha, + BlendDestinationAlpha, + Program, + VertexArray, + ArrayBuffer, + ActiveUnit, + TextureBindings.GetValueOrDefault(TextureUnit.Texture0), + TextureBindings.GetValueOrDefault(TextureUnit.Texture2)); + + public bool IsEnabled(EnableCap capability) => + _capabilities.GetValueOrDefault(capability); + + public int GetInteger(GetPName parameter) => parameter switch + { + GetPName.BlendSrcRgb => (int)BlendSourceRgb, + GetPName.BlendDstRgb => (int)BlendDestinationRgb, + GetPName.BlendSrcAlpha => (int)BlendSourceAlpha, + GetPName.BlendDstAlpha => (int)BlendDestinationAlpha, + GetPName.CurrentProgram => (int)Program, + GetPName.VertexArrayBinding => (int)VertexArray, + GetPName.ArrayBufferBinding => (int)ArrayBuffer, + GetPName.ActiveTexture => (int)ActiveUnit, + GetPName.TextureBinding2D => + (int)TextureBindings.GetValueOrDefault(ActiveUnit), + _ => throw new ArgumentOutOfRangeException(nameof(parameter)), + }; + + public bool GetBoolean(GetPName parameter) => + parameter == GetPName.DepthWritemask + ? DepthWrite + : throw new ArgumentOutOfRangeException(nameof(parameter)); + + public void SetCapability(EnableCap capability, bool enabled) => + _capabilities[capability] = enabled; + + public void DepthMask(bool enabled) => DepthWrite = enabled; + + public void BlendFuncSeparate( + BlendingFactor sourceRgb, + BlendingFactor destinationRgb, + BlendingFactor sourceAlpha, + BlendingFactor destinationAlpha) + { + BlendSourceRgb = sourceRgb; + BlendDestinationRgb = destinationRgb; + BlendSourceAlpha = sourceAlpha; + BlendDestinationAlpha = destinationAlpha; + } + + public void UseProgram(uint program) => Program = program; + + public void BindVertexArray(uint vertexArray) => VertexArray = vertexArray; + + public void BindBuffer(BufferTargetARB target, uint buffer) + { + Assert.Equal(BufferTargetARB.ArrayBuffer, target); + ArrayBuffer = buffer; + } + + public void ActiveTexture(TextureUnit unit) => ActiveUnit = unit; + + public void BindTexture(TextureTarget target, uint texture) + { + Assert.Equal(TextureTarget.Texture2D, target); + TextureBindings[ActiveUnit] = texture; + } + } + + private static void AssertAppearsInOrder(string source, params string[] needles) + { + int cursor = -1; + foreach (string needle in needles) + { + int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); + Assert.True(next >= 0, $"Missing expected source fragment: {needle}"); + Assert.True(next > cursor, $"Out-of-order source fragment: {needle}"); + cursor = next; + } + } + + private static string FindRepoRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + return directory.FullName; + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not find AcDream.slnx."); + } +} diff --git a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs b/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs index b13533ad..2b635778 100644 --- a/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs +++ b/tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; @@ -32,7 +31,7 @@ public class DumpLayoutTests "docs", "research", "2026-06-25-retail-ui-layout-dump.json"); } - private static (GpuTextureSlot, int, int) NoTex(uint _) => (new GpuTextureSlot(1), 1, 1); + private static (uint, int, int) NoTex(uint _) => (1u, 1, 1); // ── Helpers ────────────────────────────────────────────────────────── diff --git a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs b/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs index 30ac80f9..5d478223 100644 --- a/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs +++ b/tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -13,7 +12,7 @@ namespace AcDream.App.Tests.Studio; /// public class LayoutSourceTests { - private static (GpuTextureSlot handle, int width, int height) NoTex(uint _) => (new GpuTextureSlot(1u), 1, 1); + private static (uint handle, int width, int height) NoTex(uint _) => (1u, 1, 1); /// Resolve the client dat directory, or null if unavailable (skip the test). private static string? ResolveDatDir() diff --git a/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs b/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs index 70bf0623..6aee28dc 100644 --- a/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -21,11 +20,11 @@ public sealed class CursorFeedbackControllerTests var accept = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), - DragAccept: DragAcceptState.Accept, + DragAccept: UiItemSlot.DragAcceptState.Accept, HoverResizeEdges: ResizeEdges.Right)); var reject = c.Resolve(new CursorFeedbackSnapshot( DragPayload: new object(), - DragAccept: DragAcceptState.Reject, + DragAccept: UiItemSlot.DragAcceptState.Reject, HoverResizeEdges: ResizeEdges.Right)); Assert.Equal(CursorFeedbackKind.DragAccept, accept.Kind); @@ -212,7 +211,7 @@ public sealed class CursorFeedbackControllerTests { ["Drag_rollover_accept"] = acceptCursor, }); - slot.SetItem(Target, iconTexture: new GpuTextureSlot(1u)); + slot.SetItem(Target, iconTexture: 1u); root.AddChild(slot); root.OnMouseMove(20, 20); var c = new CursorFeedbackController(interaction); diff --git a/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs index d3b8547b..52ba5332 100644 --- a/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs +++ b/tests/AcDream.App.Tests/UI/DatWidgetFactoryZOrderTests.cs @@ -1,11 +1,10 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; public class DatWidgetFactoryZOrderTests { - private static (GpuTextureSlot, int, int) NoChrome(uint id) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoChrome(uint id) => (0u, 0, 0); [Fact] public void ZOrder_HigherZLevel_DrawsBehind() diff --git a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs index 232248e5..d5452b3d 100644 --- a/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs +++ b/tests/AcDream.App.Tests/UI/DragDropSpineTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using Xunit; @@ -41,7 +40,7 @@ public class DragDropSpineTests [Fact] public void UiItemList_registerDragHandler_roundtrips() { - var list = new UiItemList(_ => (GpuTextureSlot.Unassigned, 0, 0)); + var list = new UiItemList(_ => (0u, 0, 0)); Assert.Null(list.DragHandler); var h = new SpyHandler(); list.RegisterDragHandler(h); @@ -57,7 +56,7 @@ public class DragDropSpineTests public void GetDragPayload_boundCell_snapshotsFields() { var cell = new UiItemSlot { SlotIndex = 4, SourceKind = ItemDragSource.ShortcutBar }; - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x5001u, 0x99u); var p = Assert.IsType(cell.GetDragPayload()); Assert.Equal(0x5001u, p.ObjId); Assert.Equal(ItemDragSource.ShortcutBar, p.SourceKind); @@ -73,10 +72,10 @@ public class DragDropSpineTests public void GetDragGhost_boundCell_returnsIconTuple() { var cell = new UiItemSlot { Width = 32, Height = 32 }; - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x5001u, 0x99u); var g = cell.GetDragGhost(); Assert.NotNull(g); - Assert.Equal(new GpuTextureSlot(0x99u), g!.Value.tex); + Assert.Equal(0x99u, g!.Value.tex); Assert.Equal(32, g.Value.w); Assert.Equal(32, g.Value.h); } @@ -85,16 +84,16 @@ public class DragDropSpineTests public void GetDragGhost_prefersDedicatedUnderlayFreeTexture() { var cell = new UiItemSlot { Width = 36, Height = 36 }; // retail bag cell is larger than its icon - cell.SetItem(0x5001u, new GpuTextureSlot(0x99u), dragIconTexture: new GpuTextureSlot(0x77u)); + cell.SetItem(0x5001u, 0x99u, dragIconTexture: 0x77u); - Assert.Equal((new GpuTextureSlot(0x77u), 32, 32), cell.GetDragGhost()); - Assert.Equal(new GpuTextureSlot(0x99u), cell.IconTexture); // source cell keeps the full m_pIcon + Assert.Equal((0x77u, 32, 32), cell.GetDragGhost()); + Assert.Equal(0x99u, cell.IconTexture); // source cell keeps the full m_pIcon } // ── cell drop-target: DragEnter overlay + DropReleased dispatch ────────── private static (UiItemList list, UiItemSlot cell, SpyHandler h) ListWithHandler() { - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)); // non-zero resolve so overlay draw is harmless + var list = new UiItemList(_ => (1u, 1, 1)); // non-zero resolve so overlay draw is harmless var h = new SpyHandler(); list.RegisterDragHandler(h); return (list, list.Cell, h); @@ -109,7 +108,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.Accept; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Accept, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Accept, cell.DragAcceptVisual); } [Fact] @@ -118,7 +117,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.Reject; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual); } [Fact] @@ -127,7 +126,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); h.Acceptance = ItemDragAcceptance.None; cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.None, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual); } [Fact] @@ -136,7 +135,7 @@ public class DragDropSpineTests var (_, cell, h) = ListWithHandler(); cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragOver, Payload: SomePayload())); - Assert.Equal(DragAcceptState.None, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.None, cell.DragAcceptVisual); } [Fact] @@ -180,16 +179,16 @@ public class DragDropSpineTests root.OnMouseDown(UiMouseButton.Left, 10, 10); root.OnMouseMove(20, 10); // BeginDrag → snapshot ghost cell.Clear(); // simulate the lift emptying the source - Assert.Equal((new GpuTextureSlot(0x99u), 32, 32), root.DragGhostForTest); + Assert.Equal((0x99u, 32, 32), root.DragGhostForTest); } [Fact] public void FinishDrag_overNothing_deliversNoDrop_butLiftStands() { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; list.Cell.Width = 32; list.Cell.Height = 32; - list.Cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + list.Cell.SetItem(0x5001u, 0x99u); var h = new SpyHandler(); list.RegisterDragHandler(h); root.AddChild(list); @@ -238,10 +237,10 @@ public class DragDropSpineTests private static (UiRoot root, UiItemList list, UiItemSlot cell) RootWithBoundSlot(uint itemId) { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, Width = 32, Height = 32 }; // Tests don't run OnDraw (which sizes the cell), so size the cell explicitly. list.Cell.Width = 32; list.Cell.Height = 32; - if (itemId != 0) list.Cell.SetItem(itemId, new GpuTextureSlot(0x99u)); + if (itemId != 0) list.Cell.SetItem(itemId, 0x99u); root.AddChild(list); return (root, list, list.Cell); } @@ -250,7 +249,7 @@ public class DragDropSpineTests RootWithCatalogSlot(uint entryId) { var root = new UiRoot { Width = 800, Height = 600 }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 0, Top = 0, @@ -263,7 +262,7 @@ public class DragDropSpineTests EntryId = entryId, Width = 32, Height = 32, - SpriteResolve = _ => (new GpuTextureSlot(1u), 1, 1), + SpriteResolve = _ => (1u, 1, 1), }; list.AddItem(cell); root.AddChild(list); @@ -428,15 +427,15 @@ public class DragDropSpineTests { var cell = new UiItemSlot(); // no parent list → FindList() null cell.OnEvent(new UiEvent(0u, cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, cell.DragAcceptVisual); } [Fact] public void DragEnter_listWithoutHandler_defaultsToReject() { - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)); // no RegisterDragHandler + var list = new UiItemList(_ => (1u, 1, 1)); // no RegisterDragHandler list.Cell.OnEvent(new UiEvent(0u, list.Cell, UiEventType.DragEnter, Payload: SomePayload())); - Assert.Equal(DragAcceptState.Reject, list.Cell.DragAcceptVisual); + Assert.Equal(UiItemSlot.DragAcceptState.Reject, list.Cell.DragAcceptVisual); } // ── item drag inside a Draggable window (the LIVE toolbar topology) ────── @@ -450,9 +449,9 @@ public class DragDropSpineTests { var root = new UiRoot { Width = 800, Height = 600 }; var frame = new UiPanel { Left = 10, Top = 300, Width = 200, Height = 60, Draggable = true }; - var list = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) { Left = 5, Top = 5, Width = 32, Height = 32 }; + var list = new UiItemList(_ => (1u, 1, 1)) { Left = 5, Top = 5, Width = 32, Height = 32 }; list.Cell.Width = 32; list.Cell.Height = 32; - if (itemId != 0) list.Cell.SetItem(itemId, new GpuTextureSlot(0x99u)); + if (itemId != 0) list.Cell.SetItem(itemId, 0x99u); frame.AddChild(list); root.AddChild(frame); return (root, frame, list); diff --git a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs index 21ca2c89..11a9740a 100644 --- a/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/ItemInteractionControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -252,7 +251,7 @@ public sealed class ItemInteractionControllerTests }); h.GroundObject = chest; var cell = new UiItemSlot(); - cell.SetItem(itemId, GpuTextureSlot.Unassigned); + cell.SetItem(itemId, 0u); var payload = new ItemDragPayload(itemId, ItemDragSource.Inventory, 0, cell); Assert.True(h.Controller.PlaceIn3D(payload, chest)); diff --git a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs index 1d34ad6b..e4a55791 100644 --- a/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.Spells; using AcDream.Content; @@ -15,7 +14,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class AppraisalUiControllerTests { private const uint ObjectId = 0x50000001u; - private static (GpuTextureSlot, int, int) NoTexture(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTexture(uint _) => (0u, 0, 0); [Fact] public void ItemResponse_UsesAuthoredItemSubviewTitleAndScrollbars() @@ -742,11 +741,11 @@ public sealed class AppraisalUiControllerTests () => { }, selection: selection, spellbook: spellbook, - resolveSpellIcon: id => new GpuTextureSlot(id + 1_000u), + resolveSpellIcon: id => id + 1_000u, resolveComponentIcon: did => { resolvedComponentIcons.Add(did); - return new GpuTextureSlot(did + 2_000u); + return did + 2_000u; }, spellComponents: _ => components, magicSkill: _ => 200u, @@ -802,18 +801,14 @@ public sealed class AppraisalUiControllerTests AppraisalUiController.SpellIconId)!; UiTextureElement icon = Assert.Single( iconHost.Children.OfType()); - Assert.Equal(new GpuTextureSlot(metadata.SpellId + 1_000u), icon.Texture); + Assert.Equal(metadata.SpellId + 1_000u, icon.Texture); Assert.Equal( new uint[] { 0x06000010u, 0x06000011u }, resolvedComponentIcons); UiElement formula = layout.FindElement( AppraisalUiController.SpellFormulaListId)!; Assert.Equal( - new[] - { - new GpuTextureSlot(0x06000010u + 2_000u), - new GpuTextureSlot(0x06000011u + 2_000u), - }, + new uint[] { 0x06000010u + 2_000u, 0x06000011u + 2_000u }, formula.Children .OfType() .Select(cell => cell.RuntimeImageTexture!.Value)); @@ -833,8 +828,8 @@ public sealed class AppraisalUiControllerTests SelectionState? selection = null, RetailAppraisalNameResolver? itemNames = null, Spellbook? spellbook = null, - Func? resolveSpellIcon = null, - Func? resolveComponentIcon = null, + Func? resolveSpellIcon = null, + Func? resolveComponentIcon = null, Func>? spellComponents = null, Func? magicSkill = null, SpellExamineComponentTemplateFactory? spellComponentTemplates = null) diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs index 36a6d64e..d2b334fb 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterLayoutImportProbe.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using System.IO; @@ -31,10 +30,10 @@ public sealed class CharacterLayoutImportProbe if (datDir is null) return; using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (new GpuTextureSlot(1u), 30, 26), null); + var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (1u, 30, 26), null); Assert.NotNull(layout); - CharacterStatController.Bind(layout!, SampleData.SampleCharacter, spriteResolve: _ => (new GpuTextureSlot(1u), 30, 26)); + CharacterStatController.Bind(layout!, SampleData.SampleCharacter, spriteResolve: _ => (1u, 30, 26)); var rows = new List(); CollectRows(layout!.Root, rows); @@ -62,7 +61,7 @@ public sealed class CharacterLayoutImportProbe if (datDir is null) return; using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (new GpuTextureSlot(1u), 30, 26), null); + var layout = LayoutImporter.Import(dats, CharacterLayout, _ => (1u, 30, 26), null); Assert.NotNull(layout); var close = layout!.FindElement(WindowChromeController.CharacterCloseButtonId); diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index f1f3ec80..5d2a39f0 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -282,7 +281,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); var rows = list.Children.OfType().ToList(); Assert.Equal(9, rows.Count); @@ -548,7 +547,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); // Minimal sprite resolver — returns a fake non-zero handle so UiPanel draws it. - static (GpuTextureSlot, int, int) FakeResolve(uint id) => (new GpuTextureSlot(1), 32, 8); + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: FakeResolve); @@ -567,7 +566,7 @@ public class CharacterStatControllerTests { var list = new UiPanel(); var layout = Fake((CharacterStatController.ListBoxId, list)); - static (GpuTextureSlot, int, int) FakeResolve(uint id) => (new GpuTextureSlot(1), 32, 8); + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: FakeResolve); @@ -792,7 +791,7 @@ public class CharacterStatControllerTests Assert.Equal(3, titles.Children.Count); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); Assert.Equal(rootChildCount, layout.Root.Children.Count); Assert.Equal(RetailUiStateIds.Open, attributes.ActiveRetailStateId); @@ -815,7 +814,7 @@ public class CharacterStatControllerTests var skills = Assert.IsType(layout.FindElement(CharacterStatController.TabSkillsId)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); skills.OnClick!(); Assert.Equal(RetailUiStateIds.Closed, attributes.ActiveRetailStateId); @@ -837,7 +836,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); @@ -892,7 +891,7 @@ public class CharacterStatControllerTests var layout = Fake((CharacterStatController.ListBoxId, list)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); Assert.Equal(12, SkillRows(list).Count); @@ -930,7 +929,7 @@ public class CharacterStatControllerTests }); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); Assert.Equal("Strength", FirstRowName(visibleList)); Assert.Equal("", FirstRowName(hiddenDuplicateList)); @@ -964,7 +963,7 @@ public class CharacterStatControllerTests (CharacterStatController.FooterLine2Value, l2Value)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); var rows = SkillRows(list); @@ -994,7 +993,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); completed(); }); ClickTab(layout, left: 92f); @@ -1028,7 +1027,7 @@ public class CharacterStatControllerTests (CharacterStatController.RaiseOneId, btn1)); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); SkillRows(list)[5].OnClick!(); @@ -1053,7 +1052,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); completed(); }); ClickTab(layout, left: 92f); @@ -1097,7 +1096,7 @@ public class CharacterStatControllerTests var requests = new List(); CharacterStatController.Bind(layout, () => sheet, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (request, completed) => { requests.Add(request); @@ -1150,7 +1149,7 @@ public class CharacterStatControllerTests Action? completeRaise = null; CharacterStatController.Bind(layout, () => sheet, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16), + spriteResolve: id => (id, 16, 16), onRaiseRequest: (_, completed) => completeRaise = completed); ClickTab(layout, left: 92f); @@ -1214,7 +1213,7 @@ public class CharacterStatControllerTests }); CharacterStatController.Bind(layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ClickTab(layout, left: 92f); @@ -1538,7 +1537,7 @@ public class CharacterStatControllerTests CharacterStatController.Bind( layout, SampleData.SampleCharacter, - spriteResolve: id => (new GpuTextureSlot(id), 16, 16)); + spriteResolve: id => (id, 16, 16)); ApplyLayoutPass(layout.Root); @@ -1636,7 +1635,7 @@ public class CharacterStatControllerTests Width = width, Height = height, }; - return new UiDatElement(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)) + return new UiDatElement(info, static _ => (0u, 0, 0)) { Top = top, Width = width, @@ -1650,7 +1649,7 @@ public class CharacterStatControllerTests var info = new ElementInfo { Id = id, Type = 1 }; info.StateMedia["Normal"] = (1u, 1); info.StateMedia["Ghosted"] = (2u, 1); - return new UiButton(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)); + return new UiButton(info, static _ => (0u, 0, 0)); } private static UiText MakeTab(uint id, float left) @@ -1677,7 +1676,7 @@ public class CharacterStatControllerTests Name = "Open", PassToChildren = true, }; - return Assert.IsType(DatWidgetFactory.Create(info, static _ => (GpuTextureSlot.Unassigned, 0, 0), null)); + return Assert.IsType(DatWidgetFactory.Create(info, static _ => (0u, 0, 0), null)); } private static ImportedLayout Fake(params (uint id, UiElement e)[] items) diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs index 76236c62..628247f0 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Chat; @@ -14,7 +13,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class ChatLayoutConformanceTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); private static ElementInfo? Find(ElementInfo n, uint id) { diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs index 9240f18a..31030d0d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -22,7 +21,7 @@ namespace AcDream.App.Tests.UI.Layout; public class ChatWindowControllerTests { // ── Null-resolve helper (no GL needed) ───────────────────────────────── - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); // ── Capture bus — records every Publish call ──────────────────────────── private sealed class CaptureBus : ICommandBus diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs index 27c05acc..d8c42076 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -19,7 +18,7 @@ public sealed class CombatLayoutConformanceTests ElementInfo info = FixtureLoader.LoadCombatInfos(); ImportedLayout layout = LayoutImporter.Build( info, - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: _ => "localized"); ApplyAnchors(layout.Root); diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs index 4e47bfd1..5334ea81 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Combat; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class CombatUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void CombatMode_ShowsPhysicalAndMagicPages_AndSelectsMediumByDefault() diff --git a/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs b/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs index 084e829b..926573b7 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CreatureAppraisalRowsTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -131,7 +130,7 @@ public sealed class CreatureAppraisalRowsTests { var templates = new CreatureAppraisalRowTemplateFactory( FixtureLoader.LoadExaminationRowTemplateInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), defaultFont: null); var row = new CreatureAppraisalRow( "Strength", diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs index 904d2c17..8b148736 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryFontResolveTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -21,7 +20,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class DatWidgetFactoryFontResolveTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: null fontResolve → DatFont == datFont (backward-compat) ───── diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index 48bac99d..f4c3b449 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public class DatWidgetFactoryTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: Type 7 → UiMeter ───────────────────────────────────────────── @@ -335,7 +334,7 @@ public class DatWidgetFactoryTests public void Create_buildsUiItemList_forItemListClassId() { var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 }; - var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null); Assert.IsType(w); } diff --git a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs index 12ee12d7..86c0122f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Spells; @@ -7,7 +6,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class EffectsUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); private static EffectRowTemplateFactory Templates() => new(FixtureLoader.LoadEffectRowTemplateInfos(), NoTex, defaultFont: null); @@ -48,7 +47,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.InfoTextId] = info, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; Assert.False(info.PreserveEndOnLayout); @@ -87,7 +86,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.ListId] = list, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 100u)); @@ -130,7 +129,7 @@ public sealed class EffectsUiControllerTests info.Width = 96f; using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 42u)); @@ -174,7 +173,7 @@ public sealed class EffectsUiControllerTests [EffectsUiController.ListId] = list, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive: true, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( @@ -226,7 +225,7 @@ public sealed class EffectsUiControllerTests int closes = 0; using EffectsUiController? controller = EffectsUiController.Bind( - layout, spellbook, positive, () => 0d, NoTex, id => new GpuTextureSlot(id), + layout, spellbook, positive, () => 0d, NoTex, id => id, Templates(), "SELECT A SPELL", () => closes++); Assert.NotNull(controller); diff --git a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs index e4c6a8fd..bbcf5f93 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ExternalContainerControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.World; @@ -32,7 +31,7 @@ public sealed class ExternalContainerControllerTests RetailWindowHandle handle = RetailWindowFrame.Mount( screen, root, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), options); var frame = Assert.IsType(handle.OuterFrame); @@ -76,7 +75,7 @@ public sealed class ExternalContainerControllerTests var root = new TestElement { Width = 800f, Height = 110f }; var close = new UiButton( new ElementInfo { Id = ExternalContainerController.CloseButtonId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)); + static _ => (0u, 0, 0)); var scrollbar = new UiScrollbar { Width = 784f, Height = 16f }; root.AddChild(Top); root.AddChild(Containers); @@ -95,7 +94,7 @@ public sealed class ExternalContainerControllerTests Window = RetailWindowFrame.Mount( Screen, root, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = WindowNames.ExternalContainer, @@ -130,8 +129,8 @@ public sealed class ExternalContainerControllerTests Selection, Interaction, Split, - static (_, _, _, _, _) => GpuTextureSlot.Unassigned, - static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + static (_, _, _, _, _) => 0u, + static (_, _, _, _, _) => 0u, Uses.Add, (item, container, placement) => Puts.Add((item, container, placement)), (item, container, placement, amount) => @@ -286,7 +285,7 @@ public sealed class ExternalContainerControllerTests h.Selection.Select(Item, SelectionChangeSource.Inventory); h.Split.Reset(5u, 2u); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); var payload = new ItemDragPayload(Item, ItemDragSource.Inventory, 0, source); UiItemSlot target = h.Contents.GetItem(0)!; @@ -305,7 +304,7 @@ public sealed class ExternalContainerControllerTests h.Objects.AddOrUpdate(new ClientObject { ObjectId = Item, Type = ItemType.Misc }); h.Objects.MoveItem(Item, Player, 0); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); Assert.True(h.Interaction.PlaceWorldItemInBackpack(0x70000A01u)); h.Controller.HandleDropRelease( @@ -334,7 +333,7 @@ public sealed class ExternalContainerControllerTests h.Selection.Select(Item, SelectionChangeSource.Inventory); h.Split.Reset(5u, 2u); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); Assert.True(h.Interaction.PlaceWorldItemInBackpack(0x70000A02u)); h.Controller.HandleDropRelease( @@ -356,7 +355,7 @@ public sealed class ExternalContainerControllerTests h.Objects.AddOrUpdate(new ClientObject { ObjectId = Item, Type = ItemType.Misc }); h.Objects.MoveItem(Item, Player, 0); var source = new UiItemSlot { SourceKind = ItemDragSource.Inventory }; - source.SetItem(Item, GpuTextureSlot.Unassigned); + source.SetItem(Item, 0u); h.Controller.HandleDropRelease( h.Contents, diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index d284fe46..c3897b13 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.IO; using System.Text.Json; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; /// no dats required. Fixtures were generated from the real portal.dat and /// serialized with . /// -internal static class FixtureLoader +public static class FixtureLoader { private static readonly JsonSerializerOptions _opts = new() { @@ -27,7 +26,7 @@ internal static class FixtureLoader public static ImportedLayout LoadVitals() { var root = LoadVitalsInfos(); - return LayoutImporter.Build(root, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + return LayoutImporter.Build(root, _ => (0u, 0, 0), null); } /// @@ -47,7 +46,7 @@ internal static class FixtureLoader /// conformance checks on tree structure and resolved types. /// public static ImportedLayout LoadChat() - => LayoutImporter.Build(LoadChatInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadChatInfos(), _ => (0u, 0, 0), null); /// /// Deserializes the committed chat_21000006.json fixture into a raw @@ -60,44 +59,44 @@ internal static class FixtureLoader /// Builds the committed retail radar LayoutDesc 0x21000074 fixture. public static ImportedLayout LoadRadar() - => LayoutImporter.Build(LoadRadarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadRadarInfos(), _ => (0u, 0, 0), null); /// Returns the resolved ElementInfo tree for retail radar LayoutDesc 0x21000074. public static AcDream.App.UI.Layout.ElementInfo LoadRadarInfos() => LoadInfos("radar_21000074.json"); public static ImportedLayout LoadToolbar() - => LayoutImporter.Build(LoadToolbarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadToolbarInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadToolbarInfos() => LoadInfos("toolbar_21000016.json"); public static ImportedLayout LoadInventory() - => LayoutImporter.Build(LoadInventoryInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadInventoryInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadInventoryInfos() => LoadInfos("inventory_21000023.json"); public static ImportedLayout LoadPaperdoll() - => LayoutImporter.Build(LoadPaperdollInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPaperdollInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPaperdollInfos() => LoadInfos("paperdoll_21000024.json"); public static ImportedLayout LoadCharacter() - => LayoutImporter.Build(LoadCharacterInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadCharacterInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCharacterInfos() => LoadInfos("character_2100002E.json"); public static ImportedLayout LoadCombat() - => LayoutImporter.Build(LoadCombatInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadCombatInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCombatInfos() => LoadInfos("combat_21000073.json"); public static ImportedLayout LoadSpellbook() - => LayoutImporter.Build(LoadSpellbookInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadSpellbookInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadSpellbookInfos() => LoadInfos("spellbook_21000034.json"); @@ -109,7 +108,7 @@ internal static class FixtureLoader => LoadInfos("component_row_21000033_10000467.json"); public static ImportedLayout LoadExamination() - => LayoutImporter.Build(LoadExaminationInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadExaminationInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadExaminationInfos() => LoadInfos("examine_2100006B_100005F2.json"); @@ -121,31 +120,31 @@ internal static class FixtureLoader => LoadInfos("examine_component_2100006B_1000032E.json"); public static ImportedLayout LoadPowerbar() - => LayoutImporter.Build(LoadPowerbarInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPowerbarInfos() => LoadInfos("powerbar_21000072.json"); public static ImportedLayout LoadConfirmationDialog() - => LayoutImporter.Build(LoadConfirmationDialogInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadConfirmationDialogInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadConfirmationDialogInfos() => LoadInfos("dialogs_2100003C.json"); public static ImportedLayout LoadFpsDisplay() - => LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadFpsDisplayInfos() => LoadInfos("smartbox_fps_2100000F.json"); public static ImportedLayout LoadPositiveEffects() - => LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadPositiveEffectsInfos() => LoadInfos("effects_positive_2100001B.json"); public static ImportedLayout LoadNegativeEffects() - => LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadNegativeEffectsInfos() => LoadInfos("effects_negative_2100001B.json"); @@ -155,31 +154,31 @@ internal static class FixtureLoader public static ImportedLayout LoadCharacterInformation() => LayoutImporter.Build( - LoadCharacterInformationInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + LoadCharacterInformationInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadCharacterInformationInfos() => LoadInfos("character_info_2100006E_10000183.json"); public static ImportedLayout LoadIndicators() - => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadIndicatorsInfos() => LoadInfos("indicators_21000071.json"); public static ImportedLayout LoadLinkStatus() - => LayoutImporter.Build(LoadLinkStatusInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadLinkStatusInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadLinkStatusInfos() => LoadInfos("link_status_2100001D.json"); public static ImportedLayout LoadVitae() - => LayoutImporter.Build(LoadVitaeInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadVitaeInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadVitaeInfos() => LoadInfos("vitae_21000020.json"); public static ImportedLayout LoadMiniGame() - => LayoutImporter.Build(LoadMiniGameInfos(), _ => (GpuTextureSlot.Unassigned, 0, 0), null); + => LayoutImporter.Build(LoadMiniGameInfos(), _ => (0u, 0, 0), null); public static ElementInfo LoadMiniGameInfos() => LoadInfos("mini_game_2100001E.json"); diff --git a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs index 28bd062b..8e5c01f9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class IndicatorBarControllerTests { private const uint Player = 0x50000001u; - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void AuthoredFixture_BuildsAllSevenRetailIndicatorButtons() diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 695af4ab..9e7dcc3f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -66,7 +65,7 @@ public class InventoryControllerTests StackSplitQuantityState? stackSplitQuantity = null, ItemInteractionController? itemInteraction = null) => InventoryController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, strength: () => strength, datFont: null, ownerName: ownerName is null ? null : () => ownerName, sendUse: uses is null ? null : g => uses.Add(g), @@ -84,7 +83,7 @@ public class InventoryControllerTests private static UiButton MakeButton(uint id) { var info = new ElementInfo { Id = id, Type = 1 }; - return new UiButton(info, static _ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 16, Height = 16 }; + return new UiButton(info, static _ => (0u, 0, 0)) { Width = 16, Height = 16 }; } // Seed a side bag (a container) in the player's pack, plus optionally its own contents. @@ -380,7 +379,7 @@ public class InventoryControllerTests { var (layout, grid, containers, top, _, _, _, _) = BuildLayout(); InventoryController.Bind(layout, new ClientObjectTable(), () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, strength: () => 100, + iconIds: (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, contentsEmptySprite: 0x06004D20u, sideBagEmptySprite: 0x06005D9Cu, mainPackEmptySprite: 0x06005D9Cu); @@ -557,7 +556,7 @@ public class InventoryControllerTests nowMs: () => 1_000); InventoryController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -590,7 +589,7 @@ public class InventoryControllerTests objects.AddOrUpdate(new ClientObject { ObjectId = Player, IconId = 0x06001234u }); (ItemType type, uint icon)? mainPackCall = null; InventoryController.Bind(layout, objects, () => Player, - iconIds: (t, icon, _, _, _) => { if (icon == 0x0600127Eu) mainPackCall = (t, icon); return GpuTextureSlot.Unassigned; }, + iconIds: (t, icon, _, _, _) => { if (icon == 0x0600127Eu) mainPackCall = (t, icon); return 0u; }, strength: () => 100, selection: new SelectionState(), datFont: null); // Retail draws a constant backpack over the Container type-underlay (IconData::RenderIcons @@ -683,7 +682,7 @@ public class InventoryControllerTests var puts = new List<(uint item, uint container, int placement)>(); using var ctrl = Bind(layout, objects, puts: puts, selection: selection); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(loot, GpuTextureSlot.Unassigned); + source.SetItem(loot, 0u); var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source); ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload); @@ -733,7 +732,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -782,7 +781,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -835,7 +834,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -843,7 +842,7 @@ public class InventoryControllerTests puts.Add((item, container, placement)), itemInteraction: interaction); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(draggedLoot, GpuTextureSlot.Unassigned); + source.SetItem(draggedLoot, 0u); inventory.HandleDropRelease( grid, @@ -895,7 +894,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -903,7 +902,7 @@ public class InventoryControllerTests puts.Add((item, container, placement)), itemInteraction: interaction); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(draggedLoot, GpuTextureSlot.Unassigned); + source.SetItem(draggedLoot, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(directLoot)); Assert.Equal( @@ -960,7 +959,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1023,7 +1022,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1031,7 +1030,7 @@ public class InventoryControllerTests merges.Add((source, target, amount)), itemInteraction: interaction); var sourceCell = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - sourceCell.SetItem(sourceStack, GpuTextureSlot.Unassigned); + sourceCell.SetItem(sourceStack, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(pendingLoot)); UiItemSlot targetCell = Enumerable.Range(0, grid.GetNumUIItems()) @@ -1084,7 +1083,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: selection, datFont: null, @@ -1093,7 +1092,7 @@ public class InventoryControllerTests itemInteraction: interaction, stackSplitQuantity: splitQuantity); var sourceCell = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - sourceCell.SetItem(sourceStack, GpuTextureSlot.Unassigned); + sourceCell.SetItem(sourceStack, 0u); Assert.True(interaction.PlaceWorldItemInBackpack(pendingLoot)); inventory.HandleDropRelease( @@ -1132,7 +1131,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1173,7 +1172,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1213,7 +1212,7 @@ public class InventoryControllerTests layout, objects, () => Player, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: () => 100, selection: new SelectionState(), datFont: null, @@ -1240,7 +1239,7 @@ public class InventoryControllerTests var puts = new List<(uint item, uint container, int placement)>(); using var ctrl = Bind(layout, objects, puts: puts); var source = new UiItemSlot { SourceKind = ItemDragSource.Ground }; - source.SetItem(loot, GpuTextureSlot.Unassigned); + source.SetItem(loot, 0u); var payload = new ItemDragPayload(loot, ItemDragSource.Ground, 0, source); ctrl.HandleDropRelease(grid, grid.GetItem(1)!, payload); diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs index 582e5643..32e074fe 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryFrameImportProbe.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.IO; using AcDream.App.UI; @@ -44,7 +43,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat — skip using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); // A representative spread across the slot grid (head, shield, the weapon composite, cloak, @@ -64,7 +63,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat — skip (this is a smoke test) using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); var backdrop = layout!.FindElement(Backdrop); @@ -96,7 +95,7 @@ public class InventoryFrameImportProbe if (datDir is null) return; // CI: no live dat - skip using var dats = new DatCollection(datDir, DatAccessType.Read); - var layout = LayoutImporter.Import(dats, Frame, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + var layout = LayoutImporter.Import(dats, Frame, _ => (0u, 0, 0), null); Assert.NotNull(layout); var close = layout!.FindElement(WindowChromeController.InventoryCloseButtonId); @@ -109,7 +108,7 @@ public class InventoryFrameImportProbe layout, new ClientObjectTable(), playerGuid: static () => 0u, - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, strength: static () => 100, selection: new AcDream.Core.Selection.SelectionState(), datFont: null, diff --git a/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs index ea74c23e..6a5d2a81 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ItemCooldownUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -31,7 +30,7 @@ public sealed class ItemCooldownUiControllerTests double now = 112.5d; var root = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(list); uint[] sprites = Enumerable.Range(1, 10) .Select(index => 0x06000000u + (uint)index) @@ -47,7 +46,7 @@ public sealed class ItemCooldownUiControllerTests Assert.Equal(sprites[5], list.Cell.ActiveCooldownSprite()); var future = new UiItemSlot(); - future.SetItem(0x5001u, new GpuTextureSlot(100u)); + future.SetItem(0x5001u, 100u); list.AddItem(future); Assert.Equal(sprites[5], future.ActiveCooldownSprite()); @@ -82,7 +81,7 @@ public sealed class ItemCooldownUiControllerTests var root = new UiPanel(); var window = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(window); window.AddChild(list); uint[] sprites = Enumerable.Range(1, 10) @@ -118,7 +117,7 @@ public sealed class ItemCooldownUiControllerTests var spellbook = new Spellbook(); var root = new UiPanel(); var list = new UiItemList(); - list.Cell.SetItem(0x5001u, new GpuTextureSlot(99u)); + list.Cell.SetItem(0x5001u, 99u); root.AddChild(list); int clockReads = 0; ItemCooldownUiController controller = ItemCooldownUiController.Bind( diff --git a/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs index 131024d3..641fa9f9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/JumpPowerbarLayoutConformanceTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ public sealed class JumpPowerbarLayoutConformanceTests public void RetailFixture_UsesExactFloatyGeometryTrackAndJumpFill() { ElementInfo info = FixtureLoader.LoadPowerbarInfos(); - ImportedLayout layout = LayoutImporter.Build(info, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + ImportedLayout layout = LayoutImporter.Build(info, _ => (0u, 0, 0), null); var meter = Assert.IsType( layout.FindElement(JumpPowerbarController.MeterId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs index 74ec3cc4..7bce7247 100644 --- a/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/LayoutImporterTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -10,7 +9,7 @@ namespace AcDream.App.Tests.UI.Layout; /// public class LayoutImporterTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); // ── Test 1: Health meter element → UiMeter with correct rect ───────────── diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs index e559da32..44fc107c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Items; @@ -60,7 +59,7 @@ public sealed class PaperdollClickMapTests ImportedLayout layout = Assert.IsType(LayoutImporter.Import( dats, 0x21000023u, - static _ => (GpuTextureSlot.Unassigned, 0, 0), + static _ => (0u, 0, 0), null)); UiElement dragMask = Assert.IsAssignableFrom( layout.FindElement(PaperdollController.DollDragMaskId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs index 49dfd1e3..ff74b09f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PaperdollControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -67,7 +66,7 @@ public class PaperdollControllerTests systemMessage: systemMessages is null ? null : systemMessages.Add); configureInteraction?.Invoke(itemInteraction); return PaperdollController.Bind(layout, objects, () => Player, - iconIds: (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: (_, _, _, _, _) => 0x1234u, itemInteraction: itemInteraction, emptySlotSprite: emptySlot, selection: selection ?? new SelectionState(), diff --git a/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs index 0a10482e..2d18244d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RadarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -133,7 +132,7 @@ public sealed class RadarControllerTests root.Children.Add(lockButton); root.Children.Add(Sprite(RadarController.DragButtonId, 87, 6, 27, 27, 0x060074C9u, type: 2)); - return LayoutImporter.Build(root, _ => (GpuTextureSlot.Unassigned, 0, 0), null); + return LayoutImporter.Build(root, _ => (0u, 0, 0), null); } private static ElementInfo Sprite( diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs index c0c8c2ac..1ea87958 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -181,7 +180,7 @@ public sealed class RetailPanelUiControllerTests return RetailWindowFrame.Mount( root, content, - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), new RetailWindowFrame.Options { WindowName = name, diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs index a7c90025..3d670338 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailWindowFrameTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class RetailWindowFrameTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Theory] [InlineData(9, 459f, 288f)] diff --git a/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs index 3563b1fd..57b3bca3 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SelectedObjectControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using System.Linq; @@ -53,7 +52,7 @@ public class SelectedObjectControllerTests ["StackedItemSelected"] = (0x06004CF4u, 3), }, }; - var overlayEl = new UiDatElement(overlayInfo, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var overlayEl = new UiDatElement(overlayInfo, _ => (0u, 0, 0)); dict[SelectedObjectController.OverlayId] = overlayEl; root.AddChild(overlayEl); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs index efb57f75..bccff893 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellExamineComponentTemplateFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -17,16 +16,16 @@ public sealed class SpellExamineComponentTemplateFactoryTests FixtureLoader.LoadExaminationComponentTemplateInfos(); var factory = new SpellExamineComponentTemplateFactory( template, - did => (new GpuTextureSlot(did + 1_000u), 32, 32), + did => (did + 1_000u, 32, 32), defaultFont: null); UiDatElement root = Assert.IsType( - factory.Create(new GpuTextureSlot(0xABCDu), owned)); + factory.Create(0xABCDu, owned)); UiDatElement missing = Assert.IsType( Assert.Single(root.Children)); Assert.Equal(SpellExamineComponentTemplateFactory.TemplateId, root.ElementId); - Assert.Equal(new GpuTextureSlot(0xABCDu), root.RuntimeImageTexture); + Assert.Equal(0xABCDu, root.RuntimeImageTexture); Assert.Empty(root.Children.OfType()); Assert.Equal( SpellExamineComponentTemplateFactory.MissingOverlayId, diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs index 9b6f0b21..0c431349 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Spells; using AcDream.Content; using AcDream.App.UI; @@ -31,7 +30,7 @@ public sealed class SpellbookWindowControllerTests { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadSpellbookInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: value => value.StringId switch { @@ -82,7 +81,7 @@ public sealed class SpellbookWindowControllerTests { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadSpellbookInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), datFont: null, stringResolve: value => value.StringId == 164868556u ? "Delete" : "Filter"); @@ -303,7 +302,7 @@ public sealed class SpellbookWindowControllerTests selection.Select(objectId, SelectionChangeSource.Inventory); }, setDesiredComponent: (component, amount) => desired.Add((component, amount)), - resolveComponentIcon: icon => new GpuTextureSlot(icon + 0x1000u))!; + resolveComponentIcon: icon => icon + 0x1000u)!; controller.ShowPage(SpellbookWindowPage.Components); UiItemList list = Assert.IsType( @@ -340,7 +339,7 @@ public sealed class SpellbookWindowControllerTests UiElement iconHost = Assert.IsAssignableFrom(copper.Content.FindElement( ComponentBookTemplateFactory.IconId)); Assert.Equal( - new GpuTextureSlot(0x06001100u), + 0x06001100u, Assert.IsType(Assert.Single(iconHost.Children)).Texture); copper.OnEvent(new UiEvent(0u, copper, UiEventType.Click)); @@ -402,7 +401,7 @@ public sealed class SpellbookWindowControllerTests SelectionState? selection = null, Action? selectObject = null, Action? setDesiredComponent = null, - Func? resolveComponentIcon = null) + Func? resolveComponentIcon = null) => SpellbookWindowController.Bind( layout, spellbook, @@ -410,8 +409,8 @@ public sealed class SpellbookWindowControllerTests () => 1u, components ?? new Dictionary(), selection ?? new SelectionState(), - spellId => new GpuTextureSlot(spellId + 1000u), - resolveComponentIcon ?? (iconId => new GpuTextureSlot(iconId)), + spellId => spellId + 1000u, + resolveComponentIcon ?? (iconId => iconId), spellId => spellId == 103u ? 8 : 1, selectObject ?? (_ => { }), addFavorite ?? (_ => { }), @@ -431,7 +430,7 @@ public sealed class SpellbookWindowControllerTests new ComponentBookTemplateFactory( FixtureLoader.LoadComponentCategoryTemplateInfos(), FixtureLoader.LoadComponentRowTemplateInfos(), - _ => (GpuTextureSlot.Unassigned, 0, 0), + _ => (0u, 0, 0), defaultFont: null), RowStyle, rowFont: null); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs index f31136b8..69464a70 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.Spells; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -11,7 +10,7 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class SpellcastingUiControllerTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] public void ImportedFixture_UsesRetailEmptyCells_ShortcutDigits_AndCompleteCastButton() @@ -161,8 +160,8 @@ public sealed class SpellcastingUiControllerTests Assert.True(host.Visible); UiCatalogSlot slot = Assert.IsType(host.Children[^1]); Assert.Equal(2u, slot.EntryId); - Assert.Equal(new GpuTextureSlot(2670u), slot.CatalogIconTexture); - Assert.Equal(new GpuTextureSlot(2u), slot.CatalogOverlayTexture); + Assert.Equal(2670u, slot.CatalogIconTexture); + Assert.Equal(2u, slot.CatalogOverlayTexture); slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); var cast = Assert.IsType( @@ -339,8 +338,8 @@ public sealed class SpellcastingUiControllerTests new NoopSpellCastOperations()); return SpellcastingUiController.Bind( layout, spellbook, casting, objects, () => 1u, - spellId => new GpuTextureSlot(spellId), - item => new GpuTextureSlot(item.ObjectId), + spellId => spellId, + item => item.ObjectId, useItem, selectionState, (tab, position, spellId) => diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index e4da3eef..31fb6e31 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System; using System.Collections.Generic; using AcDream.App.UI; @@ -67,7 +66,7 @@ public class ToolbarControllerTests void AddSlot(uint id) { - var list = new UiItemList(_ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 32, Height = 32 }; + var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = list; slots[id] = list; root.AddChild(list); } @@ -102,7 +101,7 @@ public class ToolbarControllerTests }, }; } - var button = new UiButton(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { Width = 32, Height = 32 }; + var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = button; root.AddChild(button); } @@ -118,10 +117,10 @@ public class ToolbarControllerTests { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); - Assert.Equal(new GpuTextureSlot(0x77u), slots[Row1[0]].Cell.IconTexture); + Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture); Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // others empty } @@ -144,7 +143,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(0x77u), + iconIds: (_, _, _, _, _) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); @@ -162,7 +161,7 @@ public class ToolbarControllerTests { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x88u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); @@ -178,7 +177,7 @@ public class ToolbarControllerTests var shortcuts = new List { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; var controller = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x88u), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); controller.Dispose(); controller.Dispose(); @@ -204,7 +203,7 @@ public class ToolbarControllerTests var selection = new SelectionState(); ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), + iconIds: (_,_,_,_,_) => 0x77u, useItem: g => used = g, selection: selection); UiItemSlot cell = slots[Row1[0]].Cell; @@ -227,7 +226,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); ctrl.BindPanelButtons( panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character, toggles.Add); @@ -278,7 +277,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, itemInteraction: interaction); ctrl.BindPanelButtons( @@ -305,7 +304,7 @@ public class ToolbarControllerTests var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); @@ -345,7 +344,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, useItem: static _ => { }, itemInteraction: interaction, playerGuid: () => player, @@ -395,7 +394,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: static (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: static (_, _, _, _, _) => 0u, useItem: static _ => { }, itemInteraction: interaction, playerGuid: () => player, @@ -427,7 +426,7 @@ public class ToolbarControllerTests var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); @@ -448,7 +447,7 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!; var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!; @@ -525,7 +524,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selected); @@ -583,7 +582,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selection.SelectedObjectId ?? 0u, @@ -649,7 +648,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selection.SelectedObjectId ?? 0u, @@ -692,7 +691,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; @@ -731,7 +730,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; @@ -753,7 +752,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => GpuTextureSlot.Unassigned, + iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, toggleCombat: () => toggles++); @@ -780,7 +779,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: id => used = id, selectItem: id => selected = id); @@ -817,7 +816,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction, selectItem: id => selection.Select(id, SelectionChangeSource.Toolbar), @@ -871,7 +870,7 @@ public class ToolbarControllerTests layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction); @@ -910,7 +909,7 @@ public class ToolbarControllerTests layout, repo, new ShortcutStore(), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction, sendAddShortcut: entry => sends.Add(((uint)entry.Index, entry.ObjectId))); @@ -936,7 +935,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); // Only peace indicator (index 0 = 0x10000192) is visible. Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind"); @@ -956,7 +955,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); ctrl.SetCombatMode(CombatMode.Melee); @@ -978,7 +977,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }, combatState: combat); // Initially NonCombat after bind. @@ -1017,7 +1016,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); // Top row: ShortcutNum == slot index, ghosted == false. @@ -1046,7 +1045,7 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(mode); @@ -1056,7 +1055,7 @@ public class ToolbarControllerTests var cell = slots[Row1[i]].Cell; Assert.Equal(i, cell.ShortcutNum); Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular in {mode}"); - cell.SetItem((uint)(0x5000 + i), new GpuTextureSlot(0x99u)); + cell.SetItem((uint)(0x5000 + i), 0x99u); Assert.Same(FakeRegular, cell.ActiveDigitArray()); } // Bottom row still has no number. @@ -1074,13 +1073,13 @@ public class ToolbarControllerTests var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(CombatMode.Magic); foreach (var id in Row1) { - slots[id].Cell.SetItem(id, new GpuTextureSlot(0x99u)); + slots[id].Cell.SetItem(id, 0x99u); Assert.True(slots[id].Cell.ShortcutGhosted); Assert.Same(FakeGhosted, slots[id].Cell.ActiveDigitArray()); } @@ -1106,7 +1105,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); foreach (var id in Row1) @@ -1128,7 +1127,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty); foreach (var id in Row1) @@ -1149,7 +1148,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null); foreach (var id in Row1) @@ -1175,7 +1174,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0x77u); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call from initial Populate @@ -1200,7 +1199,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0x99u); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { }); Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); @@ -1226,7 +1225,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5004u, SpellId: 0) }; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0xAAu), useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { }); Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound @@ -1253,7 +1252,7 @@ public class ToolbarControllerTests int iconCallCount = 0; ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => { iconCallCount++; return new GpuTextureSlot(0xBBu); }, useItem: _ => { }); + iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call for the shortcut item @@ -1275,7 +1274,7 @@ public class ToolbarControllerTests var ctrl = ToolbarController.Bind(layout, repo, new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); for (int i = 0; i < Row1.Length; i++) { @@ -1300,7 +1299,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0x5001u, ItemDragSource.Inventory, 0, new UiItemSlot()); @@ -1315,7 +1314,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot()); Assert.Equal(ItemDragAcceptance.None, ctrl.OnDragOver(list, list.Cell, payload)); @@ -1344,7 +1343,7 @@ public class ToolbarControllerTests uint selected = 0; var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem, selectItem: item => selected = item, selectedObjectId: () => selected); Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); @@ -1369,7 +1368,7 @@ public class ToolbarControllerTests new(Index: 5, ObjectId: 0x5002u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1398,7 +1397,7 @@ public class ToolbarControllerTests var (adds, _) = NewSpies(out var add, out var remove); var controller = ToolbarController.Bind( layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: remove); @@ -1421,7 +1420,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1447,7 +1446,7 @@ public class ToolbarControllerTests }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); @@ -1480,7 +1479,7 @@ public class ToolbarControllerTests }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_, _, _, _, _) => new GpuTextureSlot(1u), + iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); @@ -1505,7 +1504,7 @@ public class ToolbarControllerTests { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts), - iconIds: (_,_,_,_,_) => new GpuTextureSlot(0x77u), useItem: _ => { }, + iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem); var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell); @@ -1524,7 +1523,7 @@ public class ToolbarControllerTests var (layout, slots, _) = FakeToolbar(); ToolbarController.Bind(layout, new ClientObjectTable(), new ShortcutStore(), - iconIds: (_,_,_,_,_) => GpuTextureSlot.Unassigned, useItem: _ => { }); + iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9 } } diff --git a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs index dc20149b..5bf05158 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; @@ -11,7 +10,7 @@ public class UiDatElementTests var info = new ElementInfo(); info.StateMedia[""] = (0x06000001, 1); // DirectState (DrawMode Normal=1) info.StateMedia["ShowDetail"] = (0x06000002, 3); // named (Alphablend=3) - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { ActiveState = "ShowDetail" }; + var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "ShowDetail" }; Assert.Equal(0x06000002u, e.ActiveMedia().File); Assert.Equal(3, e.ActiveMedia().DrawMode); e.ActiveState = ""; @@ -22,7 +21,7 @@ public class UiDatElementTests [Fact] public void ActiveMedia_NoMedia_ReturnsZero() { - var e = new UiDatElement(new ElementInfo(), _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(new ElementInfo(), _ => (0, 0, 0)); Assert.Equal(0u, e.ActiveMedia().File); Assert.Equal(0, e.ActiveMedia().DrawMode); } @@ -32,7 +31,7 @@ public class UiDatElementTests { var info = new ElementInfo(); info.StateMedia[""] = (0x06000005, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) { ActiveState = "NoSuchState" }; + var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "NoSuchState" }; Assert.Equal(0x06000005u, e.ActiveMedia().File); } @@ -50,7 +49,7 @@ public class UiDatElementTests info.StateMedia["Normal"] = (0x0000AAAAu, 1); info.StateMedia["Hover"] = (0x0000BBBBu, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // Should have defaulted to "Normal" state. Assert.Equal(0x0000AAAAu, e.ActiveMedia().File); @@ -68,7 +67,7 @@ public class UiDatElementTests info.StateMedia["Maximized"] = (0x0000CCCCu, 1); info.StateMedia["Normal"] = (0x0000DDDDu, 1); - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // DefaultStateName "Minimized" wins over "Normal" implicit default. Assert.Equal(0x0000BBBBu, e.ActiveMedia().File); @@ -84,7 +83,7 @@ public class UiDatElementTests var info = new ElementInfo(); info.StateMedia[""] = (0x06007777u, 1); // DirectState only (e.g. vitals chrome corner) - var e = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var e = new UiDatElement(info, _ => (0, 0, 0)); // No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState). Assert.Equal(0x06007777u, e.ActiveMedia().File); @@ -101,7 +100,7 @@ public class UiDatElementTests Image = new UiImageMedia(0x06000009u, 3), }; info.StateMedia["ShowDetail"] = (0x06000009u, 3); - var element = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)) + var element = new UiDatElement(info, _ => (0u, 0, 0)) { Left = info.X, Top = info.Y, @@ -122,7 +121,7 @@ public class UiDatElementTests { var info = new ElementInfo { DefaultStateName = "Normal" }; info.StateMedia["Normal"] = (0x06000001u, 1); - var element = new UiDatElement(info, _ => (GpuTextureSlot.Unassigned, 0, 0)); + var element = new UiDatElement(info, _ => (0u, 0, 0)); Assert.False(element.TrySetRetailState(RetailUiStateIds.ShowDetail)); Assert.Equal("Normal", element.ActiveState); diff --git a/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs index dbcc3d32..03a4873c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiViewportFactoryTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -6,7 +5,7 @@ namespace AcDream.App.Tests.UI.Layout; public class UiViewportFactoryTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); [Fact] public void Factory_builds_UiViewport_for_dat_type_0xD() diff --git a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs index 815aabec..d45aa374 100644 --- a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs +++ b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; namespace AcDream.App.Tests.UI; @@ -23,7 +22,7 @@ public class MarkupDocumentTests " " + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)1, 32, 32)); Assert.IsType(panel); Assert.Equal(10f, panel.Left); @@ -43,7 +42,7 @@ public class MarkupDocumentTests "" + " " + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)1, 32, 32)); var meter = Assert.IsType(panel.Children[1]); Assert.Null(meter.Fill()); Assert.Null(meter.Label()); @@ -53,7 +52,7 @@ public class MarkupDocumentTests public void Build_ResizeAttrX_SetsHorizontalOnly() { const string xml = ""; - var panel = MarkupDocument.Build(xml, new object(), _ => (new GpuTextureSlot(1), 32, 32)); + var panel = MarkupDocument.Build(xml, new object(), _ => ((uint)1, 32, 32)); Assert.True(panel.ResizeX); Assert.False(panel.ResizeY); } @@ -66,7 +65,7 @@ public class MarkupDocumentTests "backleft=\"0x06001141\" backtile=\"0x06001140\" backright=\"0x0600113F\" " + "frontleft=\"0x06001131\" fronttile=\"0x06001132\" frontright=\"0x06001133\"/>" + ""; - var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => (new GpuTextureSlot(7), 32, 32)); + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)7, 32, 32)); var meter = Assert.IsType(panel.Children[1]); Assert.Equal(0x06001141u, meter.BackLeft); Assert.Equal(0x06001140u, meter.BackTile); diff --git a/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs b/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs index 899726a7..e1b0bc00 100644 --- a/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailUiAutomationProbeTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using System.IO; using AcDream.App.UI; @@ -97,7 +96,7 @@ public sealed class RetailUiAutomationProbeTests var root = new UiRoot { Width = 240, Height = 120 }; var objects = new ClientObjectTable(); - var source = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var source = new UiItemList(_ => (1u, 1, 1)) { DatElementId = 0x10000010u, Left = 10, @@ -107,9 +106,9 @@ public sealed class RetailUiAutomationProbeTests }; source.Cell.SlotIndex = 0; source.Cell.SourceKind = ItemDragSource.Inventory; - source.Cell.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + source.Cell.SetItem(0x5001u, 0x99u); - var target = new UiItemList(_ => (new GpuTextureSlot(1u), 1, 1)) + var target = new UiItemList(_ => (1u, 1, 1)) { DatElementId = 0x10000020u, Left = 70, diff --git a/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs b/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs index 62de7e12..44b0144c 100644 --- a/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailUiInteractionFlowTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; @@ -69,7 +68,7 @@ public sealed class RetailUiInteractionFlowTests height: 32); var slotsButton = new UiButton( new ElementInfo { Id = SlotsButtonId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)) + static _ => (0u, 0, 0)) { DatElementId = SlotsButtonId, Left = 270, @@ -87,7 +86,7 @@ public sealed class RetailUiInteractionFlowTests }; var dollDragMask = new UiButton( new ElementInfo { Id = PaperdollController.DollDragMaskId, Type = 1 }, - static _ => (GpuTextureSlot.Unassigned, 0, 0)) + static _ => (0u, 0, 0)) { DatElementId = PaperdollController.DollDragMaskId, Left = 270, @@ -179,7 +178,7 @@ public sealed class RetailUiInteractionFlowTests Layout, Objects, playerGuid: () => Player, - iconIds: static (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: static (_, _, _, _, _) => 0x1234u, strength: () => 100, selection: Selection, datFont: null, @@ -213,7 +212,7 @@ public sealed class RetailUiInteractionFlowTests Layout, Objects, playerGuid: () => Player, - iconIds: static (_, _, _, _, _) => new GpuTextureSlot(0x1234u), + iconIds: static (_, _, _, _, _) => 0x1234u, selection: Selection, itemInteraction: itemInteraction, emptySlotSprite: 0x06004D20u, @@ -266,7 +265,7 @@ public sealed class RetailUiInteractionFlowTests } private static UiItemList ItemList(uint id, float left, float top, float width, float height) - => new(static _ => (new GpuTextureSlot(1u), 32, 32)) + => new(static _ => (1u, 32, 32)) { DatElementId = id, Left = left, diff --git a/tests/AcDream.App.Tests/UI/UiButtonTests.cs b/tests/AcDream.App.Tests/UI/UiButtonTests.cs index c1238c16..b653e39e 100644 --- a/tests/AcDream.App.Tests/UI/UiButtonTests.cs +++ b/tests/AcDream.App.Tests/UI/UiButtonTests.cs @@ -1,11 +1,10 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; public class UiButtonTests { - private static (GpuTextureSlot, int, int) NoTex(uint _) => (GpuTextureSlot.Unassigned, 0, 0); + private static (uint, int, int) NoTex(uint _) => (0, 0, 0); private bool _clicked; [Fact] diff --git a/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs b/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs index 2127c8f9..bc7abf5b 100644 --- a/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs +++ b/tests/AcDream.App.Tests/UI/UiCollapsibleFrameTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; using Xunit; @@ -8,7 +7,7 @@ public class UiCollapsibleFrameTests { private static UiCollapsibleFrame MakeFrame(out UiPanel row2a, out UiPanel row2b) { - var f = new UiCollapsibleFrame(_ => (new GpuTextureSlot(1u), 1, 1)) + var f = new UiCollapsibleFrame(_ => (1u, 1, 1)) { CollapsedHeight = 96f, ExpandedHeight = 128f, @@ -45,7 +44,7 @@ public class UiCollapsibleFrameTests [Fact] public void Tick_notConfigured_isNoOp() { - var f = new UiCollapsibleFrame(_ => (new GpuTextureSlot(1u), 1, 1)); // Collapsed==Expanded==0 + var f = new UiCollapsibleFrame(_ => (1u, 1, 1)); // Collapsed==Expanded==0 f.Height = 50f; f.TickForTest(0.016); Assert.Equal(50f, f.Height); // unchanged, no divide/no forced height diff --git a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs index 3700440c..923af959 100644 --- a/tests/AcDream.App.Tests/UI/UiDatFontTests.cs +++ b/tests/AcDream.App.Tests/UI/UiDatFontTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Collections.Generic; using AcDream.App.UI; using DatReaderWriter.Types; @@ -92,10 +91,10 @@ public class UiDatFontTests ['B'] = Glyph('B', width: 7, before: 1, after: 1), }; var font = new UiDatFont( - fgTex: GpuTextureSlot.Unassigned, + fgTex: 0, fgW: 0, fgH: 0, - bgTex: GpuTextureSlot.Unassigned, + bgTex: 0, bgW: 0, bgH: 0, lineHeight: 16f, diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs index e75965d4..de5b6946 100644 --- a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs +++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using AcDream.App.UI; namespace AcDream.App.Tests.UI; @@ -22,26 +21,26 @@ public class UiItemSlotTests { var s = new UiItemSlot(); Assert.Equal(0u, s.ItemId); - Assert.Equal(GpuTextureSlot.Unassigned, s.IconTexture); + Assert.Equal(0u, s.IconTexture); } [Fact] public void SetItem_setsIdAndTexture() { var s = new UiItemSlot(); - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); Assert.Equal(0x5001u, s.ItemId); - Assert.Equal(new GpuTextureSlot(0x99u), s.IconTexture); + Assert.Equal(0x99u, s.IconTexture); } [Fact] public void Clear_afterSetItem_resetsToEmpty() { var s = new UiItemSlot(); - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.Clear(); Assert.Equal(0u, s.ItemId); - Assert.Equal(GpuTextureSlot.Unassigned, s.IconTexture); + Assert.Equal(0u, s.IconTexture); } [Fact] @@ -59,7 +58,7 @@ public class UiItemSlotTests [Fact] public void CatalogSlot_keeps_catalog_identity_separate_from_object_guid() { - var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = new GpuTextureSlot(99u) }; + var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = 99u }; Assert.Equal(1234u, slot.EntryId); Assert.Equal(0u, slot.ItemId); @@ -148,7 +147,7 @@ public class UiItemSlotTests public void ActiveDigitArray_occupiedSlot_regular_returnsRegularDigits() { var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.SetShortcutNum(0, ghosted: false); Assert.Same(Regular, s.ActiveDigitArray()); } @@ -157,7 +156,7 @@ public class UiItemSlotTests public void ActiveDigitArray_occupiedSlot_ghosted_returnsGhostedDigits() { var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; - s.SetItem(0x5001u, new GpuTextureSlot(0x99u)); + s.SetItem(0x5001u, 0x99u); s.SetShortcutNum(0, ghosted: true); Assert.Same(Ghosted, s.ActiveDigitArray()); } @@ -209,7 +208,7 @@ public class UiItemSlotTests CooldownSprites = sprites, CooldownStepProvider = id => id == 0x5001u ? 6 : 0, }; - slot.SetItem(0x5001u, new GpuTextureSlot(99u)); + slot.SetItem(0x5001u, 99u); Assert.Equal(0x06000006u, slot.ActiveCooldownSprite()); } @@ -224,7 +223,7 @@ public class UiItemSlotTests }; Assert.Equal(0u, slot.ActiveCooldownSprite()); - slot.SetItem(0x5001u, new GpuTextureSlot(99u)); + slot.SetItem(0x5001u, 99u); Assert.Equal(0u, slot.ActiveCooldownSprite()); } } diff --git a/tests/AcDream.App.Tests/UI/UiRootInputTests.cs b/tests/AcDream.App.Tests/UI/UiRootInputTests.cs index df51877f..ceca3432 100644 --- a/tests/AcDream.App.Tests/UI/UiRootInputTests.cs +++ b/tests/AcDream.App.Tests/UI/UiRootInputTests.cs @@ -1,4 +1,3 @@ -using AcDream.App.Rendering.Gpu; using System.Numerics; using AcDream.App.UI; @@ -11,7 +10,7 @@ public class UiRootInputTests { // Regression: the per-frame anchor pass must NOT reset a window's rect, // or move/resize get undone every frame. Windows are user-positioned. - var panel = new UiNineSlicePanel(_ => (new GpuTextureSlot(1), 32, 32)); + var panel = new UiNineSlicePanel(_ => ((uint)1, 32, 32)); Assert.Equal(AnchorEdges.None, panel.Anchors); } @@ -120,7 +119,7 @@ public class UiRootInputTests { var root = new UiRoot { Width = 800, Height = 600 }; var cell = new UiItemSlot { Left = 0, Top = 0, Width = 32, Height = 32 }; - cell.SetItem(0x50000A01u, new GpuTextureSlot(0x99u)); + cell.SetItem(0x50000A01u, 0x99u); root.AddChild(cell); object? payload = null; int releaseX = 0;