From 56ee5eff60190ffc03f487861fdf32df18c23b2b Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 14 Jun 2026 19:39:33 +0200 Subject: [PATCH] =?UTF-8?q?chore(D.2b):=20CLI=20dump-vitals-bars=20?= =?UTF-8?q?=E2=80=94=20read=20vitals=20LayoutDesc=20meter=20sprites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `AcDream.Cli dump-vitals-bars ` subcommand that: - Scans all 101 LayoutDesc objects in client_local_English.dat - Finds the vitals window layout (0x21000014) by locating the Health meter element id 0x100000E6 (from gmVitalsUI::PostInit decomp) - Walks each meter's sub-element tree (typed access via ElementDesc.Children, ElementDesc.States, ElementDesc.StateDesc, StateDesc.Media, MediaDescImage.File) - Prints every RenderSurface DataId (0x06xxxxxx) per vital Authoritative output: HEALTH (0x100000E6): front-bar fill 0x06005F3D / track fill 0x06005F3C E8/E9/EA pieces: 0x06001131/32/33, 0x06001141/40/3F STAMINA (0x100000EC): front-bar fill 0x06005F3F / track fill 0x06005F3E E8/E9/EA pieces: 0x06001137/38/39, 0x06001147/46/45 MANA (0x100000EE): front-bar fill 0x06005F41 / track fill 0x06005F40 E8/E9/EA pieces: 0x06001134/35/36, 0x06001144/43/42 LayoutDesc shape discovered: Fields Width, Height, Elements (HashTable). ElementDesc shape: ElementId, Type, BaseElement, BaseLayoutId, DefaultState, X/Y/Width/Height/ZLevel, LeftEdge/TopEdge/RightEdge/BottomEdge, States (Dictionary), Children (Dictionary), StateDesc (direct single state). StateDesc shape: StateId, PassToChildren, IncorporationFlags, Properties (Dictionary), Media (List). MediaDescImage shape: File (uint DataId), DrawMode. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Cli/Program.cs | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/AcDream.Cli/Program.cs b/src/AcDream.Cli/Program.cs index a4c290ee..c4ad9e71 100644 --- a/src/AcDream.Cli/Program.cs +++ b/src/AcDream.Cli/Program.cs @@ -3,8 +3,21 @@ using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Options; +using DatReaderWriter.Types; using Env = System.Environment; +// ─── subcommand dispatch ──────────────────────────────────────────────────── +if (args.Length >= 1 && args[0] == "dump-vitals-bars") +{ + string? dvbDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); + if (string.IsNullOrWhiteSpace(dvbDatDir)) + { + Console.Error.WriteLine("usage: AcDream.Cli dump-vitals-bars "); + return 2; + } + return DumpVitalsBars(dvbDatDir); +} + // Phase 0: open the four AC dat files and print how many of each asset type live in them. // This proves DatReaderWriter works on our retail dats and gives us a baseline inventory // to compare against what a future renderer needs. @@ -160,3 +173,146 @@ static (string Name, Func Count)[] CountCellByLow16(DatCollection dats) ("Region", () => dats.GetAllIdsOfType().Count()), }; } + +/// +/// dump-vitals-bars: find the vitals window LayoutDesc (0x21000014) and print the +/// RenderSurface DataIds (0x06xxxxxx) used by the Health, Stamina, and Mana meter +/// bars. Each meter element (E6/EC/EE) has two child sub-groups per bar visual +/// (front-bar and back-bar/track), each containing: +/// - elem 0x100004A9 (ShowDetail state image = Alphablend fill sprite) +/// - elem 0x100000E8 (DirectStateDesc = left-edge sprite) +/// - elem 0x100000E9 (DirectStateDesc = fill-tile sprite) +/// - elem 0x100000EA (DirectStateDesc = right-edge sprite) +/// +/// Based on the Sept 2013 EoR retail dat, vitals layout id = 0x21000014. +/// Element ids from gmVitalsUI::PostInit in acclient_2013_pseudo_c.txt. +/// +static int DumpVitalsBars(string dvbDatDir) +{ + const uint HEALTH_ELEM_ID = 0x100000E6u; + const uint STAMINA_ELEM_ID = 0x100000ECu; + const uint MANA_ELEM_ID = 0x100000EEu; + + if (!Directory.Exists(dvbDatDir)) + { + Console.Error.WriteLine($"error: directory not found: {dvbDatDir}"); + return 2; + } + + using var dats = new DatCollection(dvbDatDir, DatAccessType.Read); + + // Find the vitals layout: scan all LayoutDescs for one containing the health meter element. + Console.WriteLine("Scanning LayoutDescs for vitals window (element 0x100000E6 = Health meter)..."); + uint? vitalsId = null; + LayoutDesc? vitalsLayout = null; + foreach (var id in dats.GetAllIdsOfType()) + { + var ld = dats.Get(id); + if (ld is null) continue; + if (VbContainsElementId(ld, HEALTH_ELEM_ID)) { vitalsId = id; vitalsLayout = ld; break; } + } + + if (vitalsLayout is null) + { + Console.Error.WriteLine("ERROR: no LayoutDesc contains element 0x100000E6 (Health meter)."); + return 1; + } + Console.WriteLine($"Found vitals layout: 0x{vitalsId!.Value:X8}"); + Console.WriteLine(); + + // For each vital meter, collect all MediaDescImage DataIds from its sub-tree. + var meters = new[] { (HEALTH_ELEM_ID, "HEALTH"), (STAMINA_ELEM_ID, "STAMINA"), (MANA_ELEM_ID, "MANA") }; + foreach (var (eid, vitalName) in meters) + { + Console.WriteLine($"{vitalName} meter (element 0x{eid:X8}) in layout 0x{vitalsId!.Value:X8}:"); + var meterElem = VbFindElement(vitalsLayout!, eid); + if (meterElem is null) { Console.WriteLine(" "); continue; } + + var sprites = new List<(string Role, uint DataId, string DrawMode)>(); + VbCollectSprites(meterElem, sprites, 0); + + if (sprites.Count == 0) + { + Console.WriteLine(" "); + } + else + { + foreach (var (role, dataId, drawMode) in sprites) + Console.WriteLine($" {role,-35} 0x{dataId:X8} ({drawMode})"); + } + Console.WriteLine(); + } + + return 0; +} + +// ─── dump-vitals-bars helpers ─────────────────────────────────────────────── + +static bool VbContainsElementId(LayoutDesc ld, uint targetId) +{ + var elems = ld.Elements; + foreach (var kvp in elems) + { + if (kvp.Key == targetId) return true; + if (VbChildContains(kvp.Value, targetId)) return true; + } + return false; +} + +static bool VbChildContains(ElementDesc elem, uint targetId) +{ + foreach (var kvp in elem.Children) + { + if (kvp.Key == targetId) return true; + if (VbChildContains(kvp.Value, targetId)) return true; + } + return false; +} + +static ElementDesc? VbFindElement(LayoutDesc ld, uint targetId) +{ + foreach (var kvp in ld.Elements) + { + if (kvp.Key == targetId) return kvp.Value; + var found = VbFindChild(kvp.Value, targetId); + if (found is not null) return found; + } + return null; +} + +static ElementDesc? VbFindChild(ElementDesc elem, uint targetId) +{ + foreach (var kvp in elem.Children) + { + if (kvp.Key == targetId) return kvp.Value; + var found = VbFindChild(kvp.Value, targetId); + if (found is not null) return found; + } + return null; +} + +static void VbCollectSprites(ElementDesc elem, List<(string, uint, string)> out_, int depth) +{ + string indent = new string(' ', depth * 2); + + // Check the element's direct StateDesc + if (elem.StateDesc is not null) + VbExtractMedia(elem.StateDesc, $"{indent}elem_0x{elem.ElementId:X8}.DirectState", out_); + + // Check each named state + foreach (var kvp in elem.States) + VbExtractMedia(kvp.Value, $"{indent}elem_0x{elem.ElementId:X8}.{kvp.Key}", out_); + + // Recurse into children + foreach (var kvp in elem.Children) + VbCollectSprites(kvp.Value, out_, depth + 1); +} + +static void VbExtractMedia(StateDesc sd, string role, List<(string, uint, string)> out_) +{ + foreach (var m in sd.Media) + { + if (m is MediaDescImage img && img.File != 0) + out_.Add((role, img.File, img.DrawMode.ToString())); + } +}