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())); + } +}