tools(LayoutDump): --props and --media, for questions the decomp cannot answer

Two dump modes, both added because a chat bug turned on authored data rather
than code.

--props prints each state's property ids WITH VALUES. It is what settled the
unseen-text indicator regression: element 0x1000048C authors dat property 0x3B
("Invisible") per state —

    state 13 Ghosted  0x3B = True    -> hidden
    state 1  Normal   0x3B = False   -> shown

— which is what actually puts that element on screen. No amount of reading the
decomp would have produced those two booleans.

--media prints the RAW media sequence per state off the LayoutDesc, because
ElementInfo keeps only the FIRST image of a state (LayoutImporter.cs, the
`!imageRead` guard) and an animation is therefore invisible above that level.
It reports what it CANNOT do rather than implying an element has no media: raw
descriptors only carry what an element overrides, and states usually come from
a base element — for the indicator, base 0x10000527, which lives in a different
layout entirely. Following that chain means reimplementing LayoutImporter's
Resolve, so the mode says so instead of printing a misleading empty list.

What the two modes established between them, for whoever picks up the blink:
the media vocabulary includes MediaDescImage, MediaDescPause and MediaDescJump,
so an authored animation is a SEQUENCE with waits and a loop — a small program,
not a frame array. Supporting it means keeping the sequence through import and
running a media player, not just widening StateMedia to a list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 10:41:54 +02:00
parent a330d50df9
commit f44f7641b1

View file

@ -46,6 +46,55 @@ if (root is null)
Console.WriteLine($"layout 0x{ids[0]:X8}");
Print(root, 0);
int mediaAt = Array.IndexOf(args, "--media");
if (mediaAt >= 0)
{
// The RAW media sequence per state, straight off the LayoutDesc. ElementInfo
// keeps only the first image, so an animation is invisible above that level.
uint wanted = mediaAt + 1 < args.Length
? Convert.ToUInt32(args[mediaAt + 1], 16)
: 0u;
// Layouts are not necessarily in Portal — go through the adapter, the same
// way LayoutImporter does.
var ld = adapter.Get<DatReaderWriter.DBObjs.LayoutDesc>(ids[0]);
if (ld is null)
{
Console.WriteLine($"layout 0x{ids[0]:X8} not found");
return 2;
}
foreach (var top in ld.Elements)
Walk(top.Value);
return 0;
void Walk(DatReaderWriter.Types.ElementDesc d)
{
if (wanted == 0 || d.ElementId == wanted)
{
Console.WriteLine($"element 0x{d.ElementId:X8}");
if (d.States.Count == 0)
{
// Raw descriptors only carry what THIS element overrides; states
// and their media usually come from the base element, and
// LayoutDesc::InqFullDesc @0x0069A520 resolves that chain.
// Following it here would mean reimplementing LayoutImporter's
// Resolve, so say so rather than imply the element has none.
Console.WriteLine(
$" (no states of its own — inherited from base 0x{d.BaseElement:X8};"
+ " raw media not resolved here)");
}
foreach (var st in d.States)
{
Console.WriteLine($" state {st.Key}: {st.Value.Media.Count} media");
foreach (var m in st.Value.Media)
Console.WriteLine($" {m.GetType().Name}");
}
}
foreach (var child in d.Children)
Walk(child.Value);
}
}
int resizeAt = Array.IndexOf(args, "--resize");
if (resizeAt >= 0 && resizeAt + 2 < args.Length)
{