diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 7b7e6a6f..282d94d6 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -333,6 +333,19 @@ public static class CharacterStatController // the LAST copy of each id; the others live in the VISIBLE Attributes page and must // also be hidden. // + // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): + // All three group elements have DefaultState = StatManagement_Footer_Default + // (0x10000011) — the dat does NOT differentiate them by visibility. The parent + // element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren= + // true, but each child group also registers all three states (IncFlags=None, + // Media=0) — meaning the state-propagation produces no media change on any group. + // Retail's gmStatManagementUI uses hardcoded element-id dispatch + // (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247 + // for Meter) to access the right group's children at runtime — the groups themselves + // are never hidden/shown via the dat state mechanism. The controller is the correct + // and only place for this visibility management. See retail decomp + // gmStatManagementUI::GetFooterTitleLabel @0x004f0170. + // // Strategy: walk the Attributes page (the root child that contains the NameId anchor) // and hide every UiDatElement whose ElementId matches B or C. The page-visibility // pass hides everything in the Skills/Titles pages, so we only need to act on the @@ -368,6 +381,12 @@ public static class CharacterStatController } // ── Active-page selection (fixes the dark-overlay) ───────────────────── + // WHY this cannot be done in the importer (dat state-model audit 2026-06-26): + // The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills, + // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no + // visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI + // via SetVisible(bool) on the page containers. The controller is the correct + // and only place for initial tab-page selection. if (layout.FindElement(NameId) is { } anchor && layout.Root is { } root) { foreach (var page in root.Children) diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 49130206..24fb30f0 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -186,6 +186,25 @@ public static class LayoutImporter /// Dat shell: load the LayoutDesc, resolve inheritance for every top-level /// element, and build the widget tree. Returns null if the layout is absent /// from the dats. + /// + /// + /// Dat UIState visibility model (2026-06-26 audit): + /// The dat's ElementDesc.DefaultState field specifies which SPRITE/MEDIA + /// state an element starts in (e.g., Normal, Minimized). It does + /// NOT encode visibility of sibling Group containers. The StateDesc's + /// contains X/Y/Width/Height/ + /// ZLevel/PassToChildren — there is no Visible flag. + /// + /// + /// + /// Windows that display multiple sibling Group containers at the same position + /// (the character footer's three state-groups; the tab-page content areas) manage + /// visibility purely at runtime via C++ controller code. Retail uses + /// UIElement::SetState(stateId) on the parent to propagate state, then + /// C++ getters access the right sub-group by element id. All groups are shipped + /// as visible in the imported widget tree; the relevant controllers + /// () perform the initial show/hide. + /// /// /// Optional per-element font resolver (see /// for details). Null = original single-font behavior.