@
docs(D.2b): chat-window re-drive design spec + list-ui-layouts research tool
Plan-2 chat piece of the LayoutDesc importer. Identifies the chat window as
LayoutDesc 0x21000006 (gmMainChatUI, element class 0x10000041) and grounds a
faithful, data-driven re-drive in the named retail decomp (ChatInterface +
gmMainChatUI + UIElement_Text/_Scrollable/_Scrollbar/_Menu) plus a user-provided
retail screenshot.
Design (full-faithful scope, user-approved):
- transcript = UIElement_Text 0x10000011 (dat font, bottom-pinned, 10k behead cap,
pixel scroll, 1 line/wheel-notch)
- scrollbar = right-side track 0x10000012 + thumb 0x1000048c + up/down
- input = editable UIElement_Text 0x10000016 (caret, 100-entry history, Enter/Send)
- channel menu = UIElement_Menu 0x10000014 ("Chat" selector -> active channel)
- shared ChatCommandRouter extracted from ChatPanel
- screenshot correction: the four 0x10000522-525 left-edge elements are the
numbered CHAT TABS (1-4), not scroll buttons (a research-agent inference the
retail screenshot refutes)
- deferred (need non-UI plumbing, each gets a divergence row): tab switching/
filtering, squelch, clickable name-tags, in-element word-wrap, styled runs,
font config, opacity transition
Tooling: AcDream.Cli `list-ui-layouts <datdir> [0xRootType]` — read-only index of
every UI LayoutDesc by root element class + size + element-Type histogram; how the
chat layout was located (root type 0x10000041). Reusable for future panel re-drives.
Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
parent
50758d4795
commit
26cb34f126
3 changed files with 380 additions and 0 deletions
267
docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md
Normal file
267
docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
# D.2b — Chat-window re-drive (LayoutDesc importer, Plan 2 chat piece) — design
|
||||
|
||||
**Date:** 2026-06-15
|
||||
**Branch:** `claude/hopeful-maxwell-214a12` (D.2b retail-UI track; lighting/M1.5 is a separate branch off main)
|
||||
**Status:** design — approved scope, pending spec review
|
||||
**Predecessor:** the LayoutDesc importer + the vitals re-drive
|
||||
(`docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md`,
|
||||
`docs/research/2026-06-15-layoutdesc-format.md`,
|
||||
`claude-memory/project_d2b_retail_ui.md`).
|
||||
**Handoff that opened this work:** `docs/research/2026-06-15-chat-window-redrive-handoff.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Replace the hand-authored retail chat window (a `UiNineSlicePanel` hosting a
|
||||
`UiChatView` at a guessed rect, built inline in `GameWindow.cs` under
|
||||
`if (_options.RetailUi)`) with the **data-driven retail chat window** read from
|
||||
the dat `LayoutDesc 0x21000006` (`gmMainChatUI`) via the existing `LayoutImporter`,
|
||||
with **faithful behavioral widgets ported from the named retail decomp** and the
|
||||
**dat font** — the same way the vitals window became data-driven.
|
||||
|
||||
**The code is modern. The behavior is retail.** Every widget algorithm is ported
|
||||
from `docs/research/named-retail/acclient_2013_pseudo_c.txt` with a cited
|
||||
`class::method @address`.
|
||||
|
||||
## 2. Approved scope
|
||||
|
||||
**In scope (faithful core):**
|
||||
- Data-driven window frame from `0x21000006` (bg sprites, resize bar, grip chrome,
|
||||
translucency).
|
||||
- Transcript: dat-font `UIElement_Text` port — scrollable, bottom-pinned,
|
||||
per-line chat-kind color, 10k-glyph behead cap.
|
||||
- Scrollbar: right-side track + thumb + up/down buttons, **pixel-based** scroll,
|
||||
`thumbRatio = view/content`, wheel = **1 line per notch**.
|
||||
- Input: editable one-line field — caret, insert/delete, 100-entry command
|
||||
history (up/down arrow), focus sprite, Enter→submit.
|
||||
- Channel menu (`Chat ▸`): dropdown of channels; selection sets the active
|
||||
outbound channel (the `ChatInputParser` default channel).
|
||||
- Send button + max/min button.
|
||||
- `ChatCommandRouter`: the shared submit pipeline, extracted from `ChatPanel`
|
||||
so the ImGui devtools chat and the retail chat share one routing path.
|
||||
|
||||
**Deferred (each gets a `retail-divergence-register.md` row — these need *non-UI*
|
||||
plumbing acdream lacks, they are NOT UI scope cuts):**
|
||||
- **Numbered chat tabs (1–4) — switching + per-tab chat-type filtering.** The tab
|
||||
*sprites* render (they come free from the importer), but clicking a tab to filter
|
||||
which chat kinds show needs the per-tab `m_llTextTypeFilter` /
|
||||
`m_chatNewNonVisibleTextIndicator` system.
|
||||
- **Squelch toggle** (menu item 0) — needs a squelch subsystem.
|
||||
- **Clickable name-tags** (`StartTell` on click) — needs `StringInfo`/`TextTag`
|
||||
styled runs in `ChatLog`.
|
||||
- **In-element word-wrap at panel width** — the transcript renders pre-split
|
||||
`ChatLog` lines 1:1; faithful `GlyphList::Recalculate` wrap reworks the
|
||||
selection/hit-test model (visual-row ≠ record). Highest-risk UI piece; deferred.
|
||||
- **Per-glyph mixed-color runs / configurable font face+size** (`gmClient::sm_nFontFace`).
|
||||
- **Active/inactive opacity switch** — a single default translucency is in scope;
|
||||
the focused-brighter / unfocused-dimmer transition is deferred.
|
||||
|
||||
## 3. Retail reference (the port target)
|
||||
|
||||
`gmMainChatUI` (registered element class `0x10000041`, built from `LayoutDesc
|
||||
0x21000006`) extends a base **`ChatInterface`**. `ChatInterface` owns the
|
||||
transcript, input, inbound routing, submit, history, truncate and opacity;
|
||||
`gmMainChatUI` adds the channel menu, squelch, max/min, tab visibility and
|
||||
clickable name-tags.
|
||||
|
||||
### 3.1 Element → role map (`0x21000006`)
|
||||
|
||||
| Element | Type | Role | Decomp anchor |
|
||||
|---|---|---|---|
|
||||
| `0x1000000E` | `0x10000041` | window root (`gmMainChatUI`), bg `0x0600114D`, 800×100 authored | `gmMainChatUI::Register @0x4cd350` |
|
||||
| `0x1000000F` | 9 Resizebar | top resize bar, img `0x06001125`, cursor `0x06005E66` | — |
|
||||
| `0x1000046F` | 0 | max/min button (`Maximized 0x06005E64` / `Minimized 0x06005E65`) | `gmMainChatUI::HandleMaximizeButton @0x4cce50` |
|
||||
| `0x10000010` | 3 Field | transcript panel bg `0x06001115` | — |
|
||||
| **`0x10000011`** | 0 (UIElement_Text) | **transcript** — read-only, multiline, scrollable | `ChatInterface::PostInit @0x4f3e47` |
|
||||
| `0x1000048c` | 0 | scroll **thumb** (child of transcript) | `ChatInterface::PostInit @0x4f3e79` |
|
||||
| `0x10000012` | 0 | scrollbar **track** (right edge, 16×68) | `UIElement_Scrollable::GetScrollbarPointer_ @0x473ec0` |
|
||||
| `0x10000013` | 3 Field | input bar bg `0x0600113A` | — |
|
||||
| `0x10000014` | 6 Menu | **channel menu** (`Chat ▸`) — 14 items (squelch + 13 channels) | `gmMainChatUI::InitTalkFocusMenu @0x4cdc50`, `HandleSelection @0x4cd540` |
|
||||
| **`0x10000016`** | 0 (UIElement_Text) | **input** — editable, one-line, focus sprite `0x060011AB` | `ChatInterface::PostInit @0x4f3e86` |
|
||||
| `0x10000017/18` | 3 | input focus edges (sprite `0x06004D67`) | — |
|
||||
| `0x10000019` | 0 | **Send** button (`0x06001915`/pressed `…16`/ghost `…34`) | `ChatInterface::ListenToElementMessage @0x4f51ea` |
|
||||
| `0x10000522–525` | 0 | **numbered chat tabs 1–4** (left strip; Normal `0x06006218`/Hi `0x06006219`) | `gmMainChatUI::RecvNotice_SetPanelVisibility @0x4ccd80` |
|
||||
|
||||
> **Screenshot correction (user-provided retail ground truth, 2026-06-15):** the
|
||||
> four `0x10000522–525` elements are the **left-edge numbered chat tabs**, NOT the
|
||||
> "line/page scroll buttons" a research agent inferred from their 16×16 vertical
|
||||
> geometry. The scrollbar (track + thumb + up/down) is on the **right**. The exact
|
||||
> dat ids of the right-side scroll up/down buttons are located during Task D
|
||||
> (likely children of track `0x10000012` not surfaced in the top-level dump).
|
||||
|
||||
> **BN field-name caveat:** the decomp's `m_chatEntry` / `m_chatLog` /
|
||||
> `m_fCurrentOpacity` names are applied inconsistently across functions (a
|
||||
> Binary-Ninja artifact). The roles above are fixed by the decisive evidence —
|
||||
> the `Normal_focussed` sprite is on `0x10000016` (only an editable field gets a
|
||||
> focus state) and the multiline geometry is `0x10000011` — corroborated by both
|
||||
> surviving research agents. Port by **role**, not by the C++ member name.
|
||||
|
||||
### 3.2 Key retail algorithms (cited)
|
||||
|
||||
**Inbound** — `ChatInterface::RecvNotice_DisplayFinalStringInfo @0x4f4640`:
|
||||
append `arg4` (prefix/timestamp) then `arg3` (body) to the transcript via
|
||||
`UIElement_Text::AppendStringInfoWithFont` with per-chat-type color `arg2` (color
|
||||
table built by `BuildChatColorLookupTable`). If glyph count > `0x2710` (10000),
|
||||
`TruncateChatLog @0x4f4290` beheads from the top at a newline boundary. **Bottom-pin:**
|
||||
capture `IsAtVerticalEnd` *before* appending; if it was true, `ScrollToPosition`
|
||||
to the new end; else light the unread-text indicator.
|
||||
|
||||
**Submit** — `ChatInterface::HandleEnterKey @0x4f52d0` (fired by the *Accept*
|
||||
input-action, not raw `\r` — one-line mode drops the `\r` char) → `ProcessCommand
|
||||
@0x4f5100`: read input text, dispatch, push to `m_InputHistory` (cap 100, drop
|
||||
index 0 when full), reset history cursor to `0xFFFFFFFF`, clear input. The Send
|
||||
button (`0x10000019`) is an alternate trigger via `ListenToElementMessage`.
|
||||
|
||||
**Scroll** — `UIElement_Scrollable`: pixel offset `m_iScrollableY`, clamped to
|
||||
`[0, contentHeight − viewHeight]` (`SetScrollableXY @0x4740c0`). `thumbRatio =
|
||||
view/content` clamped to 1, bar hidden when content ≤ view
|
||||
(`UpdateScrollbarSize_ @0x4741a0`). `posRatio = scrollY/(content−view)`
|
||||
(`UpdateScrollbarPosition_ @0x473f20`). Scroll quantum = one line-height
|
||||
(`UIElement_Text::InqScrollDelta @0x4689b0`); page = view height; **wheel = 1 line
|
||||
per notch** (`HandleMouseWheel @0x471450`).
|
||||
|
||||
**Input** — caret `m_nCursorPos` (glyph index); `GlyphList::FindPixelsFromPos
|
||||
@0x472b40` = Σ glyph advances to the caret; midpoint-snap hit-test
|
||||
`FindPosFromLineAndPixels @0x4732d0`; ~1 Hz blink. Glyph advance =
|
||||
`HorizontalOffsetBefore + Width + HorizontalOffsetAfter` (signed bytes,
|
||||
`Font::GetCharWidthA @0x4433f0`) — **already implemented** by
|
||||
`UiDatFont.GlyphAdvance`. History: `SelectCommandFromHistory` (up=back, down=fwd),
|
||||
sentinel `0xFFFFFFFF` = "not browsing".
|
||||
|
||||
**Channel menu** — `InitTalkFocusMenu @0x4cdc50` fills `UIElement_Menu 0x10000014`
|
||||
with 14 items: item 0 = squelch toggle, items 1–13 = channels carrying attr
|
||||
`0x1000000B` = channel enum (1=Say, 2=Tell/Target, 3=Emote, 4=Fellowship,
|
||||
5=Patron, 6=Trade, 7=Allegiance, 8–0xD=area/custom). `HandleSelection @0x4cd540`
|
||||
reads the enum, calls `SetTalkFocus(enum)`, updates the label, marks the item
|
||||
selected.
|
||||
|
||||
## 4. Architecture (acdream)
|
||||
|
||||
Faithful structure: an importer builds the generic frame; a **controller**
|
||||
(`ChatInterface`+`gmMainChatUI`::PostInit analogue) binds behavior by element id
|
||||
and swaps the transcript/input placeholders for behavioral widgets. New classes
|
||||
live in `src/AcDream.App/UI/` (widgets, GL-side) and `src/AcDream.UI.Abstractions/`
|
||||
(the shared submit router).
|
||||
|
||||
| Component | Kind | Retail analogue | Responsibility |
|
||||
|---|---|---|---|
|
||||
| `ChatWindowController` | new (`App/UI/Layout/`) | `ChatInterface` + `gmMainChatUI` PostInit | import `0x21000006`; `FindElement(id)`; swap transcript/input widgets; wire scrollbar/menu/send/max-min; route in/outbound |
|
||||
| `UiChatView` | **extend** (`App/UI/`) | `UIElement_Text` (transcript) | + `UiDatFont? DatFont`; dat-font measure/advance/draw; **1-line** wheel quantum; keep bottom-pin + drag-select + Ctrl+C |
|
||||
| `UiChatInput` | new (`App/UI/`) | `UIElement_Text` (editable) | caret, insert/delete, 100-entry history, focus sprite swap, dat font, `Action<string>? OnSubmit` |
|
||||
| `UiScrollable` | new (`App/UI/`) | `UIElement_Scrollable` | pixel scroll math: `ScrollY`, `ContentHeight`, `ViewHeight`, `ClampScroll`, `ThumbRatio`, `ThumbOffsetRatio`, line/page delta |
|
||||
| `UiChatScrollbar` | new (`App/UI/`) | composed scrollbar | own the imported track/thumb/up-down sprites; size+place thumb from `UiScrollable`; clicks/drag → `UiScrollable` |
|
||||
| `UiChannelMenu` | new (`App/UI/`) | `UIElement_Menu` | dropdown popup of channels; selection → active `ChatChannelKind`; label reflects selection |
|
||||
| `ChatCommandRouter` | new (`UI.Abstractions/Panels/Chat/`) | `ProcessCommand` | shared submit: client-command intercept → unknown-verb guard → `ChatInputParser.Parse(text, channel, lastTell, lastOutgoing)` → `Publish(SendChatCmd)` |
|
||||
| `UiDatFont` | no change | `Font` | already implements retail glyph advance |
|
||||
|
||||
**Why two widget classes (`UiChatView` + `UiChatInput`) when retail uses one
|
||||
`UIElement_Text` with mode bits:** acdream's retained-mode widget layer predates
|
||||
D.2b; the behavioral contract (read-only multiline scroll vs editable one-line) is
|
||||
identical, only the class split differs. Accepted **ADAPTATION** divergence; both
|
||||
classes share the `UiDatFont.GlyphAdvance` measure seam so geometry is consistent.
|
||||
|
||||
**Placeholder swap:** the transcript (`0x10000011`) and input (`0x10000016`)
|
||||
render no background sprite of their own (bg comes from parent panels
|
||||
`0x10000010` / `0x10000013`), so `ChatWindowController` reads each placeholder's
|
||||
rect + anchors, instantiates `UiChatView` / `UiChatInput` there, adds it to the
|
||||
placeholder's parent, and removes the placeholder. Mirrors `GetChildRecursive(id)`
|
||||
binding in `ChatInterface::PostInit`.
|
||||
|
||||
## 5. Data flow
|
||||
|
||||
- **Inbound:** `ChatLog → ChatVM.RecentLinesDetailed()` (200-deep tail) →
|
||||
`UiChatView.LinesProvider` (per-`ChatKind` color via `RetailChatColor`). Pipeline
|
||||
unchanged. Bottom-pin + 10k cap are `UiChatView`/`UiScrollable` behavior.
|
||||
- **Outbound:** `UiChatInput.OnSubmit(text)` →
|
||||
`ChatCommandRouter.Submit(text, vm, commandBus, activeChannel)` → `SendChatCmd`
|
||||
→ `LiveCommandBus` → `WorldSession`. `activeChannel` comes from `UiChannelMenu`.
|
||||
- **Channel:** `UiChannelMenu` selection → `ChatWindowController._activeChannel`
|
||||
(→ `ChatInputParser` default channel) + menu label update.
|
||||
- **Scroll:** transcript content height → `UiScrollable` → `UiChatScrollbar` thumb;
|
||||
wheel/buttons/drag → `UiScrollable.ScrollY` → transcript draw offset.
|
||||
|
||||
## 6. Faithfulness decisions / divergence-register rows
|
||||
|
||||
Add on landing (category in parens):
|
||||
1. **(Adaptation)** Transcript + input are two classes (`UiChatView`/`UiChatInput`)
|
||||
not one mode-flagged `UIElement_Text`. Behavior identical.
|
||||
2. **(Approximation)** Transcript renders pre-split `ChatLog` lines 1:1; no
|
||||
in-element word-wrap at panel width. Symptom: long lines not re-wrapped on
|
||||
horizontal resize. `file:line` = `UiChatView.cs`.
|
||||
3. **(Approximation)** One color per display line, not per-glyph styled runs.
|
||||
4. **(Stopgap)** Numbered chat tabs render but don't switch / filter chat kinds.
|
||||
5. **(Stopgap)** Squelch toggle + clickable name-tags render/parse-absent.
|
||||
6. **(Approximation)** Single default translucency; no focused/unfocused opacity
|
||||
transition; default dat font face+size (no `sm_nFontFace` config).
|
||||
|
||||
Retire nothing (no existing register row is fixed by this work).
|
||||
|
||||
## 7. Build sequence (tasks for the plan)
|
||||
|
||||
Pipelineable where independent; `ChatWindowController` (G) and the `GameWindow`
|
||||
cutover (H) are the integration barrier.
|
||||
|
||||
- **A. `ChatCommandRouter`** — extract the submit flow from `ChatPanel` into a
|
||||
pure `UI.Abstractions` helper; `ChatPanel` calls it; tests for client-command /
|
||||
unknown-verb / parse / publish parity. *(UI.Abstractions; no GL.)*
|
||||
- **B. `UiChatView` dat-font seam** — add `UiDatFont? DatFont`; prefer it in draw +
|
||||
`HitChar` advance + selection measure + `LineHeight`; change `WheelLines` 3→1;
|
||||
keep `BitmapFont` fallback. Tests: advance/hit-test with a synthetic dat font.
|
||||
- **C. `UiScrollable`** — port `UIElement_Scrollable` math (pixel clamp, thumb
|
||||
ratio/offset, line/page delta). Pure, fully unit-tested (no GL).
|
||||
- **D. `UiChatScrollbar`** — own imported track/thumb/up-down sprites; size+place
|
||||
thumb from `UiScrollable`; wheel/button/drag → scroll. Locate the right-side
|
||||
up/down button ids in the dat here.
|
||||
- **E. `UiChatInput`** — editable one-line widget: caret (`MeasurePrefix` =
|
||||
`UiDatFont.MeasureWidth(text[..caret])`), insert/delete, Home/End/arrows,
|
||||
100-entry history with `−1`=live sentinel, focus sprite swap, `OnSubmit`. Tests
|
||||
for caret math + history.
|
||||
- **F. `UiChannelMenu`** — channel dropdown (port `UIElement_Menu` minimally);
|
||||
13 channels → `ChatChannelKind`; selection event + label.
|
||||
- **G. `ChatWindowController`** — `LayoutImporter.Import(0x21000006)`; bind by id;
|
||||
swap transcript/input; wire scrollbar/menu/send/max-min; route inbound (ChatVM)
|
||||
+ outbound (`ChatCommandRouter`); translucency.
|
||||
- **H. `GameWindow` cutover** — replace the hand-authored
|
||||
`UiNineSlicePanel`+`UiChatView` block with `ChatWindowController`; default
|
||||
bottom-left position + resizable; remove dead code; add divergence rows;
|
||||
`dotnet build` + `dotnet test` green.
|
||||
|
||||
## 8. Testing strategy
|
||||
|
||||
- **Pure/unit (no GL, no dats):** `ChatCommandRouter` parity; `UiScrollable`
|
||||
clamp/thumb/delta golden values from the decomp; `UiChatInput` caret index ↔
|
||||
pixel + history navigation; `UiChatView` dat-font advance/hit-test via the
|
||||
`Func<char,FontCharDesc?>` seam.
|
||||
- **Layout/import (dat-free fixture):** extend the importer fixture pattern with a
|
||||
`chat_21000006.json` tree (via `ImportInfos`) asserting the element→role map and
|
||||
rects.
|
||||
- **Real-dat smoke:** `LayoutImporter.Import(0x21000006)` against the live dat
|
||||
resolves the root + all bound ids before wiring (guarded, like the vitals smoke).
|
||||
- **Visual acceptance (user):** launch live `ACDREAM_RETAIL_UI=1`; compare to the
|
||||
retail screenshot — transcript scrolls, input types + sends, channel menu
|
||||
switches, Send works, scrollbar drags, window moves/resizes, translucency.
|
||||
|
||||
## 9. Acceptance criteria
|
||||
|
||||
- [ ] Chat window is built from `LayoutDesc 0x21000006` via `LayoutImporter` — no
|
||||
hand-authored chat rect remains in `GameWindow.cs`.
|
||||
- [ ] Transcript renders inbound chat in the **dat font**, per-`ChatKind` color,
|
||||
bottom-pinned, 10k-cap, mouse-wheel = 1 line/notch, drag-select + Ctrl+C kept.
|
||||
- [ ] Right-side scrollbar: thumb sizes to content, drag + up/down scroll the
|
||||
transcript.
|
||||
- [ ] Input: type, caret moves, backspace/delete, up/down history, **Enter and the
|
||||
Send button both submit** through `ChatCommandRouter` → wire.
|
||||
- [ ] `Chat ▸` menu opens, lists channels, selection changes the outbound channel
|
||||
+ updates the label.
|
||||
- [ ] Max/min toggles window height; window moves + resizes; translucent frame.
|
||||
- [ ] Every ported widget cites a `class::method @address`; every deferral has a
|
||||
divergence-register row.
|
||||
- [ ] `dotnet build` + `dotnet test` green; user visual sign-off.
|
||||
|
||||
## 10. Deferred / follow-ups (filed, not built)
|
||||
|
||||
In-element word-wrap (+ selection rework); numbered-tab switching + per-tab chat
|
||||
filtering; squelch; clickable name-tags; per-glyph styled runs; configurable font
|
||||
face/size; active/inactive opacity transition; the unidentified top-level Type-5
|
||||
ListBox `0x1000001D` (not bound by `ChatInterface`; likely a floaty/options element).
|
||||
101
src/AcDream.Cli/LayoutIndexDump.cs
Normal file
101
src/AcDream.Cli/LayoutIndexDump.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System.Reflection;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Cli;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only research diagnostic: index EVERY UI <see cref="LayoutDesc"/> in the
|
||||
/// dat by its root element's <c>Type</c> + size + an element-Type histogram, so a
|
||||
/// panel re-drive can locate its layout from the decomp-registered class id
|
||||
/// (e.g. <c>gmMainChatUI</c> registers type <c>0x10000041</c> → the chat window
|
||||
/// is the layout whose root element has Type 0x10000041). Optionally filter to a
|
||||
/// single root Type. No writes; purely a console dump used during brainstorming.
|
||||
/// </summary>
|
||||
public static class LayoutIndexDump
|
||||
{
|
||||
public static int Run(string datDir, string? rootTypeText)
|
||||
{
|
||||
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
|
||||
uint? filter = null;
|
||||
if (!string.IsNullOrWhiteSpace(rootTypeText))
|
||||
{
|
||||
var t = rootTypeText.Trim();
|
||||
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t[2..];
|
||||
if (uint.TryParse(t, System.Globalization.NumberStyles.HexNumber, null, out var f)) filter = f;
|
||||
}
|
||||
|
||||
Console.WriteLine(filter is { } ff
|
||||
? $"=== LayoutDescs with a root element of Type 0x{ff:X8} ==="
|
||||
: "=== All LayoutDescs (id : root element Type : size : #elements : type histogram) ===");
|
||||
|
||||
int total = 0, shown = 0;
|
||||
foreach (var id in dats.GetAllIdsOfType<LayoutDesc>().OrderBy(x => x))
|
||||
{
|
||||
var l = dats.Get<LayoutDesc>(id);
|
||||
if (l is null) continue;
|
||||
total++;
|
||||
|
||||
// The root is the single top-level element (or, if several, the largest).
|
||||
ElementDesc? root = null;
|
||||
foreach (var kv in l.Elements)
|
||||
if (root is null || Area(kv.Value) > Area(root)) root = kv.Value;
|
||||
if (root is null) continue;
|
||||
|
||||
if (filter is { } want && root.Type != want) continue;
|
||||
shown++;
|
||||
|
||||
var hist = new SortedDictionary<uint, int>();
|
||||
int count = 0;
|
||||
CountTypes(root, hist, ref count);
|
||||
string h = string.Join(" ", hist.Select(kv => $"{TypeName(kv.Key)}×{kv.Value}"));
|
||||
Console.WriteLine(
|
||||
$" 0x{id:X8} root=0x{root.ElementId:X8} type=0x{root.Type:X8}({TypeName(root.Type)}) " +
|
||||
$"{root.Width}x{root.Height} n={count} [{h}]");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"shown {shown} / {total} LayoutDescs.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static long Area(ElementDesc e) => (long)e.Width * e.Height;
|
||||
|
||||
private static void CountTypes(ElementDesc e, SortedDictionary<uint, int> hist, ref int count)
|
||||
{
|
||||
count++;
|
||||
hist[e.Type] = hist.TryGetValue(e.Type, out var c) ? c + 1 : 1;
|
||||
foreach (var kv in e.Children)
|
||||
CountTypes(kv.Value, hist, ref count);
|
||||
}
|
||||
|
||||
private static string TypeName(uint t) => t switch
|
||||
{
|
||||
0 => "Text0",
|
||||
1 => "Button",
|
||||
2 => "Dragbar",
|
||||
3 => "Field",
|
||||
5 => "ListBox",
|
||||
6 => "Menu",
|
||||
7 => "Meter",
|
||||
8 => "Panel",
|
||||
9 => "Resizebar",
|
||||
0xB => "Scrollbar",
|
||||
0xC => "Text",
|
||||
0xD => "Viewport",
|
||||
0xE => "Browser",
|
||||
0x10 => "ColorPicker",
|
||||
0x11 => "GroupBox",
|
||||
0x12 => "Proto",
|
||||
0x10000041 => "gmMainChatUI",
|
||||
0x10000040 => "gmFloatyChatUI",
|
||||
0x10000050 => "gmFloatyMainChatUI",
|
||||
0x10000042 => "gmChatOptionsUI",
|
||||
0x10000009 => "gmVitalsUI",
|
||||
_ => $"0x{t:X}",
|
||||
};
|
||||
}
|
||||
|
|
@ -31,6 +31,18 @@ if (args.Length >= 1 && args[0] == "dump-vitals-layout")
|
|||
return VitalsLayoutDump.Run(dvlDatDir, dvlLayout);
|
||||
}
|
||||
|
||||
if (args.Length >= 1 && args[0] == "list-ui-layouts")
|
||||
{
|
||||
string? luiDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
string? luiRootType = args.ElementAtOrDefault(2);
|
||||
if (string.IsNullOrWhiteSpace(luiDatDir))
|
||||
{
|
||||
Console.Error.WriteLine("usage: AcDream.Cli list-ui-layouts <dat-directory> [0xRootType]");
|
||||
return 2;
|
||||
}
|
||||
return LayoutIndexDump.Run(luiDatDir, luiRootType);
|
||||
}
|
||||
|
||||
if (args.Length >= 1 && args[0] == "render-vitals-mockup")
|
||||
{
|
||||
string? rvmDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue