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>
@
101 lines
3.7 KiB
C#
101 lines
3.7 KiB
C#
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}",
|
||
};
|
||
}
|