The chat deep-dive proved the MECHANISM behind retail's green clickable
speaker name — a glyph tag coloured from property 0x1D rather than the line's
own 0x1B — but not the colour itself: BuildChatColorLookupTable @0x004F31C0
builds only the 0x1B array, so the value is authored rather than runtime-built
and the research correctly returned "UNKNOWN" instead of assuming the green in
a screenshot.
--colors prints the 0x1B/0x1D arrays of every element in a layout, which
measures it out of the installed dats:
chat 0x2100006F, transcript 0x10000011
P0x1B [0x00] R=204 G=204 B=204
P0x1D [0x00] R= 0 G=178 B= 0 <- the green
Recorded in the research note, including the trap it exposes: the tag colour is
per-ELEMENT and authored, while the line colour on that same element comes from
the runtime chat table. Filing "tag green" into the LogTextType colour table
would put it in the wrong place entirely.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
6.2 KiB
C#
157 lines
6.2 KiB
C#
// Print the AUTHORED geometry and state set of a retail LayoutDesc element
|
|
// tree, straight from the installed DATs.
|
|
//
|
|
// Layout questions -- "is this scrollbar where retail put it?", "does this
|
|
// button even have a pressed state?" -- were being answered by reading our own
|
|
// importer and inferring. This reads the authored truth instead, which is the
|
|
// only thing either question is actually about.
|
|
//
|
|
// dotnet run --project tools/LayoutDump -- 0x21000071
|
|
// dotnet run --project tools/LayoutDump -- 0x2100002F 0x1000018E --states
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Content;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
using SysEnv = System.Environment;
|
|
|
|
if (args.Length == 0)
|
|
{
|
|
Console.WriteLine("usage: LayoutDump <layoutId> [rootElementId] [--states]");
|
|
return 1;
|
|
}
|
|
|
|
bool showStates = args.Contains("--states");
|
|
bool showColors = args.Contains("--colors");
|
|
uint[] ids = args.Where(a => !a.StartsWith("--"))
|
|
.Select(a => Convert.ToUInt32(a, a.StartsWith("0x") ? 16 : 10))
|
|
.ToArray();
|
|
|
|
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
|
|
ElementInfo? root = ids.Length > 1
|
|
? LayoutImporter.ImportInfos(adapter, ids[0], ids[1])
|
|
: LayoutImporter.ImportInfos(adapter, ids[0]);
|
|
if (root is null)
|
|
{
|
|
Console.WriteLine($"layout 0x{ids[0]:X8} not found (or root 0x{(ids.Length > 1 ? ids[1] : 0):X8} missing)");
|
|
return 2;
|
|
}
|
|
|
|
Console.WriteLine($"layout 0x{ids[0]:X8}");
|
|
Print(root, 0);
|
|
|
|
int resizeAt = Array.IndexOf(args, "--resize");
|
|
if (resizeAt >= 0 && resizeAt + 2 < args.Length)
|
|
{
|
|
// Reproduce a window resize exactly, without a running client: retail's
|
|
// raw-edge policy (UIElement::UpdateForParentSizeChange @ 0x00462640) is a
|
|
// pure function of the authored rects and the new parent size, which is
|
|
// what UiElement.ApplyAnchor feeds it every frame.
|
|
int rw = int.Parse(args[resizeAt + 1]);
|
|
int rh = int.Parse(args[resizeAt + 2]);
|
|
Console.WriteLine();
|
|
Console.WriteLine($"resized to {rw}x{rh}:");
|
|
PrintResized(root, UiPixelRect.FromPositionAndSize(0, 0, rw, rh), 0);
|
|
}
|
|
|
|
if (args.Contains("--built"))
|
|
{
|
|
// What the importer actually PRODUCES, next to what the dat authored.
|
|
// A difference between the two is the whole question for any "this
|
|
// control is in the wrong place" report.
|
|
Console.WriteLine();
|
|
Console.WriteLine("built widget tree:");
|
|
ImportedLayout built = LayoutImporter.Build(root, _ => (0u, 0, 0), null, _ => null);
|
|
PrintBuilt(built.Root, 0);
|
|
}
|
|
return 0;
|
|
|
|
void PrintResized(ElementInfo e, UiPixelRect parentRect, int depth)
|
|
{
|
|
string pad = new(' ', depth * 2);
|
|
Console.WriteLine(
|
|
$"{pad}0x{e.Id:X8} type={e.Type,-10} "
|
|
+ $"x={parentRect.X0,6} y={parentRect.Y0,6} "
|
|
+ $"w={parentRect.Width,6} h={parentRect.Height,6}");
|
|
|
|
foreach (ElementInfo child in e.Children)
|
|
{
|
|
var authored = UiPixelRect.FromPositionAndSize(
|
|
(int)child.X, (int)child.Y, (int)child.Width, (int)child.Height);
|
|
var originalParent = child.HasOriginalParentSize
|
|
? UiPixelRect.FromPositionAndSize(
|
|
0, 0, (int)child.OriginalParentWidth, (int)child.OriginalParentHeight)
|
|
: UiPixelRect.FromPositionAndSize(0, 0, parentRect.Width, parentRect.Height);
|
|
|
|
UiPixelRect next = UiLayoutPolicy.Apply(
|
|
child.Left, child.Top, child.Right, child.Bottom,
|
|
authored, originalParent, authored,
|
|
UiPixelRect.FromPositionAndSize(0, 0, parentRect.Width, parentRect.Height));
|
|
|
|
PrintResized(child, next, depth + 1);
|
|
}
|
|
}
|
|
|
|
void PrintBuilt(UiElement e, int depth)
|
|
{
|
|
string pad = new(' ', depth * 2);
|
|
Console.WriteLine(
|
|
$"{pad}{e.GetType().Name,-20} id=0x{e.EventId:X8} "
|
|
+ $"L={e.Left,6:0.#} T={e.Top,6:0.#} W={e.Width,6:0.#} H={e.Height,6:0.#} "
|
|
+ $"vis={e.Visible}");
|
|
foreach (UiElement child in e.Children)
|
|
PrintBuilt(child, depth + 1);
|
|
}
|
|
|
|
void Print(ElementInfo e, int depth)
|
|
{
|
|
string pad = new(' ', depth * 2);
|
|
Console.WriteLine(
|
|
$"{pad}0x{e.Id:X8} type={e.Type,-10} "
|
|
+ $"x={e.X,6:0.#} y={e.Y,6:0.#} w={e.Width,6:0.#} h={e.Height,6:0.#} "
|
|
+ $"edges=L{e.Left}/T{e.Top}/R{e.Right}/B{e.Bottom} "
|
|
+ $"parent={(e.HasOriginalParentSize ? $"{e.OriginalParentWidth:0.#}x{e.OriginalParentHeight:0.#}" : "-")} "
|
|
+ $"z={e.ZLevel} order={e.ReadOrder}");
|
|
|
|
// Colour-array properties. 0x1B is the ordinary font-colour array and 0x1D
|
|
// the TAG font-colour array (UIElement_Text::SetFontColorHelper); both are
|
|
// indexed by the caller, so the tagged-name colour is a row in 0x1D rather
|
|
// than anything the runtime builds.
|
|
if (showColors)
|
|
{
|
|
foreach (UiStateInfo state in e.States.Values)
|
|
{
|
|
foreach (uint prop in new[] { 0x1Bu, 0x1Du })
|
|
{
|
|
if (!state.Properties.TryGetValue(prop, out UiPropertyValue? v) || v is null)
|
|
continue;
|
|
Console.WriteLine($"{pad} P0x{prop:X2} ({v.ArrayValue.Count} entries):");
|
|
for (int i = 0; i < v.ArrayValue.Count; i++)
|
|
{
|
|
UiColorValue c = v.ArrayValue[i].ColorValue;
|
|
Console.WriteLine(
|
|
$"{pad} [0x{i:X2}] R={c.Red,3} G={c.Green,3} B={c.Blue,3} A={c.Alpha,3}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (showStates && e.States.Count != 0)
|
|
{
|
|
string names = string.Join(", ", e.States
|
|
.OrderBy(kv => kv.Key)
|
|
.Select(kv => $"{kv.Key}{(kv.Value.Name.Length != 0 ? $":{kv.Value.Name}" : "")}"
|
|
+ $"[pass={kv.Value.PassToChildren}"
|
|
+ $" img={(kv.Value.Image is { } m ? $"0x{m.File:X8}" : "-")}"
|
|
+ $" media={kv.Value.MediaCount}/{kv.Value.ImageMediaCount}]"));
|
|
Console.WriteLine($"{pad} states(default={e.DefaultStateId}): {names}");
|
|
}
|
|
|
|
foreach (ElementInfo child in e.Children)
|
|
Print(child, depth + 1);
|
|
}
|