diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 36220375..4f8b7d92 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -51,14 +51,42 @@ we fail to draw (preferred: draw the authored one) before synthesizing. ## #380 — Chat tab: the two opacity sliders are missing their retail row captions -**Status:** OPEN — filed 2026-08-11 at Campaign OP gate 4 (user report: -"I also miss the text next to the bars for Inactive and Active Opacity"). -acdream's General Chat Options section renders the two sliders with only -the Transparent/Opaque endpoint labels; retail also captions each slider -row (inactive vs active opacity). Resolve the captions from the authored -layout/DAT strings (never invented English) — check whether the authored -elements exist and fail to resolve (the #375 resolver class) or are -missing from our template handling. +**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's +re-gate. Filed 2026-08-11 at Campaign OP gate 4 (user report: "I also +miss the text next to the bars for Inactive and Active Opacity"). + +**ROOT CAUSE — a DAT-resident runtime catalog, not a compiled symbol, +that nothing ever queried.** `PlayerOptionPage::AddSliderOption` never +sets a row's name-label text (byte-verified — no `StringInfo` write in +its pseudo-C body); the caption comes from a SEPARATE mechanism, +`UIOption_Slider::SetGameplayOptionProperty @0x00485030`'s own +`UIOption::InqGameplayOptionNameAndTooltip @0x004ef750` catalog lookup — +a SECOND `DBCache::GetDIDFromEnumStatic` sub-map lookup +(`(0x15, 2)`, sibling to the ALREADY-PORTED `(0x16, 2)` defaults lookup) +resolving to DID `0x78000000` (confirmed a DIFFERENT object from the +defaults catalog's `0x78000001`), a `DBProperties` with one `ArrayBase- +Property` of per-`GameplayOptionProperty` entries (name/tooltip +`StringInfo` + the owning property id). Live-DAT-read: the array has +exactly two entries, resolving via string table `0x2300000D` (the SAME +table #372 already established as this campaign's runtime-string home) +to "Inactive Opacity" / "Active Opacity" — the user's own two words. +**Fix:** `ChatOptionsDatCaptions.TryRead` ports the lookup (mirroring the +existing `ChatOptionsDatDefaults` shape); `ChatOptionsPageController` +wires the resolved captions onto element `0x1000021B` (the row's own +name-label child, present on BOTH slider templates but never referenced +by this controller before) and the tooltips onto each slider. Regressed +by `tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs` +(`Bind_WiresEachSlidersOwnRowCaption_FromTheResolvedDatCatalog`, +`Bind_MissingCaption_RendersNoText_NeverInventsEnglish`) and a new live- +mount probe (`ProbeChatOpacityCaptions` in +`OptionsPanelLiveMountProbeTests.cs`) that exercises the production +`ChatOptionsDatCaptions.TryRead` against the real DAT and asserts the +exact two strings. + +**Re-gate (§OP5, opacity sliders section): both slider rows should now +show their own caption ("Inactive Opacity" / "Active Opacity") next to +the bar, not just the Transparent/Opaque endpoint labels on the second +slider.** ## #379 — Chat-window opacity applies to ALL retained windows/panels, not only the chat windows diff --git a/docs/research/2026-08-11-campaign-op-test-script.md b/docs/research/2026-08-11-campaign-op-test-script.md index 9403b3a7..f15e24da 100644 --- a/docs/research/2026-08-11-campaign-op-test-script.md +++ b/docs/research/2026-08-11-campaign-op-test-script.md @@ -427,6 +427,12 @@ chat windows already read when deciding which lines to show. ### Opacity sliders — live drag, linked, never clamping +**Gate-4 re-test note (#380):** both sliders previously showed ONLY the +Transparent/Opaque endpoint labels on the second (Active) slider, with NO +row caption identifying which slider was which. Each slider row now shows +its own DAT-resolved caption — "Inactive Opacity" next to the first slider, +"Active Opacity" next to the second — report if either caption is missing +or shows the wrong text. **Gate-4 re-test note (#379) — step 4 below is REWRITTEN:** the sliders previously faded EVERY registered window (vitals, toolbar, inventory, diff --git a/src/AcDream.App/UI/Layout/ChatOptionsDatCaptions.cs b/src/AcDream.App/UI/Layout/ChatOptionsDatCaptions.cs new file mode 100644 index 00000000..a1ca5912 --- /dev/null +++ b/src/AcDream.App/UI/Layout/ChatOptionsDatCaptions.cs @@ -0,0 +1,129 @@ +using AcDream.Content; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Types; + +namespace AcDream.App.UI.Layout; + +/// +/// #380 (Campaign OP gate 4, 2026-08-11): reads the Chat tab's two opacity-slider +/// row CAPTIONS ("Inactive Opacity" / "Active Opacity") + tooltips from the +/// installed DAT — retail UIOption::InqGameplayOptionNameAndTooltip +/// @0x004ef750's own catalog lookup, the mechanism +/// UIOption_Slider::SetGameplayOptionProperty @0x00485030 calls for +/// EVERY UIOption_Slider (both Chat opacity rows go through this path — +/// see gmChatOptionsUI::InitOptions's own SetGameplayOptionProperty +/// calls at 0x0049fcc0/0x0049fd1a). PlayerOptionPage::AddSliderOption +/// itself never sets a row caption (byte-verified — no StringInfo write in +/// its pseudo-C body), so the row's name-label child (element 0x1000021B, +/// shared by BOTH the unlabelled and range-captioned slider templates) is never +/// populated by AddSliderOption, nor is it baked statically into the +/// LayoutDesc (the SAME shared template element is reused for every slider on +/// every tab, so it cannot carry a fixed caption) — +/// never wired ANY source for it, which is #380's root cause. +/// +/// +/// Byte-verified recipe (a SECOND DBCache::GetDIDFromEnumStatic +/// sub-map lookup, sibling to 's own +/// (0x16, 2) defaults catalog): InqGameplayOptionNameAndTooltip's own +/// call site is GetDIDFromEnumStatic(&ret, 0x15, 2) — SAME master-map +/// key (2) as the defaults lookup, DIFFERENT sub-map key (0x15 vs +/// 0x16) — so 's own +/// (enumValue, enumCategory) order is (0x15, 2). Empirically +/// confirmed against the installed DATs: resolves to DID 0x78000000 (a +/// DIFFERENT object than the defaults catalog's 0x78000001 — the two +/// enum values genuinely name separate DAT objects, not two views of one). That +/// DBProperties has ONE top-level entry, property 0xD2, an +/// ArrayBaseProperty of StructBaseProperty entries — one per +/// registered GameplayOptionProperty in the whole game (not opacity- +/// specific). Each entry carries: 0xD4 = name StringInfo, +/// 0xD5 = tooltip StringInfo, 0xD6 = the +/// GameplayOptionProperty id this entry describes (an +/// EnumBaseProperty), 0xD7/0xD8 = unrelated floats. The +/// live array has exactly two entries, matching +/// (0xD6 +/// = 0x10000080) and ActiveOpacityPropertyId (0xD6 = +/// 0x10000081) — resolving via table 0x2300000D (the SAME +/// TextFilter-family table #372 already established as this campaign's +/// runtime-string home, not the compiled-symbol table 0x23000003 the +/// section headers use) to "Inactive Opacity" / +/// "Active Opacity" — exactly the two labels the user's gate-4 report +/// named ("I also miss the text next to the bars for Inactive and Active +/// Opacity"). +/// +/// +public static class ChatOptionsDatCaptions +{ + /// DAT enum-category key for the FIRST (master-map) lookup — see + /// the class doc's byte-verified recipe (shared with + /// 's own defaults lookup). + private const uint EnumCategory = 2u; + + /// DAT enum-value key for the SECOND (sub-map) lookup — the + /// name/tooltip catalog, NOT 's + /// 0x16 defaults catalog. + private const uint EnumValue = 0x15u; + + private const uint CatalogArrayPropertyId = 0xD2u; + private const uint EntryNamePropertyId = 0xD4u; + private const uint EntryTooltipPropertyId = 0xD5u; + private const uint EntryOwningPropertyId = 0xD6u; + + /// One resolved name/tooltip pair. Either half may be + /// if its own StringInfo failed to resolve — + /// the caller renders no text rather than inventing English (matching + /// this codebase's uniform degrade discipline). + public readonly record struct Caption(string? Name, string? Tooltip); + + /// + /// Resolves the Default/Active opacity sliders' row captions + tooltips + /// from the live DAT. Returns only when the + /// catalog DID itself does not resolve or is not a DBProperties — + /// a missing INDIVIDUAL entry still returns with + /// that pair's left at its default (both null). + /// + public static bool TryRead( + IDatReaderWriter dats, + DatStringResolver strings, + out Caption defaultOpacity, + out Caption activeOpacity) + { + defaultOpacity = default; + activeOpacity = default; + + uint did = RetailDataIdResolver.Resolve(dats, EnumValue, EnumCategory); + if (did == 0u || !dats.Portal.TryGet(did, out DBProperties? props) || props is null) + return false; + + if (!props.Properties.TryGetValue(CatalogArrayPropertyId, out BaseProperty? arrayProp) + || arrayProp is not ArrayBaseProperty array) + return false; + + foreach (BaseProperty entry in array.Value) + { + if (entry is not StructBaseProperty entryStruct) continue; + if (!entryStruct.Value.TryGetValue(EntryOwningPropertyId, out BaseProperty? owningProp) + || owningProp is not EnumBaseProperty owningEnum) + continue; + + if (owningEnum.Value == ChatOptionsDatDefaults.DefaultOpacityPropertyId) + defaultOpacity = ReadCaption(entryStruct, strings); + else if (owningEnum.Value == ChatOptionsDatDefaults.ActiveOpacityPropertyId) + activeOpacity = ReadCaption(entryStruct, strings); + } + + return true; + } + + private static Caption ReadCaption(StructBaseProperty entry, DatStringResolver strings) + { + string? name = entry.Value.TryGetValue(EntryNamePropertyId, out BaseProperty? nameProp) + && nameProp is StringInfoBaseProperty nameInfo + ? strings.Resolve(nameInfo.Value.TableId.DataId, nameInfo.Value.StringId, nameInfo.Value.Token) + : null; + string? tooltip = entry.Value.TryGetValue(EntryTooltipPropertyId, out BaseProperty? tooltipProp) + && tooltipProp is StringInfoBaseProperty tooltipInfo + ? strings.Resolve(tooltipInfo.Value.TableId.DataId, tooltipInfo.Value.StringId, tooltipInfo.Value.Token) + : null; + return new Caption(name, tooltip); + } +} diff --git a/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs b/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs index 81c09296..74ec5da2 100644 --- a/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs +++ b/src/AcDream.App/UI/Layout/ChatOptionsPageController.cs @@ -107,6 +107,25 @@ public static class ChatOptionsPageController /// The slider leaf inside either slider row template's subtree. private const uint SliderElementId = 0x1000021Cu; + /// + /// #380 (2026-08-11, gate 4): the slider row's own NAME-CAPTION text + /// child ("Inactive Opacity" / "Active Opacity") — present on BOTH + /// template idx3 and idx6 (research doc §1.5's template array: "label + /// 0x1000021B + slider 0x1000021C", the SAME id + /// already cites for its own + /// slider rows sharing this template pool). Retail's + /// PlayerOptionPage::AddSliderOption never sets this element's + /// text itself (byte-verified — no StringInfo write in its + /// pseudo-C body); the caption comes from + /// UIOption_Slider::SetGameplayOptionProperty's own + /// InqGameplayOptionNameAndTooltip catalog lookup instead — see + /// 's own doc for the full byte + /// trace. Nothing wired this element at all before #380 — the user's + /// literal complaint ("I also miss the text next to the bars for + /// Inactive and Active Opacity"). + /// + private const uint SliderLabelElementId = 0x1000021Bu; + /// Labelled slider template's low/high range-caption children /// (research doc §1.5's template array; present only on template idx 6). private const uint SliderRangeMinElementId = 0x1000021Eu; @@ -199,7 +218,17 @@ public static class ChatOptionsPageController float DefaultOpacityDatDefault, float ActiveOpacityDatDefault, Func CurrentFilter, - Action SetFilter); + Action SetFilter, + // #380: the two sliders' row captions/tooltips, resolved ONCE by the + // caller (RetailUiRuntime) via ChatOptionsDatCaptions — the SAME + // "resolve once, thread the pre-resolved value through Bindings" + // shape DefaultOpacityDatDefault/ActiveOpacityDatDefault already + // established for the value half of this exact DAT lookup family. + // Null when the DAT read failed or that entry's own StringInfo did + // not resolve — the row then renders with NO caption rather than + // invented English, matching this codebase's uniform degrade rule. + ChatOptionsDatCaptions.Caption DefaultOpacityCaption, + ChatOptionsDatCaptions.Caption ActiveOpacityCaption); /// /// Builds the General Options header + 2 sliders + 5 filter sections (header + @@ -367,6 +396,16 @@ public static class ChatOptionsPageController SetRangeLabel(row2, SliderRangeMaxElementId, "ID_UI_Value_Opaque", resolveString); } + // #380: the row-name captions ("Inactive Opacity" / "Active + // Opacity") + tooltips — resolved once by the caller via + // ChatOptionsDatCaptions, threaded through Bindings exactly like the + // sibling DefaultOpacityDatDefault/ActiveOpacityDatDefault values. + // Element 0x1000021B is shared by BOTH templates (see its own doc). + if (row1 is not null) + SetOpacityCaption(row1, bindings.DefaultOpacityCaption, slider1); + if (row2 is not null) + SetOpacityCaption(row2, bindings.ActiveOpacityCaption, slider2); + if (slider1 is null || slider2 is null) return; @@ -431,6 +470,38 @@ public static class ChatOptionsPageController text.LinesProvider = () => new[] { new UiText.Line(label, text.DefaultColor) }; } + /// + /// #380: sets one opacity slider row's own name-caption (element + /// ) + tooltip from a pre-resolved + /// — the DAT-catalog + /// counterpart of (which resolves a + /// COMPILED symbol key; this resolves a RUNTIME DAT entry, so it takes + /// the already-resolved string rather than a key to hash). A null + /// leaves the row with + /// no caption rather than inventing English, matching every other + /// resolve-miss path in this controller. The tooltip attaches to the + /// SLIDER itself (the interactive/hoverable widget for this row — same + /// convention 's own slider + /// rows use), not the caption text. + /// + private static void SetOpacityCaption( + UiElement row, ChatOptionsDatCaptions.Caption caption, UiScrollbar? slider) + { + if (UiElement.FindDescendant(row, SliderLabelElementId) is UiText text) + { + if (caption.Name is { Length: > 0 } name) + text.LinesProvider = () => new[] { new UiText.Line(name, text.DefaultColor) }; + else + Console.WriteLine( + "[D.2b] ChatOptionsPageController: opacity slider caption did not " + + "resolve from the DAT name/tooltip catalog — the row renders with no " + + "text rather than invented English."); + } + + if (slider is not null && caption.Tooltip is { Length: > 0 } tooltip) + slider.TooltipText = tooltip; + } + /// /// One per-window filter block: resolves the block template DIRECTLY through /// (NOT via diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index f7e37e15..c92bb05c 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -2126,6 +2126,22 @@ public sealed class RetailUiRuntime : IDisposable + "constructor values (0.5/1.0)."); } + // #380: the two sliders' row CAPTIONS ("Inactive Opacity" / + // "Active Opacity") + tooltips — a SECOND, sibling DAT catalog + // lookup (DID 0x78000000, not the 0x78000001 defaults catalog + // above) — see ChatOptionsDatCaptions' own doc for the byte + // trace. Resolved once, same shape as the defaults read above. + if (!Layout.ChatOptionsDatCaptions.TryRead( + _bindings.Assets.Dats, strings, + out Layout.ChatOptionsDatCaptions.Caption defaultOpacityCaption, + out Layout.ChatOptionsDatCaptions.Caption activeOpacityCaption)) + { + Console.WriteLine( + "[UI] options panel: Chat tab opacity slider captions did not resolve " + + "(DID 0x78000000) — rows render with no caption rather than invented " + + "English."); + } + bool chatBound = Layout.ChatOptionsPageController.Bind( layout, controller.ChatPage, @@ -2153,6 +2169,8 @@ public sealed class RetailUiRuntime : IDisposable FlushOpacity: SaveChatOpacity, DefaultOpacityDatDefault: datDefaultOpacity, ActiveOpacityDatDefault: datActiveOpacity, + DefaultOpacityCaption: defaultOpacityCaption, + ActiveOpacityCaption: activeOpacityCaption, CurrentFilter: windowId => _bindings.Chat.Windows.GetFilter(windowId), SetFilter: (windowId, value) => { diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs index 1ba5ca61..f1341cfd 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ChatOptionsPageControllerTests.cs @@ -152,6 +152,13 @@ public sealed class ChatOptionsPageControllerTests public float ActiveOpacity = 1.0f; public float DefaultOpacityDatDefault = 0.5f; public float ActiveOpacityDatDefault = 1.0f; + // #380: realistic test defaults matching the live-DAT-verified + // strings (docs/research live-mount probe, 2026-08-11) so existing + // tests exercise the same shape production sees. + public ChatOptionsDatCaptions.Caption DefaultOpacityCaption = + new("Inactive Opacity", "Adjusts the opacity of the chat window when it is inactive"); + public ChatOptionsDatCaptions.Caption ActiveOpacityCaption = + new("Active Opacity", "Adjusts the opacity of the chat window when it is active"); public List DefaultOpacitySets { get; } = new(); public List ActiveOpacitySets { get; } = new(); public int OpacityFlushes { get; private set; } @@ -182,6 +189,8 @@ public sealed class ChatOptionsPageControllerTests FlushOpacity: () => OpacityFlushes++, DefaultOpacityDatDefault: DefaultOpacityDatDefault, ActiveOpacityDatDefault: ActiveOpacityDatDefault, + DefaultOpacityCaption: DefaultOpacityCaption, + ActiveOpacityCaption: ActiveOpacityCaption, CurrentFilter: windowId => Filters[windowId], SetFilter: (windowId, value) => { @@ -388,6 +397,111 @@ public sealed class ChatOptionsPageControllerTests Assert.Empty(fakeBindings.ActiveOpacitySets); } + /// + /// #380 regression (2026-08-11, gate 4): before this fix, NOTHING wired + /// element 0x1000021B on either slider row, so both rows rendered + /// with no caption at all — the user's literal complaint ("I also miss + /// the text next to the bars for Inactive and Active Opacity"). Drives + /// the row-caption text down from / + /// ActiveOpacityCaption the same way production resolves it + /// ( against the live DAT) and reads + /// back the ACTUAL built next to each slider — not + /// just that the field was set somewhere, but that IT rendered on the + /// correct row (Default → row 1's own caption, Active → row 2's own, + /// never swapped or bled onto the wrong slider). + /// + [Fact] + public void Bind_WiresEachSlidersOwnRowCaption_FromTheResolvedDatCatalog() + { + var fakeBindings = new FakeBindings + { + DefaultOpacityCaption = new ChatOptionsDatCaptions.Caption("Inactive Opacity", "inactive tip"), + ActiveOpacityCaption = new ChatOptionsDatCaptions.Caption("Active Opacity", "active tip"), + }; + ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); + OptionsPanelController controller = OptionsPanelController.Bind( + layout, + new OptionsPanelController.Callbacks( + Toggle: () => { }, + RequestExitToCharacterSelection: () => { }, + ExitGame: () => { }, + UseMouseTurningSettings: () => { }, + DisplaySystemMessage: _ => { }))!; + bool bound = ChatOptionsPageController.Bind( + layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null, + fakeBindings.ToBindings()); + Assert.True(bound); + + var listBox = Assert.IsType( + layout.FindElement(ChatOptionsPageController.ListBoxElementId)); + List sliders = CollectScalarSliders(listBox); + Assert.Equal(2, sliders.Count); // build order: [0]=Default, [1]=Active + + UiElement defaultRowRoot = FindRowRoot(sliders[0]); + UiElement activeRowRoot = FindRowRoot(sliders[1]); + + var defaultCaption = Assert.IsType( + UiElement.FindDescendant(defaultRowRoot, 0x1000021Bu)); + var activeCaption = Assert.IsType( + UiElement.FindDescendant(activeRowRoot, 0x1000021Bu)); + + Assert.Equal("Inactive Opacity", Assert.Single(defaultCaption.LinesProvider()).Text); + Assert.Equal("Active Opacity", Assert.Single(activeCaption.LinesProvider()).Text); + + // The tooltip attaches to the interactive slider itself, per-row. + Assert.Equal("inactive tip", sliders[0].TooltipText); + Assert.Equal("active tip", sliders[1].TooltipText); + } + + /// A DAT read failure (or a missing catalog entry) must never + /// invent English — the row renders with NO caption, exactly like every + /// other resolve-miss path already established in this controller + /// (filter labels, section headers, range captions). + [Fact] + public void Bind_MissingCaption_RendersNoText_NeverInventsEnglish() + { + var fakeBindings = new FakeBindings + { + DefaultOpacityCaption = default, // both Name/Tooltip null + ActiveOpacityCaption = default, + }; + ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost(); + OptionsPanelController controller = OptionsPanelController.Bind( + layout, + new OptionsPanelController.Callbacks( + Toggle: () => { }, + RequestExitToCharacterSelection: () => { }, + ExitGame: () => { }, + UseMouseTurningSettings: () => { }, + DisplaySystemMessage: _ => { }))!; + bool bound = ChatOptionsPageController.Bind( + layout, controller.ChatPage, MakeTemplateResolver(), (_, _) => null, + fakeBindings.ToBindings()); + Assert.True(bound); + + var listBox = Assert.IsType( + layout.FindElement(ChatOptionsPageController.ListBoxElementId)); + List sliders = CollectScalarSliders(listBox); + UiElement defaultRowRoot = FindRowRoot(sliders[0]); + + var defaultCaption = Assert.IsType( + UiElement.FindDescendant(defaultRowRoot, 0x1000021Bu)); + Assert.Empty(defaultCaption.LinesProvider()); + Assert.Null(sliders[0].TooltipText); + } + + /// Walks up from a built slider leaf to the row root + /// stacked directly under + /// the viewport — the same subtree + /// itself scopes its FindDescendant caption lookup to. + private static UiElement FindRowRoot(UiElement leaf) + { + UiElement node = leaf; + while (node.Parent is { } parent && parent is not UiScrollablePanel) + node = parent; + return node; + } + [Fact] public void DraggingDefaultSlider_AppliesLive_AndDragsActiveUp_NeverClamping() { diff --git a/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs index 501e7def..c6a6a68a 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ConfigOptionsPageControllerTests.cs @@ -930,7 +930,9 @@ public sealed class ConfigOptionsPageControllerTests DefaultOpacityDatDefault: 0.5f, ActiveOpacityDatDefault: 1.0f, CurrentFilter: _ => 0xFBFFFFFFul, - SetFilter: (_, _) => { }); + SetFilter: (_, _) => { }, + DefaultOpacityCaption: new ChatOptionsDatCaptions.Caption("Inactive Opacity", null), + ActiveOpacityCaption: new ChatOptionsDatCaptions.Caption("Active Opacity", null)); } [Fact] diff --git a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs index 94ded3f4..3b58c08f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/OptionsPanelLiveMountProbeTests.cs @@ -293,6 +293,43 @@ public sealed class OptionsPanelLiveMountProbeTests return null; } + /// #380 (gate 4): exercises the PRODUCTION + /// mechanism against the real + /// DAT and asserts the two resolved captions match the user's own + /// complaint ("Inactive Opacity" / "Active Opacity") — live-mount proof + /// that #380's fix reads the SAME two strings the fixture-driven + /// regression tests (ChatOptionsPageControllerTests) only assume + /// via a fake. + [Fact] + public void ProbeChatOpacityCaptions() + { + 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); + + bool ok = ChatOptionsDatCaptions.TryRead( + dats, strings, + out ChatOptionsDatCaptions.Caption defaultOpacity, + out ChatOptionsDatCaptions.Caption activeOpacity); + + Console.WriteLine($"[probe380] TryRead ok={ok}"); + Console.WriteLine($"[probe380] Default: Name='{defaultOpacity.Name}' Tooltip='{defaultOpacity.Tooltip}'"); + Console.WriteLine($"[probe380] Active: Name='{activeOpacity.Name}' Tooltip='{activeOpacity.Tooltip}'"); + + Assert.True(ok); + Assert.Equal("Inactive Opacity", defaultOpacity.Name); + Assert.Equal("Active Opacity", activeOpacity.Name); + Assert.False(string.IsNullOrEmpty(defaultOpacity.Tooltip)); + Assert.False(string.IsNullOrEmpty(activeOpacity.Tooltip)); + } + /// #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.