From 1b9dd6c7a860a506928a9ee9918a381c4bdba5e6 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 26 Jun 2026 11:19:03 +0200 Subject: [PATCH] =?UTF-8?q?docs(ui):=20Fix=204=20=E2=80=94=20dat=20UIState?= =?UTF-8?q?=20model=20has=20no=20Group=20visibility=20encoding;=20Characte?= =?UTF-8?q?rStatController=20hacks=20are=20correct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation (2026-06-26): the character footer three sibling groups (0x10000240/241/247) all carry DefaultState=0x10000011 (StatManagement_Footer_Default) — the dat does NOT differentiate them by visibility. Parent 0x1000022F has States {Default, Text, Meter} with PassToChildren=true, but each child group registers all three states with IncorporationFlags=None and Media=0, so state-propagation produces no visual change on any group. Retail gmStatManagementUI uses hardcoded element-id dispatch for 0x10000240/241/247 — the groups are never hidden/shown via the dat state mechanism (decomp gmStatManagementUI::GetFooterTitleLabel @0x004f0170). Tab-page containers (0x1000022B/22C/10000539) have DefaultState=Undef — no visibility encoding. Tab visibility is managed purely at runtime by gmTabUI. IncorporationFlags (DatReaderWriter): {None, PassToChildren, X, Y, Width, Height, ZLevel} — no Visible flag. The importer cannot set initial Group visibility from the dat; the controller is the correct and only place. CharacterStatController HideAllById footer pass and ContainsWidget page-visibility walk are retail-faithful and must be kept. No logic changes — comment-only documentation. Build green, 710 App tests green. Studio screenshots: character footer shows State-A by default with no overlap; vitals and toolbar unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .../UI/Layout/CharacterStatController.cs | 19 +++++++++++++++++++ src/AcDream.App/UI/Layout/LayoutImporter.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) 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.