diff --git a/docs/research/2026-08-10-options-panel-structure.md b/docs/research/2026-08-10-options-panel-structure.md index 4d3d46b3..04bd5778 100644 --- a/docs/research/2026-08-10-options-panel-structure.md +++ b/docs/research/2026-08-10-options-panel-structure.md @@ -625,7 +625,7 @@ opt-in). push low; call AddChild` sequences (`decode_addchild.py` over the raw section bytes; masks are 64-bit, `high:low`): -| Row | Mask (high:low) | Label global | Label literal (table `0x23000003`) | +| Row | Mask (high:low) | Label global | Label literal (table `0x2300000D` — CORRECTED 2026-08-11 at #372: the TextFilter label/tooltip family lives in its OWN table, dat-verified by exhaustive sweep; the original 0x23000003 attribution was inferred) | |---|---|---|---| | **0** | `0x00000000_83912021` | `ID_ChatOption_TextFilter_Gameplay` (`0x0083E840`) | Gameplay | | 1 | `0x00000000_00600040` | `ID_ChatOption_TextFilter_Combat` (`0x0083E830`) | Combat | diff --git a/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs b/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs index ed8cfebf..6037f363 100644 --- a/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs +++ b/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs @@ -81,6 +81,17 @@ public static class ChatOptionsPageController private const uint StringTableId = 0x23000003u; + /// #372 (2026-08-11): the ID_ChatOption_TextFilter_* + /// label/tooltip family lives in its OWN string table — an exhaustive + /// live-DAT sweep (OptionsPanelLiveMountProbeTests.ProbeFilterLabelHome) + /// found the keys resolve ONLY in 0x2300000D, not the + /// 0x23000003 options table the section headers and slider labels + /// use (the research doc's §8 table-attribution was inferred, not + /// dat-verified — the keys themselves were always right). All 13 filter + /// rows rendered blank-captioned at the first connected gate because of + /// this one wrong table id. + private const uint FilterStringTableId = 0x2300000Du; + /// The slider leaf inside either slider row template's subtree. private const uint SliderElementId = 0x1000021Cu; @@ -461,9 +472,9 @@ public static class ChatOptionsPageController continue; // research doc §5.2: the main window has no Gameplay row. FilterRowSpec rowSpec = FilterRows[i]; - string? label = resolveString(StringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey)); + string? label = resolveString(FilterStringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey)); string? tooltip = resolveString( - StringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey + "_Desc")); + FilterStringTableId, DatStringResolver.ComputeHash(rowSpec.RetailLabelKey + "_Desc")); if (label is null) Console.WriteLine( $"[D.2b] ChatOptionsPageController: filter row label " diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs index 83f52668..2c5046ca 100644 --- a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs @@ -87,6 +87,58 @@ public sealed class OptionsPanelLiveMountProbeTests } } + /// #372 minor half: the 13 ID_ChatOption_TextFilter_* labels fail + /// to resolve in table 0x23000003 — sweep the plausible tables and key + /// spellings against the live DAT to find their real home. + [Fact] + public void ProbeFilterLabelHome() + { + if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1") + return; + + var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Documents", + "Asheron's Call"); + using var dats = new DatCollection(datDir, DatAccessType.Read); + var strings = new DatStringResolver(dats); + + string[] keys = + { + "ID_ChatOption_TextFilter_Combat", + "ID_ChatOption_TextFilter_Gameplay", + "ID_ChatOptions_TextFilter_Combat", // plural-Options variant + "ID_Chat_TextFilter_Combat", + "ID_TextFilter_Combat", + "TextFilter_Combat", + }; + for (uint table = 0x23000001u; table <= 0x2300000Au; table++) + { + foreach (string key in keys) + { + string? hit = strings.Resolve(table, DatStringResolver.ComputeHash(key)); + if (hit is not null) + Console.WriteLine($"[probe] table 0x{table:X8} key '{key}' -> '{hit}'"); + } + } + // Also: does the KNOWN-good Character-tab key family resolve where + // documented, as a control? + Console.WriteLine( + "[probe] control ID_PlayerOption_AutoTarget in 0x23000003 -> " + + $"'{strings.Resolve(0x23000003u, DatStringResolver.ComputeHash("ID_PlayerOption_AutoTarget"))}'"); + + // Exhaustive: sweep EVERY string table in the local dat for the key. + uint targetHash = DatStringResolver.ComputeHash("ID_ChatOption_TextFilter_Combat"); + foreach (uint tableId in dats.Local.GetAllIdsOfType()) + { + string? hit = strings.Resolve(tableId, targetHash); + if (hit is not null) + Console.WriteLine($"[probe] EXHAUSTIVE hit: table 0x{tableId:X8} -> '{hit}'"); + } + Console.WriteLine("[probe] exhaustive sweep complete"); + } + private static void DumpTree(ElementInfo node, int depth, int maxDepth) { if (depth > maxDepth) return;