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>
This commit is contained in:
parent
ada863980c
commit
ff29787f12
5 changed files with 293 additions and 89 deletions
152
src/AcDream.Cli/VitalsLayoutDump.cs
Normal file
152
src/AcDream.Cli/VitalsLayoutDump.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue