using System.Text.Json;
using System.Text.Json.Serialization;
namespace AcDream.App.Studio;
// ─────────────────────────────────────────────────────────────────────────────
// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json
//
// Schema (v1):
// { "version":1, "panels":[ { "id":int, "slug":string, "title":string,
// "bucket":string, "parent_slug":string|null,
// "width":int, "height":int,
// "nodes":[ { "traversal_index":int, "element_id":int,
// "layout_id":int, "parent_layout_id":int|null,
// "parent_traversal_index":int|null, "base_layout_id":int,
// "rect":{x,y,width,height},
// "widget_kind":"Group"|"Sprite"|"Button"|"Scrollbar"|"Slider",
// "state_set":{ "default_image":{image_id,alpha_image_id}|null,
// "states":[{state_id,image:{...}}] }
// } ] } ] }
//
// All ids in the dump are DECIMAL ints (e.g. element_id=268435925 = 0x100001D5,
// image_id=100693194 = 0x060074CA). Cast to uint before use in dat/GL APIs.
//
// Rect coordinates are ABSOLUTE (screen-space origin = panel's design position
// in retail layout, NOT relative to the parent). DumpLayout.Load converts them
// to parent-relative when building the UiElement tree.
// ─────────────────────────────────────────────────────────────────────────────
/// Top-level container for the retail UI layout dump.
public sealed class UiDump
{
[JsonPropertyName("version")]
public int Version { get; set; }
[JsonPropertyName("panels")]
public List Panels { get; set; } = new();
}
/// One panel (window) exported from the retail UI.
public sealed class DumpPanel
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("slug")]
public string Slug { get; set; } = "";
[JsonPropertyName("title")]
public string Title { get; set; } = "";
[JsonPropertyName("bucket")]
public string Bucket { get; set; } = "";
[JsonPropertyName("parent_slug")]
public string? ParentSlug { get; set; }
[JsonPropertyName("width")]
public float Width { get; set; }
[JsonPropertyName("height")]
public float Height { get; set; }
[JsonPropertyName("nodes")]
public List Nodes { get; set; } = new();
}
/// One element node within a panel's traversal list.
public sealed class DumpNode
{
[JsonPropertyName("traversal_index")]
public int TraversalIndex { get; set; }
[JsonPropertyName("element_id")]
public long ElementId { get; set; }
[JsonPropertyName("layout_id")]
public long LayoutId { get; set; }
[JsonPropertyName("parent_layout_id")]
public long? ParentLayoutId { get; set; }
[JsonPropertyName("parent_traversal_index")]
public int? ParentTraversalIndex { get; set; }
[JsonPropertyName("base_layout_id")]
public long BaseLayoutId { get; set; }
[JsonPropertyName("rect")]
public DumpRect Rect { get; set; } = new();
[JsonPropertyName("widget_kind")]
public string WidgetKind { get; set; } = "Group";
[JsonPropertyName("state_set")]
public DumpStateSet StateSet { get; set; } = new();
}
/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement).
public sealed class DumpRect
{
[JsonPropertyName("x")]
public float X { get; set; }
[JsonPropertyName("y")]
public float Y { get; set; }
[JsonPropertyName("width")]
public float Width { get; set; }
[JsonPropertyName("height")]
public float Height { get; set; }
}
/// State set for a node — default image plus per-state overrides.
public sealed class DumpStateSet
{
[JsonPropertyName("default_image")]
public DumpImage? DefaultImage { get; set; }
[JsonPropertyName("states")]
public List States { get; set; } = new();
}
/// Image reference (RenderSurface dat id + optional separate alpha surface).
public sealed class DumpImage
{
[JsonPropertyName("image_id")]
public long ImageId { get; set; }
[JsonPropertyName("alpha_image_id")]
public long? AlphaImageId { get; set; }
}
/// A named state override.
public sealed class DumpState
{
[JsonPropertyName("state_id")]
public int StateId { get; set; }
[JsonPropertyName("image")]
public DumpImage Image { get; set; } = new();
}
// ─────────────────────────────────────────────────────────────────────────────
// Helper statics
// ─────────────────────────────────────────────────────────────────────────────
///
/// Parsing helpers for the retail UI dump JSON.
///
public static class UiDumpModel
{
private static readonly JsonSerializerOptions _opts = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};
/// Parse the full dump from a file path. Returns null on failure.
public static UiDump? Parse(string path)
{
try
{
using var stream = File.OpenRead(path);
return JsonSerializer.Deserialize(stream, _opts);
}
catch
{
return null;
}
}
///
/// Return the list of slugs in the dump (for smoke-testing every panel).
/// Returns an empty list if the file cannot be parsed.
///
public static IReadOnlyList ListSlugs(string path)
{
var dump = Parse(path);
if (dump is null) return Array.Empty();
return dump.Panels.Select(p => p.Slug).ToList();
}
///
/// Pick the sprite id to use for a node: prefer default_image.image_id;
/// fall back to states[0].image.image_id; return 0 if neither exists.
///
public static uint PickImageId(DumpNode node)
{
if (node.StateSet.DefaultImage is { ImageId: > 0 } di)
return (uint)di.ImageId;
if (node.StateSet.States.Count > 0 && node.StateSet.States[0].Image.ImageId > 0)
return (uint)node.StateSet.States[0].Image.ImageId;
return 0u;
}
}