acdream/src/AcDream.Cli/VitalsLayoutDump.cs
Erik ff29787f12 fix(D.2b): vitals from the real stacked-window LayoutDesc (0x2100006C)
The vitals bars were rendered from the WRONG layout. The ids in vitals.xml
(0x0600113x) belong to LayoutDesc 0x21000014 -- the 800x28 floaty side-vitals
ROW. The stacked vitals window the user sees is LayoutDesc 0x2100006C
(160x58), which uses a different sprite set and geometry. Dumped the real
tree (new dump-vitals-layout CLI, reflective) and ported it:

- Sprites (#2): the stacked-window set 0x0600747E-0x0600748F (health/stamina/
  mana, each back+front 3-slice; caps 10px, mid 130px).
- Right cap (#1) + fill model: retail UIElement_Meter::DrawChildren draws the
  back 3-slice full then the front 3-slice CLIPPED to the fill fraction (its
  own right-cap shows at 100%, the back's shows through when partial). UiMeter
  now clips the front per-slice (UV-crop) instead of growing a capless slice.
- Spacing (#5): three flush 150x16 bars at y=5/21/37 in a 160x58 window
  (16px pitch, zero gap), per the dat rects -- not the old 20px-apart guess.
- Border (#3): the window is the 8-piece chrome frame (corners 0x060074C3-C6,
  edges 0x060074BF-C2, 5px) -- dat-confirmed identical to RetailChromeSprites.

The headless render-vitals-mockup now composites this exact window
(0x2100006C) from the real sprites with the same clipped-fill model, so the
look was verified before launch. Font (#4, dat Font 0x40000000) is the next
commit.

Decomp refs: gmVitalsUI::PostInit @0x4bfce0; UIElement_Meter::DrawChildren
@0x46fbd0 (scissor-fill); geometry from LayoutDesc 0x2100006C.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:50:17 +02:00

152 lines
5.8 KiB
C#

using System.Collections;
using System.Reflection;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
namespace AcDream.Cli;
/// <summary>
/// Full reflective dump of a vitals LayoutDesc element tree: every scalar
/// property (position/size/flags) of each ElementDesc + its state sprites,
/// so the real bar rects + spacing + window size can be read from the dat
/// instead of guessed. Uses reflection so it doesn't depend on knowing the
/// DatReaderWriter property names ahead of time.
/// </summary>
public static class VitalsLayoutDump
{
public static int Run(string datDir, string? layoutIdText)
{
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
// Default to the vitals layout dump-vitals-bars found; allow override.
uint layoutId = 0x21000014u;
if (!string.IsNullOrWhiteSpace(layoutIdText))
{
var t = layoutIdText.Trim();
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t[2..];
uint.TryParse(t, System.Globalization.NumberStyles.HexNumber, null, out layoutId);
}
// First: scan ALL LayoutDescs that contain a vitals meter element, with root size,
// so we can tell whether 0x21000014 is the one the user sees (row vs stacked).
Console.WriteLine("=== LayoutDescs containing a vitals meter element (0x100000E6/EC/EE) ===");
foreach (var id in dats.GetAllIdsOfType<LayoutDesc>())
{
var l = dats.Get<LayoutDesc>(id);
if (l is null) continue;
if (!ContainsAny(l, 0x100000E6u, 0x100000ECu, 0x100000EEu)) continue;
Console.WriteLine($" 0x{id:X8} {RootSizeSummary(l)}");
}
Console.WriteLine();
var ld = dats.Get<LayoutDesc>(layoutId);
if (ld is null) { Console.Error.WriteLine($"layout 0x{layoutId:X8} not found"); return 1; }
Console.WriteLine($"=== FULL DUMP layout 0x{layoutId:X8} ===");
DumpScalars("LayoutDesc", ld, 0);
foreach (var kv in ld.Elements)
DumpElement(kv.Value, 1);
return 0;
}
private static bool ContainsAny(LayoutDesc l, params uint[] ids)
{
foreach (var kv in l.Elements)
if (ElemContains(kv.Value, ids)) return true;
return false;
}
private static bool ElemContains(ElementDesc e, uint[] ids)
{
if (Array.IndexOf(ids, e.ElementId) >= 0) return true;
foreach (var kv in e.Children)
if (ElemContains(kv.Value, ids)) return true;
return false;
}
private static string RootSizeSummary(LayoutDesc l)
{
// Print any LayoutDesc-level scalar that looks like a size.
var sb = new System.Text.StringBuilder();
foreach (var p in l.GetType().GetProperties())
{
if (p.GetIndexParameters().Length > 0) continue;
if (p.Name is "Elements") continue;
object? v; try { v = p.GetValue(l); } catch { continue; }
if (v is null) continue;
if (IsScalar(v)) sb.Append($"{p.Name}={v} ");
}
return sb.ToString().Trim();
}
private static void DumpElement(ElementDesc e, int depth)
{
string ind = new string(' ', depth * 2);
Console.WriteLine($"{ind}element 0x{e.ElementId:X8}");
DumpScalars(ind + " ", e, depth);
if (e.StateDesc is not null) DumpMedia(ind + " [DirectState]", e.StateDesc);
foreach (var s in e.States)
DumpMedia($"{ind} [state {s.Key}]", s.Value);
foreach (var c in e.Children)
DumpElement(c.Value, depth + 1);
}
private static readonly HashSet<string> Skip = new() { "Children", "States", "StateDesc", "Elements", "Media" };
private static void DumpScalars(string label, object o, int depth)
{
foreach (var (name, val) in Members(o))
{
if (Skip.Contains(name)) continue;
if (IsScalar(val))
Console.WriteLine($"{label} {name} = {Fmt(name, val)}");
}
}
private static void DumpMedia(string label, StateDesc sd)
{
foreach (var m in sd.Media)
{
var sb = new System.Text.StringBuilder();
foreach (var (name, val) in Members(m))
if (IsScalar(val)) sb.Append($"{name}={Fmt(name, val)} ");
Console.WriteLine($"{label} {m.GetType().Name}: {sb.ToString().Trim()}");
}
}
/// <summary>Enumerate public properties AND public fields (the DatReaderWriter
/// generated types expose geometry/file ids as fields, not properties).</summary>
private static IEnumerable<(string name, object val)> Members(object o)
{
var t = o.GetType();
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (p.GetIndexParameters().Length > 0) continue;
object? v; try { v = p.GetValue(o); } catch { continue; }
if (v is not null) yield return (p.Name, v);
}
foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
object? v; try { v = f.GetValue(o); } catch { continue; }
if (v is not null) yield return (f.Name, v);
}
}
private static string Fmt(string name, object v) =>
name.Contains("File", StringComparison.OrdinalIgnoreCase) && v is uint u ? $"0x{u:X8}" : v.ToString() ?? "";
private static bool IsScalar(object v)
{
var t = v.GetType();
if (v is string) return true;
if (t.IsPrimitive || t.IsEnum) return true;
if (v is IEnumerable) return false;
// value-type structs (Rectangle/Point/etc.) — print via ToString
return t.IsValueType;
}
}