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.
}