using System.Numerics; using AcDream.App.UI; namespace AcDream.App.Studio; // ───────────────────────────────────────────────────────────────────────────── // DumpLayout — load a panel from the retail UI layout dump // // The dump stores every node's rect in ABSOLUTE screen coordinates (the // panel's design position in the retail UI, not relative to its parent). // Evidence: for the "inventory" panel, the root node is at x=500,y=138 and // its direct children are also at x=500,y=161 — the child y=161 is only // 23 pixels below the parent y=138, which makes sense as a child offset // (the header row), not as the raw rect. If the rects were parent-relative, // (500,161) would place the child way off the window. // // DumpLayout converts absolute → parent-relative by computing: // child.Left = child.Rect.X - parent.Rect.X // child.Top = child.Rect.Y - parent.Rect.Y // // The root node (ParentTraversalIndex == null) is placed at (0,0) so the // whole tree sits at the UiHost origin rather than at the panel's retail // screen position. // ───────────────────────────────────────────────────────────────────────────── /// /// Builds a static tree from the retail UI layout dump /// JSON. The tree is a hierarchy of (draws its /// sprite) or plain containers (Group nodes), with each /// node's set to the dump's element_id and /// set to the widget_kind string. /// /// This source is STATIC — no controllers, no FixtureProvider, no live /// game data. It is a build reference for the UI Studio showing any of the 26 /// retail windows without needing the production panel wired up. /// public static class DumpLayout { /// /// Parse the dump at , find the panel whose slug /// matches , and build a tree. /// /// /// maps a RenderSurface id (0x06xxxxxx) to a /// (GL texture handle, native width, native height) triple — pass /// RenderStack.ResolveChrome from the studio, or a stub returning /// (1,1,1) for tests. /// /// /// Returns null and sets on failure. /// public static UiElement? Load( string dumpPath, string slug, Func resolve, out string? error) { // ── 1. Parse the dump JSON ──────────────────────────────────────── var dump = UiDumpModel.Parse(dumpPath); if (dump is null) { error = $"[dump] Failed to parse '{dumpPath}'."; return null; } // ── 2. Find the requested panel ─────────────────────────────────── var panel = dump.Panels.FirstOrDefault( p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase)); if (panel is null) { error = $"[dump] Panel slug '{slug}' not found. " + $"Available: {string.Join(", ", dump.Panels.Select(p => p.Slug))}"; return null; } if (panel.Nodes.Count == 0) { error = $"[dump] Panel '{slug}' has no nodes."; return null; } // ── 3. Build a traversal-index → node lookup ────────────────────── var byIndex = new Dictionary(panel.Nodes.Count); foreach (var n in panel.Nodes) byIndex[n.TraversalIndex] = n; // ── 4. Create UiElement objects for every node ──────────────────── var elements = new Dictionary(panel.Nodes.Count); foreach (var node in panel.Nodes) { var el = BuildElement(node, resolve); elements[node.TraversalIndex] = el; } // ── 5. Wire parent–child relationships + set parent-relative coords ─ UiElement? root = null; foreach (var node in panel.Nodes) { var el = elements[node.TraversalIndex]; if (node.ParentTraversalIndex is null) { // Root node — place at (0,0) so the tree sits at the UiHost origin. // The panel's absolute rect offset is discarded here (it was the // retail design position inside the retail screen, which we don't need). el.Left = 0f; el.Top = 0f; root = el; } else { // Non-root: convert absolute → parent-relative by subtracting parent rect. // child.Left = child.Rect.X - parent.Rect.X // child.Top = child.Rect.Y - parent.Rect.Y // This preserves the visual layout inside each group without placing the // entire panel at its retail screen origin. var parentNode = byIndex[node.ParentTraversalIndex.Value]; el.Left = node.Rect.X - parentNode.Rect.X; el.Top = node.Rect.Y - parentNode.Rect.Y; var parentEl = elements[node.ParentTraversalIndex.Value]; parentEl.AddChild(el); } } if (root is null) { error = $"[dump] Panel '{slug}': no root node found (all nodes have a parent)."; return null; } // Give the root the full panel dimensions (from the dump's width/height record). root.Width = panel.Width; root.Height = panel.Height; error = null; return root; } // ── Private helpers ─────────────────────────────────────────────────────── private static UiElement BuildElement( DumpNode node, Func resolve) { uint imageId = UiDumpModel.PickImageId(node); var kind = node.WidgetKind ?? "Group"; UiElement el; if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase)) { // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element. el = new DumpSpriteElement(imageId, resolve) { Name = kind, ClickThrough = true, // static mockup; no behavior Anchors = AnchorEdges.None, }; } else { // Group (or sprite without an image) — plain container, no own draw. el = new DumpGroupElement() { Name = kind, ClickThrough = true, Anchors = AnchorEdges.None, }; } // EventId is set from the dump's element_id (cast to uint — the decimal // values in the JSON represent the same dat handle used at runtime). el.EventId = (uint)node.ElementId; el.Left = node.Rect.X; // overwritten by caller per root/child logic el.Top = node.Rect.Y; el.Width = node.Rect.Width; el.Height = node.Rect.Height; return el; } } // ───────────────────────────────────────────────────────────────────────────── // DumpSpriteElement — minimal element that draws a single sprite // ───────────────────────────────────────────────────────────────────────────── /// /// Draws a single sprite at its native size tiled to fill /// × . Used for Sprite/Button/Scrollbar/Slider nodes from /// the retail UI dump. /// /// We do NOT reuse here because /// that class requires an ElementInfo with a populated StateMedia /// dictionary — the dat-import plumbing — which is not needed for a static dump /// preview. A minimal subclass keeps the code simpler and the dependency surface /// smaller. /// internal sealed class DumpSpriteElement : UiElement { private readonly uint _imageId; private readonly Func _resolve; public DumpSpriteElement(uint imageId, Func resolve) { _imageId = imageId; _resolve = resolve; } protected override void OnDraw(UiRenderContext ctx) { if (_imageId == 0) return; var (tex, tw, th) = _resolve(_imageId); if (tex == 0 || tw == 0 || th == 0) return; // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both // axes via GL_REPEAT, Width/tw and Height/th tile the texture). ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); } } // ───────────────────────────────────────────────────────────────────────────── // DumpGroupElement — pure container (Group nodes from the dump) // ───────────────────────────────────────────────────────────────────────────── /// /// Container element for dump Group nodes — no own draw, just hosts children. /// Extending UiElement directly (no OnDraw override) gives transparent groups, /// which matches Group nodes in the retail layout that have no background sprite. /// internal sealed class DumpGroupElement : UiElement { // No OnDraw — completely transparent container. }