feat(D.2b): cut GameWindow over to the data-driven chat window
Replace the hand-authored chat block (UiNineSlicePanel + inline UiChatView + local BuildRetailChatLines/RetailChatColor statics) with ChatWindowController.Bind(LayoutDesc 0x21000006) — the same LayoutImporter path as the vitals window. The controller places UiChatView (transcript) + UiChatInput (text entry, on-submit) + UiChatScrollbar + UiChannelMenu inside the dat-authored chrome. The dead local statics are deleted. Wired to _commandBus (same LiveCommandBus as the ImGui ChatPanel) so type+Enter dispatches SendChatCmd server-ward. Transcript keyboard set from _uiHost.Keyboard (set by WireKeyboard above the chat block) for Ctrl+C/Ctrl+A. Divergence register: added AD-28 (two-widget split vs UIElement_Text), AP-38 (no in-element word-wrap), AP-39 (per-line colour vs per-glyph runs), AP-40 (no opacity fade / shared vitalsDatFont), TS-30 (tab buttons no-op), TS-31 (no squelch); updated IA-15 to cover both vitals + chat importer paths. Build: 0 errors/warnings. Tests: 392 passed, 1 skipped (expected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9d9e036e4c
commit
12ab9663d2
3 changed files with 48 additions and 52 deletions
|
|
@ -55,11 +55,11 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md |
|
||||
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
|
||||
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
|
||||
| IA-15 | D.2b retail UI is our own UiHost/UiElement retained-mode tree drawing dat-sprite window frames, not a byte-port of keystone.dll's LayoutDesc binary tree. The vitals window is now rendered by the LayoutDesc importer (dat chrome elements read directly from `LayoutDesc 0x2100006C`), not `UiNineSlicePanel`; `UiNineSlicePanel`/`RetailChromeSprites` now back only the chat window + plugin panels | `src/AcDream.App/UI/Layout/LayoutImporter.cs` (vitals) + `src/AcDream.App/UI/UiNineSlicePanel.cs` (chat/plugins) | keystone.dll has no PDB/decomp so a byte-port is impossible by definition; we mirror retail's ElementDesc field model + controls.ini tokens, and the chrome sprites ARE the real dat RenderSurfaces (Step-0 prove-out 2026-06-14 confirmed 0x06004CC2 center + 0x060074BF..C6 bevel). The 8-piece edge/corner→position mapping is DATA-DRIVEN from the dat: the `LayoutImporter` reads `LayoutDesc 0x2100006C` and resolves chrome element positions + sprite ids directly from parsed dat fields; locked by the conformance fixture `tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json` | Remaining residual risk: anchor resolution at non-800×600 and the controls.ini cascade still lack an oracle — layout scaling at non-reference resolution and stylesheet token inheritance differ silently | `LayoutDesc 0x2100006C` (SHIPPED); `docs/research/2026-06-15-layoutdesc-format.md`; controls.ini tokens; keystone.dll layout eval (no PDB) |
|
||||
| IA-15 | D.2b retail UI is our own UiHost/UiElement retained-mode tree drawing dat-sprite window frames, not a byte-port of keystone.dll's LayoutDesc binary tree. Both the vitals window (`LayoutDesc 0x2100006C`) and the chat window (`LayoutDesc 0x21000006`) are rendered by the LayoutDesc importer; `UiNineSlicePanel`/`RetailChromeSprites` now back only plugin panels | `src/AcDream.App/UI/Layout/LayoutImporter.cs` (vitals + chat) + `src/AcDream.App/UI/Layout/ChatWindowController.cs` | keystone.dll has no PDB/decomp so a byte-port is impossible by definition; we mirror retail's ElementDesc field model + controls.ini tokens, and the chrome sprites ARE the real dat RenderSurfaces (Step-0 prove-out 2026-06-14 confirmed 0x06004CC2 center + 0x060074BF..C6 bevel). The 8-piece edge/corner→position mapping is DATA-DRIVEN from the dat: the `LayoutImporter` reads `LayoutDesc 0x2100006C`/`0x21000006` and resolves chrome element positions + sprite ids directly from parsed dat fields; vitals locked by the conformance fixture `tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json` | Remaining residual risk: anchor resolution at non-800×600 and the controls.ini cascade still lack an oracle — layout scaling at non-reference resolution and stylesheet token inheritance differ silently | `LayoutDesc 0x2100006C`/`0x21000006` (SHIPPED); `docs/research/2026-06-15-layoutdesc-format.md`; controls.ini tokens; keystone.dll layout eval (no PDB) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Adaptation (AD) — 27 rows
|
||||
## 2. Adaptation (AD) — 28 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -90,10 +90,11 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-25 | Wall-bounce velocity reflection suppressed on landing (fires only airborne-before AND airborne-after); retail bounces unless grounded→grounded-and-not-sledding | `src/AcDream.App/Input/PlayerMovementController.cs:1212` | Our per-frame architecture amplifies the artifact (post-reflection +Z defeats the `Velocity.Z <= 0` landing-snap gate → micro-bounce death spiral); at elasticity 0.05 retail's landing bounce is imperceptible; sledding reverts to retail rule | Landing-reflection-dependent behavior (slope-landing momentum, high-elasticity surfaces) won't reproduce; the suppression masks the landing-snap gate fragility and could outlive its reason | `handle_all_collisions` pc:282699-282715; ACE PhysicsObj.cs:2656-2721 |
|
||||
| AD-26 | Auto-walk arrival requires facing alignment (invented 5° arrive / 30° walk-while-turning bands); retail's check is `dist <= radius` exact | `src/AcDream.App/Input/PlayerMovementController.cs:575` | ACE does the final `Rotate(target)` server-side before the Use callback; without a local gate the body used items while facing away (user feedback 2026-05-15). Thresholds are NOT retail constants | Arrival delayed by the rotation phase; if heading convergence fights another yaw writer, `AutoWalkArrived` never fires and the queued Use/PickUp never completes | `MoveToManager::HandleMoveToPosition`; `apply_interpreted_movement` |
|
||||
| AD-27 | Use/PickUp action re-sent on natural auto-walk arrival; retail sends the action once (server MoveToChain callback completes it) | `src/AcDream.App/Input/PlayerMovementController.cs:322` | ACE's server-side chain may have timed out by the time our body arrives; the close-range re-send hits ACE's WithinUseRadius fast-path | If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius |
|
||||
| AD-28 | Chat transcript (`UiChatView`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 37 rows
|
||||
## 3. Documented approximation (AP) — 40 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -134,10 +135,13 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp) | `src/AcDream.App/Rendering/GameWindow.cs:6895` (per-frame predicate) + `:IsSealedDungeonCell` + `:OnLiveEntitySpawnedLocked`/`:OnLivePositionUpdated` (login/teleport pre-collapse hooks) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277 |
|
||||
| AP-35 | Point/spot lights use a single PER-PIXEL accumulation that ports `calc_point_light`'s `(1 − dist/falloff_eff)` LINEAR ramp (falloff_eff = Falloff × static_light_factor 1.3) + standard Lambert `N·L`; retail's path is PER-VERTEX Gouraud and additionally applies a half-Lambert wrap (`0.5·dist + N·L_vec`, lights surfaces down to `N·L ≥ −0.5`) and an x87-obscured normalization factor, neither ported | `src/AcDream.App/Rendering/Shaders/mesh_modern.frag:52` (+ `mesh.frag`; `LightInfoLoader.cs:81` folds 1.3 into Range) | The linear ramp is the user-visible fix (kills the hard-disc "spotlight" edge, #133 A7); the dropped wrap/normalization only re-shade the gradient slightly, and per-pixel vs per-vertex Gouraud chiefly differs on coarse geometry. Half-Lambert wrap + factor are an x87-decompile refinement (same artifact class as GetPowerBarLevel AP-24) | Surfaces facing slightly away from a torch (`−0.5 ≤ N·L < 0`) stay dark where retail's wrap lights them faintly; near-light gradient shading differs subtly from retail's per-vertex bake | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); static_light_factor 0x00820e24 |
|
||||
| AP-37 | LayoutDesc importer collapses the dat's nested meter structure (Type-7 meter → two Type-3 container children → three Type-3 image-slice grandchildren each) into `UiMeter`'s programmatic 3-slice fields (`BackLeft..FrontRight`) + reuses `UiMeter.DrawHBar`'s scissor-fill, instead of building those child nodes generically and porting `UIElement_Meter::DrawChildren`. Standalone Type-0 text elements are also skipped (vitals numbers render via `UiMeter.Label` bound by the controller; a dedicated dat-text widget is Plan 2). The inheritance `Merge` treats Width/Height==0 as "inherit from base", diverging from format-doc §12 rule 2 (documented inline in `ElementReader.cs`) | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` (`BuildMeter`/`SliceIds`) + `src/AcDream.App/UI/Layout/LayoutImporter.cs` (`BuildWidget` meter-child skip) | Reuses the tested `UiMeter` render that already visually matches retail's stacked vitals bars; the full nested-element + `DrawChildren` scissor port and a dat-text widget are deferred to Plan 2. Now the default vitals path (the hand-authored markup vitals was retired) and locked by the conformance fixture (`tests/AcDream.App.Tests/UI/Layout/fixtures/vitals_2100006C.json`) | A LayoutDesc whose meter structure differs from the vitals 2-container/3-slice shape, or a window needing standalone dat text, renders an empty/wrong meter or drops text — no oracle diff until the Plan-2 widgets land | `UIElement_Meter::DrawChildren` @0x46fbd0; `UIElement_Text::DrawSelf` @0x467aa0; `docs/research/2026-06-15-layoutdesc-format.md` |
|
||||
| AP-38 | Chat transcript renders pre-split `ChatLog` lines 1:1; no in-element word-wrap at the panel's current pixel width | `src/AcDream.App/UI/UiChatView.cs` | Retail does in-element wrap via `UIElement_Text::SizeToFit`; our pre-split lines are always shorter than 440 px in practice; a line that overflows clips at the edge rather than wrapping | Very long server system messages (server shutdowns, broadcast announcements) clip rather than wrapping — no information loss, just visual truncation | `UIElement_Text::SizeToFit` @0x467980; `gmMainChatUI` layout |
|
||||
| AP-39 | Chat lines carry one color per `ChatKind` (per-line solid color); retail `UIElement_Text` supports per-glyph styled runs (bold, different hue per segment) | `src/AcDream.App/UI/UiChatView.cs:13` | Retail glyph-run parsing lives inside keystone.dll with no PDB/decomp; per-line per-kind coloring is the correct tonal palette and covers all existing chat types | Chat lines retail renders with multiple colors or bold names (e.g. "PlayerName says: text") render as one flat color; subtle visual difference but functionally complete | `UIElement_Text` glyph-run styling (keystone.dll, no decomp) |
|
||||
| AP-40 | Single default translucency for the chat window chrome; no focused/unfocused opacity transition; dat font face/size taken from the vitals `vitalsDatFont` (same dat font, not a chat-specific size lookup) | `src/AcDream.App/Rendering/GameWindow.cs` (chatController binding line) | Retail fades the chat window to ~80% alpha when unfocused (`gmMainChatUI::UpdateAlpha @0x4cdea0`); the opacity animation deferred to the Plan-2 window-manager input integration; sharing `vitalsDatFont` is safe — retail uses the same AC-default font for both | The chat window is always fully opaque/same-font rather than subtly fading when idle; no wrong text, but the focused/unfocused breathing rhythm is absent | `gmMainChatUI::UpdateAlpha` @0x4cdea0; `UCF::SetAceFont @0x4d3940` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Temporary stopgap (TS) — 29 rows
|
||||
## 4. Temporary stopgap (TS) — 31 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -170,6 +174,8 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
|
||||
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
|
||||
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:331` | Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
|
||||
| TS-30 | Numbered chat tabs (element ids `0x10000522`–`0x10000525`) render as clickable buttons but do not switch channel filter or affect the transcript — tab state is a no-op | `src/AcDream.App/UI/Layout/ChatWindowController.cs:210` | Retail's tab switching routes transcript lines by chat channel (`gmMainChatUI::gmScrollWindow` sub-windows per tab); the tab wiring is D.5 scope | Tab clicks produce no visible transcript change; retail would filter to the selected channel — all chat always shows in all tabs | `gmMainChatUI::PostInit` tab setup @0x4ce2a0; holtburger chat tab handling |
|
||||
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -426,6 +426,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **✓ 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.
|
||||
- **✓ SHIPPED — D.2b — Custom retail-look backend (Spec 1: markup engine + first panel).** Shipped 2026-06-14 (`626d06e`→`019350f`). Wired the dormant `UiHost`/`UiElement` tree into `GameWindow` (`ACDREAM_RETAIL_UI=1`) and built the **Approach-C markup engine**: `MarkupDocument` (XML → `UiElement` subtree, `{Binding}` reflection) + `ControlsIni` stylesheet loader + an `IUiRegistry` plugin UI surface (plugins ship markup + a binding, drained into the same `UiRoot`). First retail-faithful panel: a markup-driven Vitals window (`vitals.xml`) — 8-piece dat-sprite frame (`UiNineSlicePanel`) + three `UiMeter` bars (red/gold/blue + cur/max numbers) bound to `VitalsVM`, rendering live over the 3D world and coexisting with the ImGui devtools path. Retired divergence TS-30, added IA-15. Spec `docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md`; plan `docs/superpowers/plans/2026-06-14-d2b-retail-panel-frame-plan.md`. NOTE the prove-out corrected two stale "facts": retail vitals are **bars not orbs**, stamina is **gold not cyan**. **Remaining: D.3 (AcFont — using the stb stopgap for now) + the glassy gradient bar sprite / brightness tune (polish); input integration (movable/clickable windows — the `UiRoot` drag/click machinery exists but isn't bridged to the Phase-K dispatcher yet); and the rest of the panels (D.5).**
|
||||
- **✓ SHIPPED — D.2b LayoutDesc importer (Plan 1 + default flip).** Plan 1 shipped 2026-06-15. *Retroactive registration — the spec requested this phase but it was not pre-registered before implementation.* Data-driven vitals window from `LayoutDesc 0x2100006C`: `LayoutImporter` resolves `BaseElement`/`BaseLayoutId` inheritance, walks the `ElementDesc` tree, and builds a live `UiElement` tree via `DatWidgetFactory` (Type 7 → `UiMeter`; all others → `UiDatElement` generic renderer). `VitalsController` binds live HP/Stamina/Mana by element id (mirrors retail `gmVitalsUI`). A/B visual gate **PASSED**: pixel-identical to the hand-authored window. **Default flip shipped 2026-06-15 (`bf77a23`):** the importer is now the default vitals window at `ACDREAM_RETAIL_UI=1`; the hand-authored `vitals.xml` and the `ACDREAM_RETAIL_UI_IMPORTER` flag were retired (`vitals.xml` recoverable from git history). The window is movable (Anchors=None + Draggable) AND horizontally resizable (Resizable/ResizeX, `8aa643f`): the dat edge-anchors reflow the pieces on width change per retail `UIElement::UpdateForParentSizeChange @0x00462640` (an earlier "fixed-size" note was wrong — inverted edge-flag reading, corrected; stretch is `RightEdge==1`). Faithful grip/dragbar-*driven* drag/resize INPUT is Plan 2. Post-flip number-render fixes (`43064ba`, `34243f2`): submission-order sprite draw (stamina/mana numbers had been overpainted by their own bars) + glyph pixel-snap (sharp at all resize widths). `MarkupDocument`/`UiNineSlicePanel` remain for the chat window + plugin panels. Spec: `docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md`; plan: `docs/superpowers/plans/2026-06-15-layoutdesc-importer.md`.
|
||||
- **✓ SHIPPED — D.2b LayoutDesc importer (Plan 2 — chat-window re-drive).** Shipped 2026-06-15. `GameWindow`'s hand-authored chat block (`UiNineSlicePanel` + inline `UiChatView`) replaced by `ChatWindowController.Bind(LayoutDesc 0x21000006, …)` — the same importer path as vitals. `ChatWindowController` places `UiChatView` (transcript) + `UiChatInput` (text entry + on-submit) + `UiChatScrollbar` (scrollbar thumb) + `UiChannelMenu` (channel selector) inside the dat-authored chrome; dead local statics `BuildRetailChatLines`/`RetailChatColor` deleted from `GameWindow` (moved into the controller). Wired to `_commandBus` (same `LiveCommandBus` as the ImGui `ChatPanel`) so type+Enter dispatches `SendChatCmd` server-ward. Transcript keyboard set from `_uiHost.Keyboard` for Ctrl+C/Ctrl+A. 392 tests green. Added divergence rows AD-28 / AP-38–40 / TS-30–31; updated IA-15.
|
||||
- **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.)**
|
||||
- **✓ SHIPPED — D.4 — Dat sprites + 9-slice panel backgrounds.** Shipped 2026-06-14 with D.2b. `TextureCache.GetOrUploadRenderSurface(id, out w, out h)` decodes `RenderSurface` (`0x06xxxxxx`) **directly** (not via the Surface→SurfaceTexture chain — the prove-out finding) → GL `Texture2D`; `DrawSprite` (explicit UV-rect, per-texture batch) added to `TextRenderer` + `UiRenderContext` + a `uUseTexture=2` RGBA frag branch; `UiNineSlicePanel` composes the universal 8-piece bevel (corners `0x060074C3..C6`, edges `0x060074BF/C0/C1/C2`, center `0x06004CC2`). Remaining art polish: the glassy gradient bar fill sprite (D.2b polish).
|
||||
- **D.5 — Core panels.** Attributes (`chunk_00470000.c:FUN_0047ba70`), Skills (same), Paperdoll (`chunk_004A0000.c:FUN_004A5200`), Inventory, Spellbook (`chunk_004C0000.c`), Fellowship, Allegiance. Each uses the port sketches in slice 05. **(Targets `AcDream.UI.Abstractions` — ships with D.2a using ImGui-rendered widgets; reskinned by D.2b.)** The *chat* panel originally listed under D.5 shipped early in Phase I (I.4 input + I.7 combat translator superseded the chat-panel design here); this entry now covers Attributes / Skills / Paperdoll / Inventory / Spellbook / Fellowship / Allegiance only.
|
||||
|
|
|
|||
|
|
@ -1833,58 +1833,47 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable.");
|
||||
}
|
||||
|
||||
// Retail chat window — a draggable/resizable nine-slice frame hosting a
|
||||
// scrollable transcript (UiChatView). Read-only + wheel-scroll for now;
|
||||
// drag-select + Ctrl+C copy land in the next D.2b sub-step. A dedicated
|
||||
// ChatVM with a deeper tail (200) feeds the scrollback; it shares the
|
||||
// same live ChatLog (Chat) as the ImGui panel.
|
||||
// Retail chat window — data-driven from LayoutDesc 0x21000006 (gmMainChatUI),
|
||||
// the same importer path as vitals. ChatWindowController binds the transcript,
|
||||
// input, scrollbar and channel menu and routes through ChatVM + ChatCommandRouter.
|
||||
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||
var chatWindow = new AcDream.App.UI.UiNineSlicePanel(ResolveChrome)
|
||||
AcDream.App.UI.Layout.ElementInfo? chatRootInfo;
|
||||
AcDream.App.UI.Layout.ImportedLayout? chatLayout;
|
||||
lock (_datLock)
|
||||
{
|
||||
Left = 10, Top = 432, Width = 440, Height = 184,
|
||||
MinWidth = 180, MinHeight = 80,
|
||||
};
|
||||
var chatView = new AcDream.App.UI.UiChatView
|
||||
{
|
||||
Left = 8, Top = 8, Width = 424, Height = 168,
|
||||
Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top
|
||||
| AcDream.App.UI.AnchorEdges.Right | AcDream.App.UI.AnchorEdges.Bottom,
|
||||
Font = _debugFont,
|
||||
LinesProvider = () => BuildRetailChatLines(retailChatVm),
|
||||
// Drag-select + Ctrl+C copy need the keyboard for clipboard +
|
||||
// modifier state. UiHost.Keyboard is set during WireKeyboard above.
|
||||
Keyboard = _uiHost.Keyboard,
|
||||
};
|
||||
chatWindow.AddChild(chatView);
|
||||
_uiHost.Root.AddChild(chatWindow);
|
||||
|
||||
// Map the VM's formatted tail into coloured view lines. Per-ChatKind
|
||||
// palette (retail-ish): speech white, tells magenta, channels blue,
|
||||
// system yellow, emotes grey, combat orange. Refined later if needed.
|
||||
static System.Collections.Generic.IReadOnlyList<AcDream.App.UI.UiChatView.Line> BuildRetailChatLines(
|
||||
AcDream.UI.Abstractions.Panels.Chat.ChatVM vm)
|
||||
{
|
||||
var detailed = vm.RecentLinesDetailed();
|
||||
var result = new AcDream.App.UI.UiChatView.Line[detailed.Count];
|
||||
for (int i = 0; i < detailed.Count; i++)
|
||||
result[i] = new AcDream.App.UI.UiChatView.Line(
|
||||
detailed[i].Text, RetailChatColor(detailed[i].Kind));
|
||||
return result;
|
||||
chatRootInfo = AcDream.App.UI.Layout.LayoutImporter.ImportInfos(
|
||||
_dats!, AcDream.App.UI.Layout.ChatWindowController.LayoutId);
|
||||
chatLayout = chatRootInfo is null ? null
|
||||
: AcDream.App.UI.Layout.LayoutImporter.Build(chatRootInfo, ResolveChrome, vitalsDatFont);
|
||||
}
|
||||
|
||||
static System.Numerics.Vector4 RetailChatColor(AcDream.Core.Chat.ChatKind kind) => kind switch
|
||||
if (chatRootInfo is not null && chatLayout is not null)
|
||||
{
|
||||
AcDream.Core.Chat.ChatKind.LocalSpeech => new(1f, 1f, 1f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.RangedSpeech => new(1f, 0.95f, 0.8f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.Channel => new(0.6f, 0.8f, 1f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.Tell => new(1f, 0.5f, 1f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.System => new(1f, 1f, 0.45f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.Popup => new(1f, 0.85f, 0.4f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.Emote => new(0.8f, 0.8f, 0.7f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.SoulEmote => new(0.8f, 0.8f, 0.7f, 1f),
|
||||
AcDream.Core.Chat.ChatKind.Combat => new(1f, 0.6f, 0.25f, 1f),
|
||||
_ => new(0.9f, 0.9f, 0.9f, 1f),
|
||||
};
|
||||
var chatController = AcDream.App.UI.Layout.ChatWindowController.Bind(
|
||||
chatRootInfo, chatLayout, retailChatVm, _commandBus ?? (AcDream.UI.Abstractions.ICommandBus)AcDream.UI.Abstractions.NullCommandBus.Instance,
|
||||
vitalsDatFont, _debugFont, ResolveChrome);
|
||||
if (chatController is not null)
|
||||
{
|
||||
// Ctrl+C / Ctrl+A on the transcript need the keyboard for clipboard + modifiers.
|
||||
// _uiHost.Keyboard is set by WireKeyboard above — it is non-null here.
|
||||
chatController.Transcript.Keyboard = _uiHost.Keyboard;
|
||||
// Top-level retail window: user-positioned at the bottom-left, movable + resizable.
|
||||
// KEEP the dat-authored size (do NOT override Width/Height) so the child anchors
|
||||
// capture their dat margins on the first layout — the same reason the vitals root
|
||||
// keeps its dat size. The user resizes/moves from there.
|
||||
var chatRoot = chatController.Root;
|
||||
chatRoot.Left = 10;
|
||||
chatRoot.Top = 460; // bottom-left default; pending the user's visual review
|
||||
chatRoot.Anchors = AcDream.App.UI.AnchorEdges.None;
|
||||
chatRoot.Draggable = true;
|
||||
chatRoot.Resizable = true;
|
||||
chatRoot.MinWidth = 200f;
|
||||
chatRoot.MinHeight = 80f;
|
||||
_uiHost.Root.AddChild(chatRoot);
|
||||
Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006).");
|
||||
}
|
||||
else Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006.");
|
||||
}
|
||||
else Console.WriteLine("[D.2b] chat: LayoutDesc 0x21000006 not found.");
|
||||
|
||||
// Drain plugin-registered markup panels (buffered before the GL
|
||||
// window opened) into the same UiRoot tree. A faulty plugin markup
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue