// 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 [rootElementId] [--states]"); return 1; } bool showStates = args.Contains("--states"); 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}"); 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); }