using System.Diagnostics; using AcDream.Cli; 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); } if (args.Length >= 1 && args[0] == "dump-vitals-layout") { string? dvlDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? dvlLayout = args.ElementAtOrDefault(2); if (string.IsNullOrWhiteSpace(dvlDatDir)) { Console.Error.WriteLine("usage: AcDream.Cli dump-vitals-layout [0xLayoutId]"); return 2; } return VitalsLayoutDump.Run(dvlDatDir, dvlLayout); } if (args.Length >= 1 && args[0] == "list-ui-layouts") { string? luiDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? luiRootType = args.ElementAtOrDefault(2); if (string.IsNullOrWhiteSpace(luiDatDir)) { Console.Error.WriteLine("usage: AcDream.Cli list-ui-layouts [0xRootType]"); return 2; } return LayoutIndexDump.Run(luiDatDir, luiRootType); } if (args.Length >= 1 && args[0] == "render-vitals-mockup") { string? rvmDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string rvmOut = args.ElementAtOrDefault(2) ?? "vitals-mockup.png"; if (string.IsNullOrWhiteSpace(rvmDatDir)) { Console.Error.WriteLine("usage: AcDream.Cli render-vitals-mockup [out.png]"); return 2; } return VitalsMockup.Render(rvmDatDir, rvmOut); } if (args.Length >= 1 && args[0] == "dump-sprite-sheet") { string? dssDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? dssIds = args.ElementAtOrDefault(2); string dssOut = args.ElementAtOrDefault(3) ?? "sprite-sheet.png"; if (string.IsNullOrWhiteSpace(dssDir) || string.IsNullOrWhiteSpace(dssIds)) { Console.Error.WriteLine("usage: AcDream.Cli dump-sprite-sheet <0xId,0xId,...> [out.png]"); return 2; } return VitalsMockup.ExportSheet(dssDir, dssIds, dssOut); } if (args.Length >= 1 && args[0] == "dump-font-atlas") { string? dfaDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? dfaFont = args.ElementAtOrDefault(2); // 0xFontId (default 0x40000000) string? dfaSample = args.ElementAtOrDefault(3); // sample string string dfaOut = args.ElementAtOrDefault(4) ?? "font-atlas"; if (string.IsNullOrWhiteSpace(dfaDir)) { Console.Error.WriteLine("usage: AcDream.Cli dump-font-atlas [0xFontId] [sample] [outBase]"); return 2; } return FontAtlasDump.Run(dfaDir, dfaFont, dfaSample, dfaOut); } if (args.Length >= 1 && args[0] == "probe") { // probe if (args.Length < 6) { Console.Error.WriteLine("usage: AcDream.Cli probe "); return 2; } return VitalsMockup.Probe(args[1], int.Parse(args[2]), int.Parse(args[3]), int.Parse(args[4]), int.Parse(args[5])); } if (args.Length >= 1 && args[0] == "crop") { // crop if (args.Length < 8) { Console.Error.WriteLine("usage: AcDream.Cli crop "); return 2; } return VitalsMockup.Crop(args[1], int.Parse(args[2]), int.Parse(args[3]), int.Parse(args[4]), int.Parse(args[5]), int.Parse(args[6]), args[7]); } if (args.Length >= 1 && args[0] == "dump-edges") { string? deDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? deId = args.ElementAtOrDefault(2); if (string.IsNullOrWhiteSpace(deDir) || string.IsNullOrWhiteSpace(deId)) { Console.Error.WriteLine("usage: AcDream.Cli dump-edges <0xId>"); return 2; } return VitalsMockup.DumpEdges(deDir, deId); } if (args.Length >= 1 && args[0] == "mock-selbar") { string? msbDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string msbOut = args.ElementAtOrDefault(2) ?? "selbar.png"; if (string.IsNullOrWhiteSpace(msbDir)) { Console.Error.WriteLine("usage: AcDream.Cli mock-selbar [out.png]"); return 2; } return VitalsMockup.MockSelBar(msbDir, msbOut); } if (args.Length >= 1 && args[0] == "export-ui-sprite") { string? eusDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); string? eusId = args.ElementAtOrDefault(2); string eusOut = args.ElementAtOrDefault(3) ?? "sprite.png"; if (string.IsNullOrWhiteSpace(eusDatDir) || string.IsNullOrWhiteSpace(eusId)) { Console.Error.WriteLine("usage: AcDream.Cli export-ui-sprite <0xId> [out.png]"); return 2; } return VitalsMockup.ExportSprite(eusDatDir, eusId, eusOut); } // 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. string? datDir = args.FirstOrDefault() ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (string.IsNullOrWhiteSpace(datDir)) { Console.Error.WriteLine("usage: AcDream.Cli "); Console.Error.WriteLine(" or: set ACDREAM_DAT_DIR and run with no args"); return 2; } if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: directory not found: {datDir}"); return 2; } string[] required = ["client_portal.dat", "client_cell_1.dat", "client_highres.dat", "client_local_English.dat"]; var missing = required.Where(f => !File.Exists(Path.Combine(datDir, f))).ToArray(); if (missing.Length > 0) { Console.Error.WriteLine($"error: missing dat files in {datDir}:"); foreach (var f in missing) Console.Error.WriteLine($" - {f}"); return 2; } Console.WriteLine($"acdream asset dump"); Console.WriteLine($"dat dir: {datDir}"); Console.WriteLine(); var sw = Stopwatch.StartNew(); using var dats = new DatCollection(datDir, DatAccessType.Read); sw.Stop(); Console.WriteLine($"opened 4 dats in {sw.ElapsedMilliseconds} ms"); Console.WriteLine(); // File sizes, just so we can see they're real foreach (var f in required) { var path = Path.Combine(datDir, f); var mb = new FileInfo(path).Length / 1024.0 / 1024.0; Console.WriteLine($" {f,-30} {mb,8:F1} MB"); } Console.WriteLine(); // Count assets by type. Grouped by what matters for a renderer / client. // Uses DatCollection.GetAllIdsOfType() which routes to the right database internally. var sections = new (string Section, (string Name, Func Count)[] Rows)[] { ("visual — geometry", new (string, Func)[] { ("GfxObj", () => dats.GetAllIdsOfType().Count()), ("GfxObjDegradeInfo", () => dats.GetAllIdsOfType().Count()), ("Setup", () => dats.GetAllIdsOfType().Count()), ("Scene", () => dats.GetAllIdsOfType().Count()), ("Environment", () => dats.GetAllIdsOfType().Count()), }), ("visual — texturing / materials", new (string, Func)[] { ("Surface", () => dats.GetAllIdsOfType().Count()), ("SurfaceTexture", () => dats.GetAllIdsOfType().Count()), ("RenderSurface", () => dats.GetAllIdsOfType().Count()), ("RenderTexture", () => dats.GetAllIdsOfType().Count()), ("RenderMaterial", () => dats.GetAllIdsOfType().Count()), ("MaterialInstance", () => dats.GetAllIdsOfType().Count()), ("MaterialModifier", () => dats.GetAllIdsOfType().Count()), ("Palette", () => dats.GetAllIdsOfType().Count()), ("PalSet", () => dats.GetAllIdsOfType().Count()), }), // Note: Cell dat uses mask-based IDs for LandBlock/LandBlockInfo/EnvCell — the low // 16 bits distinguish the type. DatReaderWriter 2.1.4's GetAllIdsOfType() only // handles range-based types, so we walk the cell b-tree once manually. ("terrain / cells", CountCellByLow16(dats)), ("animation", new (string, Func)[] { ("Animation", () => dats.GetAllIdsOfType().Count()), ("MotionTable", () => dats.GetAllIdsOfType().Count()), }), ("particles & physics", new (string, Func)[] { ("ParticleEmitter", () => dats.GetAllIdsOfType().Count()), ("PhysicsScript", () => dats.GetAllIdsOfType().Count()), ("PhysicsScriptTable", () => dats.GetAllIdsOfType().Count()), }), ("audio", new (string, Func)[] { ("SoundTable", () => dats.GetAllIdsOfType().Count()), ("Wave", () => dats.GetAllIdsOfType().Count()), }), ("game data tables", new (string, Func)[] { ("SpellTable", () => dats.GetAllIdsOfType().Count()), ("SpellComponentTable", () => dats.GetAllIdsOfType().Count()), ("SkillTable", () => dats.GetAllIdsOfType().Count()), ("CombatTable", () => dats.GetAllIdsOfType().Count()), ("VitalTable", () => dats.GetAllIdsOfType().Count()), ("ExperienceTable", () => dats.GetAllIdsOfType().Count()), ("ClothingTable", () => dats.GetAllIdsOfType().Count()), ("CharGen", () => dats.GetAllIdsOfType().Count()), ("ChatPoseTable", () => dats.GetAllIdsOfType().Count()), ("ContractTable", () => dats.GetAllIdsOfType().Count()), }), ("ui / strings", new (string, Func)[] { ("Font", () => dats.GetAllIdsOfType().Count()), ("StringTable", () => dats.GetAllIdsOfType().Count()), ("LanguageString", () => dats.GetAllIdsOfType().Count()), ("LanguageInfo", () => dats.GetAllIdsOfType().Count()), ("LayoutDesc", () => dats.GetAllIdsOfType().Count()), }), }; long grandTotal = 0; foreach (var (section, rows) in sections) { Console.WriteLine($"[{section}]"); long sectionTotal = 0; foreach (var (name, count) in rows) { var n = count(); sectionTotal += n; Console.WriteLine($" {name,-22} {n,10:N0}"); } Console.WriteLine($" {"subtotal",-22} {sectionTotal,10:N0}"); Console.WriteLine(); grandTotal += sectionTotal; } Console.WriteLine($"grand total: {grandTotal:N0} indexed assets across tracked types"); return 0; static (string Name, Func Count)[] CountCellByLow16(DatCollection dats) { // Walk the cell b-tree once and bucket by low 16 bits: // 0xFFFF → LandBlock (terrain heightmap) // 0xFFFE → LandBlockInfo (static objects on the landblock) // other → EnvCell (indoor dungeon cell) int landBlocks = 0, landBlockInfos = 0, envCells = 0, other = 0; foreach (var file in dats.Cell.Tree) { var low = file.Id & 0xFFFFu; if (low == 0xFFFFu) landBlocks++; else if (low == 0xFFFEu) landBlockInfos++; else if (file.Id != 0) envCells++; else other++; } return new (string, Func)[] { ("LandBlock", () => landBlocks), ("LandBlockInfo", () => landBlockInfos), ("EnvCell", () => envCells), ("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())); } }