diff --git a/.gitignore b/.gitignore index 904fdf9c..1731f2eb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,3 @@ references/ # Claude Code session state .claude/ launch.log -launch-*.log - -# ImGui auto-saved window/docking state (per-user, not source) -imgui.ini diff --git a/AcDream.slnx b/AcDream.slnx index 1cf8f243..e7fd39ac 100644 --- a/AcDream.slnx +++ b/AcDream.slnx @@ -6,8 +6,6 @@ - - @@ -16,6 +14,5 @@ - diff --git a/CLAUDE.md b/CLAUDE.md index 96449ed3..84c1d809 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,14 +25,13 @@ The codebase is organized by layer (see architecture doc). Current phase state lives in memory (`memory/project_*.md`), plans in `docs/plans/`, research in `docs/research/`. -**UI strategy:** three-layer split — swappable backend (ImGui.NET + -`Silk.NET.OpenGL.Extensions.ImGui` for Phase D.2a short-term, custom -retail-look toolkit for D.2b later) / stable `AcDream.UI.Abstractions` -layer (ViewModels + Commands + `IPanel` / `IPanelRenderer`) / unchanged -game state. **All plugin-facing UI targets `AcDream.UI.Abstractions` — -never import a backend namespace from a panel.** Full design: -`docs/plans/2026-04-24-ui-framework.md`. Memory crib: -`memory/project_ui_architecture.md`. +**UI strategy:** three-layer split — swappable backend (Hexa.NET.ImGui for +Phase D.2a short-term, custom retail-look toolkit for D.2b later) / +stable `AcDream.UI.Abstractions` layer (ViewModels + Commands + `IPanel` +/ `IPanelRenderer`) / unchanged game state. **All plugin-facing UI +targets `AcDream.UI.Abstractions` — never import a backend namespace +from a panel.** Full design: `docs/plans/2026-04-24-ui-framework.md`. +Memory crib: `memory/project_ui_architecture.md`. ## How to operate diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 5f2c01a9..38d52ec9 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -135,29 +135,6 @@ Copy this block when adding a new issue: --- -## #5 — VitalsPanel stamina/mana bars always null (absolute values not stored) - -**Status:** OPEN -**Severity:** LOW (cosmetic — HP bar already works; stam/mana would be a nice-to-have) -**Filed:** 2026-04-25 -**Component:** ui / net / player-state - -**Description:** Phase D.2a shipped `VitalsVM` with `StaminaPercent` / `ManaPercent` returning `float?` null. `VitalsPanel` already renders an HP progress bar from `CombatState.GetHealthPercent(localGuid)` because per-entity health is tracked from combat notifications. Stamina and mana are absolute values and only arrive in `PlayerDescription (0x0013)` — which we currently parse then discard. Result: the Vitals window shows HP only. - -**Root cause / status:** We need a `LocalPlayerState` Core class (analogous to `CombatState` but scoped to the local player) that retains parsed `PlayerDescription` fields — at minimum: `CurrentStamina` + `MaxStamina` + `CurrentMana` + `MaxMana`. `AppraiseInfoParser.CreatureProfile` already has the shape for these values; we just don't persist them. - -**Files:** -- `src/AcDream.Core.Net/Parsers/PlayerDescriptionParser.cs` — parses then discards (verify path) -- `src/AcDream.Core.Net/Parsers/AppraiseInfoParser.cs` — has `CreatureProfile` with absolute values -- `src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs` — `StaminaPercent` / `ManaPercent` would divide `LocalPlayerState.Current*` by `Max*` -- `src/AcDream.App/Rendering/GameWindow.cs` — construct `LocalPlayerState`, hand to `VitalsVM`, wire into event dispatch - -**Research:** none needed — wire-level field positions are already decoded in `PlayerDescriptionParser`. - -**Acceptance:** With `ACDREAM_DEVTOOLS=1`, the Vitals window shows three progress bars (HP / Stamina / Mana) that update when the server sends `PlayerDescription` or any delta event (`UpdateHealth`, `UpdateStamina`, `UpdateMana`). - ---- - # Recently closed *(none yet — move DONE items here with closed-date + commit SHA)* diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index 80536a17..e5f7f856 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -74,8 +74,7 @@ designed 2026-04-24. Full design: `docs/plans/2026-04-24-ui-framework.md`. ``` ┌─────────────────────────────────────────────────────────────┐ │ UI BACKEND (swappable) │ -│ ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui │ -│ (Phase D.2a, short-term) │ +│ Hexa.NET.ImGui (Phase D.2a, short-term) │ │ or custom retail-look toolkit (Phase D.2b, later) │ ├─────────────────────────────────────────────────────────────┤ │ AcDream.UI.Abstractions (stable contract) │ diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index aabe5685..15595904 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -46,7 +46,6 @@ | G.1+ | Full sky visuals + weather + dynamic-light shader — SkyDescLoader parses Region 0x13000000 dat keyframes with retail fog fields (start/end/mode); WeatherSystem picks Clear/Overcast/Rain/Snow/Storm deterministically per in-game day with 10s fade; SkyRenderer draws far-plane-1e6 celestial meshes with UV scroll; SceneLightingUbo binds at std140 location=1 with 8 Light slots + fog + lightning flash; terrain.vert + mesh.frag + mesh_instanced.frag + sky.frag all consume the shared UBO; LightingHookSink auto-registers Setup.Lights per entity + flips IsLit on SetLightHook; ParticleRenderer renders rain/snow billboards; F7 cycles day time override, F10 cycles weather; WorldSession surfaces server time via ServerTimeUpdated (ConnectRequest + TimeSync flag) | Tests ✓ | | H.1 | Chat wire layer — Talk (0x0015) / Tell (0x005D) / ChatChannel (0x0147) outbound, HearSpeech (0x02BB local + 0x02BC ranged) inbound, ChatLog ring buffer with adapters for every chat source | Tests ✓ | | Glue | GameEventWiring.WireAll — single-call registration mapping parsed GameEvents → Core state classes (ChatLog, CombatState, Spellbook, ItemRepository); GameWindow exposes state classes + wires them to live session | Tests ✓ | -| D.2a | UI scaffold — `AcDream.UI.Abstractions` stable contract (`IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + `VitalsVM` / `VitalsPanel`); `AcDream.UI.ImGui` backend on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` (pivoted from Hexa.NET.ImGui on 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL and crashed 0xC0000005 without them); VitalsPanel wired into GameWindow behind `ACDREAM_DEVTOOLS=1` with `ImGui.WantCaptureKeyboard` WASD suppression. 11 new tests. | Live ✓ | Plus polish that doesn't get its own phase number: - FlyCamera default speed lowered + Shift-to-boost @@ -127,11 +126,9 @@ Plus polish that doesn't get its own phase number: > [`docs/plans/2026-04-24-ui-framework.md`](2026-04-24-ui-framework.md) > for the full design. Short version: > -> 1. **D.2a — ImGui as the short-term backend.** Wire up in days, +> 1. **D.2a — Hexa.NET.ImGui as the short-term backend.** Wire up in days, > iterate game logic (chat-send, inventory actions, vitals HUD reading -> real state) in weeks. Looks like a debugger; that's fine. *(Shipped -> 2026-04-25 on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` after a -> day-one pivot away from Hexa.NET.ImGui; see shipped table.)* +> real state) in weeks. Looks like a debugger; that's fine. > 2. **Stable `AcDream.UI.Abstractions` layer** — ViewModels + Commands + > `IPanel` / `IPanelRenderer` interfaces. Backend-agnostic. Plugin API > publishes against this layer and never sees ImGui. @@ -145,7 +142,7 @@ Plus polish that doesn't get its own phase number: **Sub-pieces:** - **D.1 — 2D ortho overlay + font rendering.** ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas + `TextRenderer` + `DebugOverlay`). -- **✓ SHIPPED — D.2a — ImGui scaffold + `AcDream.UI.Abstractions` layer.** Shipped 2026-04-25. Wires ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModel (`VitalsVM`) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP from `CombatState.GetHealthPercent`. **Backend pivoted Hexa.NET.ImGui → ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` during integration** — Hexa's native OpenGL3 backend does its own GL function resolution via GLFW/SDL and crashed with `0xC0000005` in `ImGuiImplOpenGL3.InitNative` against Silk.NET (no GLFW/SDL present). The Silk.NET extension is purpose-built for this scenario and is the `ImGui.NET` mitigation path that `docs/plans/2026-04-24-ui-framework.md` already called out as a "one-morning operation". Stam/Mana return `float?` null in D.2a because absolute values need `LocalPlayerState` + `PlayerDescription (0x0013)` parsing (filed post-D.2a). 11 new `AcDream.UI.Abstractions.Tests` green. +- **D.2a — Hexa.NET.ImGui scaffold + `AcDream.UI.Abstractions` layer.** NEW pre-piece introduced 2026-04-24. Wires Hexa.NET.ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModels (`VitalsVM` etc.) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP/stam/mana from `IGameState`. This is what gets game-logic iteration moving; looks like a debugger, acceptable. - **D.2b — Custom retail-look backend.** Implements the same `IPanel` / `IPanelRenderer` contracts using a custom retained-mode toolkit sourced from retail dat assets. Requires D.2a shipped. Panels get reskinned one at a time; ImGui stays as the `ACDREAM_DEVTOOLS=1` overlay forever. The original 2026-04-17 scaffold research (`UiRoot` / `UiElement` / `UiPanel` / `UiHost` + retail event codes + focus / drag-drop state machine + `WorldMouseFallThrough`) is the implementation foundation here — see `docs/research/retail-ui/`. - **D.3 — AcFont from portal.dat.** Replace stb_truetype system font with retail `Font` DBObjs (`0x40000000..0x40000FFF`) baked from `RenderSurface` source sheets — see research slice 03 §4. Preserves retail visual identity. **(D.2b dependency — needs the custom renderer.)** - **D.4 — Dat sprites + 9-slice panel backgrounds.** Load `RenderSurface` (`0x06xxxxxx`) as GL textures; add `DrawSprite` to `UiRenderContext`. Enables retail panel art. **(D.2b dependency.)** diff --git a/docs/plans/2026-04-24-ui-framework.md b/docs/plans/2026-04-24-ui-framework.md index e6fce264..85d8a41c 100644 --- a/docs/plans/2026-04-24-ui-framework.md +++ b/docs/plans/2026-04-24-ui-framework.md @@ -1,51 +1,13 @@ # UI framework plan -**Date:** 2026-04-24 (design), shipped 2026-04-25 -**Status:** **Phase D.2a shipped** — `AcDream.UI.Abstractions` + ImGui backend -+ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from -`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during -first-light integration — see the pivot note below. Phase D.2b (custom -retail-look backend) remains design-only. +**Date:** 2026-04-24 +**Status:** design — not yet implemented **Owner:** lead engineer (erik) + Claude Captures the UI strategy agreed via discussion on 2026-04-24. Documents the choices AND the alternatives considered so future sessions can re-evaluate with the same context. -## 2026-04-25 pivot: Hexa.NET.ImGui → ImGui.NET - -The original choice (documented below) was `Hexa.NET.ImGui` + -`Hexa.NET.ImGui.Backends.OpenGL3`. It did not survive first-light -integration: - -- First launch with Hexa's backend crashed with `0xC0000005` inside - `Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative`. -- Root cause: Hexa's native OpenGL3 backend does its own GL function - resolution, looking up symbols via GLFW or SDL. Silk.NET uses neither, - so the resolved function pointers were null and the native code - dereferenced them on init. -- Hexa's Silk.NET examples rely on GLFW being co-loaded (its default - on Hexa's own scenes) — not applicable here. - -**Mitigation path** was already written into this doc (§"What we give -up": *"switching to ImGui.NET later is a one-morning operation if Hexa -misbehaves"*) and taken: - -- Packages swapped → `ImGui.NET 1.91.6.1` + `Silk.NET.OpenGL.Extensions.ImGui 2.23.0`. -- `Silk.NET.OpenGL.Extensions.ImGui.ImGuiController` handles the whole - integration (GL backend init against the Silk.NET GL binding, - keyboard + mouse IO event subscription). No hand-written input - bridge needed. -- `ImGuiBootstrapper` is a ~10-line `IDisposable` wrapping the - `ImGuiController` instance. `ImGuiPanelRenderer` wraps `ImGuiNET.ImGui.*`. -- Boundary discipline preserved — panels never import `ImGuiNET` - directly; they only use `IPanelRenderer`. The backend swap is - invisible above the abstraction layer, as designed. - -Sections below from §"Choice: Hexa.NET.ImGui" onward are kept as the -historical design reasoning. They remain useful if we ever re-evaluate -native AOT / upstream-tracking tradeoffs. - ## Goal acdream needs a playable game UI: chat, vitals HUD, inventory, character diff --git a/docs/research/retail-ui/00-master-synthesis.md b/docs/research/retail-ui/00-master-synthesis.md index 0dbbf771..7882a9cb 100644 --- a/docs/research/retail-ui/00-master-synthesis.md +++ b/docs/research/retail-ui/00-master-synthesis.md @@ -1,12 +1,10 @@ # Retail AC Client GUI — Master Synthesis -> **Scope note (2026-04-24, updated 2026-04-25):** This document -> describes retail's Keystone UI toolkit — it is the research foundation -> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a -> (shipped ImGui scaffold, `AcDream.UI.Abstractions` + ImGui.NET + -> `Silk.NET.OpenGL.Extensions.ImGui` + `VitalsPanel`). When reading this -> for implementation guidance, assume D.2a has shipped a working -> `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`, +> **Scope note (2026-04-24):** This document describes retail's Keystone +> UI toolkit — it is the research foundation for **Phase D.2b (custom +> retail-look backend)**, not **Phase D.2a (Hexa.NET.ImGui scaffold)**. +> When reading this for implementation guidance, assume D.2a has shipped +> a working `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`, > ViewModels, Commands) and you are building the custom retained-mode > toolkit that implements the same contracts using dat-sourced fonts / > sprites / cursors. See `docs/plans/2026-04-24-ui-framework.md` for the diff --git a/memory/project_ui_architecture.md b/memory/project_ui_architecture.md index bf5e4ad6..2d6240de 100644 --- a/memory/project_ui_architecture.md +++ b/memory/project_ui_architecture.md @@ -15,14 +15,10 @@ └─────────────────────────────────────────┘ ``` -- **UI backend** (bottom swap axis): **`ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`** for Phase D.2a +- **UI backend** (bottom swap axis): `Hexa.NET.ImGui` for Phase D.2a (short-term, debugger-look, validates game logic fast). Custom retail-look toolkit for Phase D.2b (long-term, uses dat assets). ImGui stays **forever** as the `ACDREAM_DEVTOOLS=1` overlay. - (Pivoted from `Hexa.NET.ImGui` on 2026-04-25 — Hexa's native OpenGL3 - backend resolves GL via GLFW/SDL internally and crashed `0xC0000005` - against Silk.NET; the Silk.NET extension is purpose-built for this - stack.) - **ViewModels + Commands** (the stable contract): per-panel data records (`VitalsVM`, `InventoryVM`, `ChatVM`, …) and action records (`UseItemCmd`, `SendChatCmd`, `CastSpellCmd`, …). Lives in @@ -36,19 +32,16 @@ - `src/AcDream.UI.Abstractions/` — `IPanel`, `IPanelHost`, `IPanelRenderer`, `ICommandBus`, all ViewModels + Commands. Backend- agnostic. -- `src/AcDream.UI.ImGui/` — `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` - implementation of `IPanelRenderer` + ImGui bootstrap. Phase D.2a. - `ImGuiController` (from the Silk.NET extension) handles GL backend + - input event subscription; `ImGuiBootstrapper` is a thin IDisposable - wrapper; `ImGuiPanelRenderer` wraps the widgets `IPanelRenderer` needs. +- `src/AcDream.UI.ImGui/` — Hexa.NET.ImGui-based implementation of + `IPanelRenderer` + ImGui bootstrap. Phase D.2a. - `src/AcDream.UI.Retail/` (later) — custom retained-mode toolkit using dat assets, same `IPanelRenderer` contract. Phase D.2b. ## Hard rules 1. **No panel references a backend namespace.** If a panel imports - `ImGuiNET` / `Silk.NET.OpenGL.Extensions.ImGui` or a custom-toolkit - widget class directly, it's a bug — extend `IPanelRenderer` instead. + `Hexa.NET.ImGui` or a custom-toolkit widget class directly, it's a + bug. 2. **Plugin API targets the abstraction layer only.** Plugins define `IPanel` instances; they never see which backend draws them. 3. **Features that only ImGui can express → not in `IPanelRenderer`.** diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index 6fb3af04..277ae99c 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -24,8 +24,6 @@ - - diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ab4479f3..cbd4b27f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -281,14 +281,6 @@ public sealed class GameWindow : IDisposable public readonly AcDream.Core.Spells.Spellbook SpellBook = new(); public readonly AcDream.Core.Items.ItemRepository Items = new(); - // 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.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap; - private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost; - private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm; - private static readonly bool DevToolsEnabled = - Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1"; - // Phase G.1-G.2 world lighting/time state. public readonly AcDream.Core.World.WorldTimeService WorldTime = new AcDream.Core.World.WorldTimeService( @@ -878,35 +870,6 @@ public sealed class GameWindow : IDisposable } } - // Phase D.2a — ImGui devtools overlay. Zero cost when the env var - // isn't set: no context creation, no per-frame branches hit. - // See docs/plans/2026-04-24-ui-framework.md + memory/project_ui_architecture.md. - if (DevToolsEnabled) - { - try - { - _imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!); - _panelHost = new AcDream.UI.ImGui.ImGuiPanelHost(); - - // VitalsVM: GUID=0 at construction; set later at EnterWorld - // (see the _playerServerGuid assignment path). Pre-login the - // HP bar just reads 1.0 (safe default) — harmless. - _vitalsVm = new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat); - _panelHost.Register( - new AcDream.UI.Abstractions.Panels.Vitals.VitalsPanel(_vitalsVm)); - - Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel registered)"); - } - catch (Exception ex) - { - Console.WriteLine($"devtools: ImGui init failed: {ex.Message} — devtools disabled"); - _imguiBootstrap?.Dispose(); - _imguiBootstrap = null; - _panelHost = null; - _vitalsVm = null; - } - } - uint centerLandblockId = 0xA9B4FFFFu; Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}"); @@ -1186,7 +1149,6 @@ public sealed class GameWindow : IDisposable var chosen = _liveSession.Characters.Characters[0]; _playerServerGuid = chosen.Id; // Phase B.2: store for Tab-key player-mode entry - _vitalsVm?.SetLocalPlayerGuid(chosen.Id); // Phase D.2a — devtools HP bar tracks this guid _worldState.MarkPersistent(chosen.Id); // player entity survives landblock unloads Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}"); _liveSession.EnterWorld(user, characterIndex: 0); @@ -3540,13 +3502,6 @@ public sealed class GameWindow : IDisposable var kb = _input.Keyboards[0]; - // Phase D.2a — suppress game-side WASD / interaction polling when - // ImGui has keyboard focus (e.g. a text field is active). Without - // this, typing "walk" into a chat field would actually walk. - bool suppressGameInput = - DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard; - if (suppressGameInput) return; - if (_cameraController.IsFlyMode) { _cameraController.Fly.Update( @@ -3754,12 +3709,6 @@ public sealed class GameWindow : IDisposable _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - // Phase D.2a — begin ImGui frame. Paired with the Render() call - // after the scene draws (below). ImGuiController.Update() - // consumes buffered Silk.NET input events and calls ImGui.NewFrame. - if (DevToolsEnabled && _imguiBootstrap is not null) - _imguiBootstrap.BeginFrame((float)deltaSeconds); - // Phase 6.4: advance per-entity animation playback before drawing // so the renderer always sees the up-to-date per-part transforms. if (_animatedEntities.Count > 0) @@ -4053,20 +4002,6 @@ public sealed class GameWindow : IDisposable } } - // Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws - // so ImGui composites on top. ImGuiController save/restores the - // GL state it touches (blend, scissor, VAO, shader, texture); any - // state not in its save-list (e.g. GL_FRAMEBUFFER_SRGB, unused - // today) would need manual protection. - if (DevToolsEnabled && _imguiBootstrap is not null && _panelHost is not null) - { - var ctx = new AcDream.UI.Abstractions.PanelContext( - (float)deltaSeconds, - AcDream.UI.Abstractions.NullCommandBus.Instance); - _panelHost.RenderAll(ctx); - _imguiBootstrap.Render(); - } - // Update the window title with performance stats every ~0.5s. _perfAccum += deltaSeconds; _perfFrameCount++; diff --git a/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj b/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj deleted file mode 100644 index 0fc070c7..00000000 --- a/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - net10.0 - enable - enable - latest - true - - - - - diff --git a/src/AcDream.UI.Abstractions/ICommandBus.cs b/src/AcDream.UI.Abstractions/ICommandBus.cs deleted file mode 100644 index 71742969..00000000 --- a/src/AcDream.UI.Abstractions/ICommandBus.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// Publishes user-intent commands from panels to the systems that handle -/// them (WorldSession, ChatService, Inventory, ...). Panels never touch -/// those systems directly — they a record -/// and the bus dispatches. -/// -/// -/// D.2a scaffolding: is the default wire-up -/// — commands are accepted but dropped. Real routing lands alongside -/// chat and inventory (Sprint 2 of the UI plan) when we actually need -/// commands flowing server-ward. -/// -/// -public interface ICommandBus -{ - /// - /// Publish a command record. The bus routes by runtime type via - /// registered handlers. Never blocks; handlers run on the publish - /// thread today (render thread for panel-triggered commands). - /// - void Publish(T command) where T : notnull; -} diff --git a/src/AcDream.UI.Abstractions/IPanel.cs b/src/AcDream.UI.Abstractions/IPanel.cs deleted file mode 100644 index 5a4a37f7..00000000 --- a/src/AcDream.UI.Abstractions/IPanel.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// A UI panel — chat window, inventory, vitals HUD, character sheet, etc. -/// Panels are backend-agnostic: they only call into -/// primitives, never reach through to a specific UI library (Hexa.NET.ImGui -/// in Phase D.2a, a custom retail-look toolkit in Phase D.2b). -/// -/// -/// Hard rule: no using Hexa.NET.ImGui inside a panel file. If a -/// widget needs a feature the abstraction doesn't expose, extend -/// ; do not import the backend. See -/// docs/plans/2026-04-24-ui-framework.md. -/// -/// -public interface IPanel -{ - /// Stable, globally-unique identifier. Convention: acdream.{name}. - string Id { get; } - - /// Human-readable window title shown in the chrome of the panel. - string Title { get; } - - /// - /// Whether the panel is currently visible. Backends read this per frame; - /// panels may mutate it in response to their own close-button handling. - /// - bool IsVisible { get; set; } - - /// - /// Draw the panel for one frame. Called by - /// on the render thread once ImGui's (or the future custom backend's) - /// frame has begun. Panels issue drawing calls through - /// and publish user-intent actions through .. - /// - void Render(PanelContext ctx, IPanelRenderer renderer); -} diff --git a/src/AcDream.UI.Abstractions/IPanelHost.cs b/src/AcDream.UI.Abstractions/IPanelHost.cs deleted file mode 100644 index bbd685f7..00000000 --- a/src/AcDream.UI.Abstractions/IPanelHost.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// Owns the set of live s and drives per-frame draw -/// dispatch. The backend (Hexa.NET.ImGui in D.2a, custom in D.2b) implements -/// this; GameWindow creates one at startup and registers panels. -/// -/// -/// Does not call ImGui.NewFrame / ImGui.Render — those -/// belong to the caller so GL-state ownership is unambiguous. Caller pattern: -/// -/// -/// -/// // per frame, render thread -/// inputBridge.BeginFrame(size, dt); -/// ImGui.NewFrame(); -/// panelHost.RenderAll(ctx); -/// ImGui.Render(); -/// ImGuiImplOpenGL3.RenderDrawData(ImGui.GetDrawData()); -/// -/// -public interface IPanelHost -{ - /// Register a panel for per-frame rendering. Idempotent by . - void Register(IPanel panel); - - /// Remove the panel with the matching id. No-op if not present. - void Unregister(string panelId); - - /// - /// Iterate every visible panel and call . Call - /// order within a frame is the registration order; panels with - /// set to false are skipped entirely. - /// - void RenderAll(PanelContext ctx); -} diff --git a/src/AcDream.UI.Abstractions/IPanelRenderer.cs b/src/AcDream.UI.Abstractions/IPanelRenderer.cs deleted file mode 100644 index 93e25847..00000000 --- a/src/AcDream.UI.Abstractions/IPanelRenderer.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// Drawing primitives exposed to panels. The only API panels use to -/// emit pixels. The ImGui backend maps these straight onto ImGui calls; the -/// later custom retail-look backend will map the same primitives onto its -/// own retained-mode toolkit using retail dat-sourced fonts / sprites. -/// -/// -/// Keep this interface small and retail-friendly. If a widget requires a -/// feature the custom backend couldn't express with dat assets, don't add -/// it — find a different widget shape that both backends can satisfy. -/// -/// -public interface IPanelRenderer -{ - /// - /// Begin a top-level window. Matches retail's root UiPanel + - /// ImGui's Begin. Returns false if the window is collapsed - /// — the caller must still call to balance. - /// - bool Begin(string title); - - /// Close the most recent . - void End(); - - /// Draw a single line of text. No formatting / markdown. - void Text(string text); - - /// Keep the next widget on the same line as the previous one. - void SameLine(); - - /// Horizontal rule separator. - void Separator(); - - /// - /// A filled progress bar. - /// is clamped by the backend to [0, 1]. - /// is the pixel width of the full bar. - /// is optional text (e.g. "54%") rendered on top. - /// - void ProgressBar(float fraction, float width, string? overlay = null); -} diff --git a/src/AcDream.UI.Abstractions/NullCommandBus.cs b/src/AcDream.UI.Abstractions/NullCommandBus.cs deleted file mode 100644 index 2c111ea8..00000000 --- a/src/AcDream.UI.Abstractions/NullCommandBus.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// No-op . Accepts any published command and -/// discards it. Used as the default in D.2a until chat / inventory panels -/// need real command routing. -/// -public sealed class NullCommandBus : ICommandBus -{ - /// Shared singleton — the bus is stateless. - public static readonly NullCommandBus Instance = new(); - - private NullCommandBus() { } - - /// - public void Publish(T command) where T : notnull - { - // Intentionally empty. Panel-emitted commands in D.2a are - // read-only diagnostics; nothing routes server-ward yet. - } -} diff --git a/src/AcDream.UI.Abstractions/PanelContext.cs b/src/AcDream.UI.Abstractions/PanelContext.cs deleted file mode 100644 index 37caba1d..00000000 --- a/src/AcDream.UI.Abstractions/PanelContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace AcDream.UI.Abstractions; - -/// -/// Per-frame context passed to each call. -/// Struct + record for zero-allocation per frame. Add fields here as new -/// capabilities become panel-facing — e.g. a future IGameState -/// handle once we need richer data than individual ViewModels can carry. -/// -/// -/// Carried by value; cheap. Passed per-render; do not cache across frames. -/// -/// -public readonly record struct PanelContext( - float DeltaSeconds, - ICommandBus Commands); diff --git a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs b/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs deleted file mode 100644 index 5040c7ea..00000000 --- a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace AcDream.UI.Abstractions.Panels.Vitals; - -/// -/// First real UI panel — shows the local player's vitals as progress bars. -/// Backend-agnostic; renders exclusively through -/// so the same file works under Hexa.NET.ImGui (D.2a) and the future custom -/// retail-look toolkit (D.2b). -/// -/// -/// D.2a shows only HP (percent). / -/// return null until a -/// LocalPlayerState is wired (follow-up issue). When they start -/// returning non-null, this panel picks them up automatically. -/// -/// -public sealed class VitalsPanel : IPanel -{ - private const float BarWidth = 200f; - - private readonly VitalsVM _vm; - - public VitalsPanel(VitalsVM vm) - { - _vm = vm ?? throw new ArgumentNullException(nameof(vm)); - } - - /// - public string Id => "acdream.vitals"; - - /// - public string Title => "Vitals"; - - /// - public bool IsVisible { get; set; } = true; - - /// - public void Render(PanelContext ctx, IPanelRenderer renderer) - { - if (!renderer.Begin(Title)) - { - renderer.End(); - return; - } - - // HP — always available from CombatState. - float hp = _vm.HealthPercent; - renderer.Text("HP"); - renderer.SameLine(); - renderer.ProgressBar(hp, BarWidth, overlay: $"{hp * 100f:F0}%"); - - // Stamina — show only when the VM has a real value. - if (_vm.StaminaPercent is float stam) - { - renderer.Text("Stam"); - renderer.SameLine(); - renderer.ProgressBar(stam, BarWidth, overlay: $"{stam * 100f:F0}%"); - } - - // Mana — show only when the VM has a real value. - if (_vm.ManaPercent is float mana) - { - renderer.Text("Mana"); - renderer.SameLine(); - renderer.ProgressBar(mana, BarWidth, overlay: $"{mana * 100f:F0}%"); - } - - renderer.End(); - } -} diff --git a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs b/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs deleted file mode 100644 index 6d512f29..00000000 --- a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs +++ /dev/null @@ -1,75 +0,0 @@ -using AcDream.Core.Combat; - -namespace AcDream.UI.Abstractions.Panels.Vitals; - -/// -/// ViewModel for the vitals HUD panel. Reads live health percentage for the -/// local player from (which is fed by the server's -/// UpdateHealth (0x01C0) GameEvent). -/// -/// -/// D.2a scope limits: -/// -/// -/// -/// HP comes from and is percent-only -/// (0..1). Absolute current/max HP is not wired yet. -/// Stamina / Mana are always null — those values live in -/// AppraiseInfoParser.CreatureProfile (parsed from -/// PlayerDescription (0x0013)) but the parsed record is -/// currently discarded. Wiring a LocalPlayerState cache is -/// a separate follow-up; see docs/ISSUES.md. -/// -/// -/// -/// GUID timing: the local player's server GUID isn't known at -/// OnLoad (pre-login). Construct with -/// left as 0; GameWindow calls the setter when the live session -/// receives its guid at EnterWorld. Before the GUID is set, -/// returns 1.0 (via CombatState's safe -/// default for unknown guids) — the bar reads "full", which is harmless. -/// -/// -public sealed class VitalsVM -{ - private readonly CombatState _combat; - private uint _localPlayerGuid; - - /// - /// Build a VitalsVM bound to a instance. The - /// GUID starts at 0; call once the - /// live session assigns it. - /// - public VitalsVM(CombatState combat) - { - _combat = combat ?? throw new ArgumentNullException(nameof(combat)); - _localPlayerGuid = 0; - } - - /// - /// Push the authoritative local-player GUID from WorldSession. - /// One-way setter — only GameWindow should call it, exactly once - /// per live session. - /// - public void SetLocalPlayerGuid(uint guid) => _localPlayerGuid = guid; - - /// - /// Current health percent (0..1) for the local player. Returns 1.0 - /// before login or if the server has never sent an UpdateHealth for - /// this GUID. - /// - public float HealthPercent => _combat.GetHealthPercent(_localPlayerGuid); - - /// - /// Stamina percent (0..1) or null when absolute values aren't wired. - /// D.2a always returns null; to be populated by a future - /// LocalPlayerState that caches PlayerDescription (0x0013). - /// - public float? StaminaPercent => null; - - /// - /// Mana percent (0..1) or null when absolute values aren't wired. - /// Same status as . - /// - public float? ManaPercent => null; -} diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj deleted file mode 100644 index 66f5bbad..00000000 --- a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - net10.0 - enable - enable - latest - true - - - - - - - - - - - - - diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs deleted file mode 100644 index bbcc3a81..00000000 --- a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Silk.NET.Input; -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; - -namespace AcDream.UI.ImGui; - -/// -/// Owns the ImGuiController from Silk.NET.OpenGL.Extensions.ImGui, -/// which handles the whole Silk.NET ↔ ImGui.NET integration: -/// -/// Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding -/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one). -/// Subscribes to Silk.NET's window + input events to drive IO. -/// Per frame: Update(dt) calls ImGui.NewFrame(); Render() -/// calls ImGui.Render() + uploads draw data via its OpenGL3 backend. -/// -/// -/// -/// Instance-scoped rather than static so GL-context lifetime is explicit. -/// GameWindow owns the one instance and disposes on shutdown. -/// -/// -/// -/// History: tried Hexa.NET.ImGui + Hexa.NET.ImGui.Backends.OpenGL3 first -/// per the original plan, but its native OpenGL3 backend resolves GL functions -/// via GLFW / SDL internally and crashed (0xC0000005) in InitNative without -/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25. -/// -/// -public sealed class ImGuiBootstrapper : IDisposable -{ - private readonly ImGuiController _controller; - - public ImGuiBootstrapper(GL gl, IView window, IInputContext input) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(window); - ArgumentNullException.ThrowIfNull(input); - // ImGuiController constructor handles: - // - ImGui.CreateContext() - // - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL) - // - Keyboard + mouse event subscription (bound to Silk.NET IInputContext) - // - Default style = dark - _controller = new ImGuiController(gl, window, input); - } - - /// - /// Begin an ImGui frame. Call BEFORE any ImGui.* widget calls. - /// Internally: consumes buffered input events, calls ImGui.NewFrame(). - /// - public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds); - - /// - /// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all - /// panel draws, within the same frame as . The - /// OpenGL3 backend save/restores the GL state it touches (shader, VAO, - /// texture, blend, scissor); state not in its save-list (e.g. - /// GL_FRAMEBUFFER_SRGB) is caller's responsibility. - /// - public void Render() => _controller.Render(); - - public void Dispose() => _controller.Dispose(); -} diff --git a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs b/src/AcDream.UI.ImGui/ImGuiPanelHost.cs deleted file mode 100644 index d9a3a4c1..00000000 --- a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs +++ /dev/null @@ -1,46 +0,0 @@ -using AcDream.UI.Abstractions; - -namespace AcDream.UI.ImGui; - -/// -/// implementation for the ImGui backend. Owns the -/// registered panel set; iterates + draws every frame when the caller is -/// inside an ImGui frame (between ImGui.NewFrame and -/// ImGui.Render). -/// -/// -/// This class does not call ImGui.NewFrame / ImGui.Render -/// itself. Those belong to the caller (GameWindow) so GL-state -/// ownership is explicit and the render-loop integration point is obvious. -/// -/// -public sealed class ImGuiPanelHost : IPanelHost -{ - private readonly Dictionary _panels = new(); - private readonly ImGuiPanelRenderer _renderer = new(); - - /// - public void Register(IPanel panel) - { - ArgumentNullException.ThrowIfNull(panel); - _panels[panel.Id] = panel; // idempotent by Id - } - - /// - public void Unregister(string panelId) => _panels.Remove(panelId); - - /// - public void RenderAll(PanelContext ctx) - { - // Order-independent — ImGui windows stack in the order they're drawn - // for focus purposes but we have <=1 panel in D.2a. - foreach (var panel in _panels.Values) - { - if (!panel.IsVisible) continue; - panel.Render(ctx, _renderer); - } - } - - /// Current registered count (for diagnostics). - public int Count => _panels.Count; -} diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs deleted file mode 100644 index d1fde2f9..00000000 --- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Numerics; -using AcDream.UI.Abstractions; -using ImGuiNET; - -namespace AcDream.UI.ImGui; - -/// -/// implemented as thin wrappers around -/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear -/// outside of bootstrap plumbing — panels that need a feature must -/// extend the abstraction here, not by importing ImGuiNET in panel -/// files. -/// -public sealed class ImGuiPanelRenderer : IPanelRenderer -{ - /// - public bool Begin(string title) => ImGuiNET.ImGui.Begin(title); - - /// - public void End() => ImGuiNET.ImGui.End(); - - /// - public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text); - - /// - public void SameLine() => ImGuiNET.ImGui.SameLine(); - - /// - public void Separator() => ImGuiNET.ImGui.Separator(); - - /// - public void ProgressBar(float fraction, float width, string? overlay = null) - { - // Clamp defensively; ImGui clamps internally but the abstraction - // contract promises to handle out-of-range values. - if (fraction < 0f) fraction = 0f; - else if (fraction > 1f) fraction = 1f; - - var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font - ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty); - } -} diff --git a/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj b/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj deleted file mode 100644 index f99c6e22..00000000 --- a/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net10.0 - enable - enable - false - - - - - - - - - - - - - - - - - - diff --git a/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs b/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs deleted file mode 100644 index 6f34d5ea..00000000 --- a/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace AcDream.UI.Abstractions.Tests; - -public sealed class NullCommandBusTests -{ - private sealed record FakeCmd(int Value); - - [Fact] - public void Publish_DoesNotThrow_OnAnyRecordType() - { - var bus = NullCommandBus.Instance; - - bus.Publish(new FakeCmd(42)); - bus.Publish("a string command"); - bus.Publish(12345); - } - - [Fact] - public void Instance_IsSingleton() - { - Assert.Same(NullCommandBus.Instance, NullCommandBus.Instance); - } -} diff --git a/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs b/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs deleted file mode 100644 index 2a665c93..00000000 --- a/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace AcDream.UI.Abstractions.Tests; - -public sealed class PanelContextTests -{ - [Fact] - public void Fields_RoundTripThroughConstructor() - { - var ctx = new PanelContext(DeltaSeconds: 0.016f, Commands: NullCommandBus.Instance); - - Assert.Equal(0.016f, ctx.DeltaSeconds); - Assert.Same(NullCommandBus.Instance, ctx.Commands); - } - - [Fact] - public void RecordEquality_ByValue() - { - var a = new PanelContext(1f / 60f, NullCommandBus.Instance); - var b = new PanelContext(1f / 60f, NullCommandBus.Instance); - - // Record-struct equality is value-based on DeltaSeconds + reference-based - // on Commands (since ICommandBus is a reference type, same instance → equal). - Assert.Equal(a, b); - } -} diff --git a/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs deleted file mode 100644 index 3abf2c35..00000000 --- a/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using AcDream.Core.Combat; -using AcDream.UI.Abstractions.Panels.Vitals; - -namespace AcDream.UI.Abstractions.Tests; - -public sealed class VitalsVMTests -{ - [Fact] - public void HealthPercent_ReturnsCombatStateValue_AfterUpdateHealth() - { - var combat = new CombatState(); - uint guid = 0x5000_0042u; - combat.OnUpdateHealth(guid, 0.42f); - - var vm = new VitalsVM(combat); - vm.SetLocalPlayerGuid(guid); - - Assert.Equal(0.42f, vm.HealthPercent, precision: 3); - } - - [Fact] - public void HealthPercent_ReturnsOne_WhenGuidUnknown() - { - var combat = new CombatState(); - var vm = new VitalsVM(combat); - - // No SetLocalPlayerGuid call — defaults to 0 which CombatState has never seen. - Assert.Equal(1f, vm.HealthPercent); - } - - [Fact] - public void HealthPercent_ReturnsOne_WhenGuidSetButNeverUpdated() - { - var combat = new CombatState(); - var vm = new VitalsVM(combat); - vm.SetLocalPlayerGuid(0xDEAD_BEEFu); - - Assert.Equal(1f, vm.HealthPercent); - } - - [Fact] - public void StaminaPercent_IsNull_ForD2aScope() - { - // D.2a explicitly defers Stamina until LocalPlayerState + PlayerDescription - // wiring. When that arrives VitalsVM.StaminaPercent becomes non-null and - // VitalsPanel starts drawing the Stam bar automatically. - var vm = new VitalsVM(new CombatState()); - Assert.Null(vm.StaminaPercent); - } - - [Fact] - public void ManaPercent_IsNull_ForD2aScope() - { - var vm = new VitalsVM(new CombatState()); - Assert.Null(vm.ManaPercent); - } - - [Fact] - public void SetLocalPlayerGuid_ReroutesHealthLookup_WithoutStaleCache() - { - // Simulate the realistic GameWindow flow: VM is constructed pre-login - // with GUID=0, then SetLocalPlayerGuid is called at EnterWorld. - var combat = new CombatState(); - uint playerGuid = 0x5003_E219u; - combat.OnUpdateHealth(playerGuid, 0.75f); - - var vm = new VitalsVM(combat); - // Before SetLocalPlayerGuid — reads GUID=0 → returns safe 1.0. - Assert.Equal(1f, vm.HealthPercent); - - vm.SetLocalPlayerGuid(playerGuid); - Assert.Equal(0.75f, vm.HealthPercent, precision: 3); - } - - [Fact] - public void Constructor_ThrowsOnNullCombat() - { - Assert.Throws(() => new VitalsVM(null!)); - } -}