chore(D.2b): CLI dump-vitals-bars — read vitals LayoutDesc meter sprites
Adds `AcDream.Cli dump-vitals-bars <datDir>` 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<uint,ElementDesc>).
ElementDesc shape: ElementId, Type, BaseElement, BaseLayoutId, DefaultState,
X/Y/Width/Height/ZLevel, LeftEdge/TopEdge/RightEdge/BottomEdge,
States (Dictionary<UIStateId,StateDesc>), Children (Dictionary<uint,ElementDesc>),
StateDesc (direct single state).
StateDesc shape: StateId, PassToChildren, IncorporationFlags,
Properties (Dictionary<uint,BaseProperty>), Media (List<MediaDesc>).
MediaDescImage shape: File (uint DataId), DrawMode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b303baf4a1
commit
56ee5eff60
1 changed files with 156 additions and 0 deletions
|
|
@ -3,8 +3,21 @@ using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Options;
|
using DatReaderWriter.Options;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
using Env = System.Environment;
|
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 <dat-directory>");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return DumpVitalsBars(dvbDatDir);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 0: open the four AC dat files and print how many of each asset type live in them.
|
// 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
|
// This proves DatReaderWriter works on our retail dats and gives us a baseline inventory
|
||||||
// to compare against what a future renderer needs.
|
// to compare against what a future renderer needs.
|
||||||
|
|
@ -160,3 +173,146 @@ static (string Name, Func<int> Count)[] CountCellByLow16(DatCollection dats)
|
||||||
("Region", () => dats.GetAllIdsOfType<Region>().Count()),
|
("Region", () => dats.GetAllIdsOfType<Region>().Count()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<LayoutDesc>())
|
||||||
|
{
|
||||||
|
var ld = dats.Get<LayoutDesc>(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(" <element not found>"); continue; }
|
||||||
|
|
||||||
|
var sprites = new List<(string Role, uint DataId, string DrawMode)>();
|
||||||
|
VbCollectSprites(meterElem, sprites, 0);
|
||||||
|
|
||||||
|
if (sprites.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" <no sprites found in sub-tree>");
|
||||||
|
}
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue