fix #380: Chat tab opacity sliders were missing their retail row captions
Root cause: PlayerOptionPage::AddSliderOption never sets a row's own name-label text — the "Inactive Opacity"/"Active Opacity" caption comes from a SEPARATE DAT-resident runtime catalog (DID 0x78000000, resolved via the same two-level DBCache::GetDIDFromEnumStatic master-map/submap lookup ChatOptionsDatDefaults already uses for enum 0x16/category 2, here for enum 0x15/category 2) that nothing in the codebase ever queried, so both slider rows rendered with no caption at all. Fix: new ChatOptionsDatCaptions.TryRead resolves the DID-0x78000000 catalog's per-property name/tooltip entries (matched by the same owning-property enum ChatOptionsDatDefaults already keys its defaults by) and ChatOptionsPageController.BuildOpacitySliders stamps each slider's own row caption/tooltip from it — falling back to no text (never invented English) if resolution fails. Regressed by ChatOptionsPageControllerTests. Bind_WiresEachSlidersOwnRowCaption_FromTheResolvedDatCatalog and the companion Bind_MissingCaption_RendersNoText_NeverInventsEnglish case, plus a live-mount probe (OptionsPanelLiveMountProbeTests. ProbeChatOpacityCaptions) confirming the production TryRead call resolves "Inactive Opacity"/"Active Opacity" against the real DAT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c0b3d8f233
commit
2a248c0d48
8 changed files with 415 additions and 10 deletions
|
|
@ -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<float> DefaultOpacitySets { get; } = new();
|
||||
public List<float> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #380 regression (2026-08-11, gate 4): before this fix, NOTHING wired
|
||||
/// element <c>0x1000021B</c> 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 <see cref="ChatOptionsPageController.Bindings.DefaultOpacityCaption"/>/
|
||||
/// <c>ActiveOpacityCaption</c> the same way production resolves it
|
||||
/// (<see cref="ChatOptionsDatCaptions"/> against the live DAT) and reads
|
||||
/// back the ACTUAL built <see cref="UiText"/> 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).
|
||||
/// </summary>
|
||||
[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<UiTemplateListBox>(
|
||||
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
||||
List<UiScrollbar> 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<UiText>(
|
||||
UiElement.FindDescendant(defaultRowRoot, 0x1000021Bu));
|
||||
var activeCaption = Assert.IsType<UiText>(
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>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).</summary>
|
||||
[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<UiTemplateListBox>(
|
||||
layout.FindElement(ChatOptionsPageController.ListBoxElementId));
|
||||
List<UiScrollbar> sliders = CollectScalarSliders(listBox);
|
||||
UiElement defaultRowRoot = FindRowRoot(sliders[0]);
|
||||
|
||||
var defaultCaption = Assert.IsType<UiText>(
|
||||
UiElement.FindDescendant(defaultRowRoot, 0x1000021Bu));
|
||||
Assert.Empty(defaultCaption.LinesProvider());
|
||||
Assert.Null(sliders[0].TooltipText);
|
||||
}
|
||||
|
||||
/// <summary>Walks up from a built slider leaf to the row root
|
||||
/// <see cref="UiTemplateListBox.AddPrebuiltRow"/> stacked directly under
|
||||
/// the viewport — the same subtree <see cref="ChatOptionsPageController.BuildOpacitySliders"/>
|
||||
/// itself scopes its <c>FindDescendant</c> caption lookup to.</summary>
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -293,6 +293,43 @@ public sealed class OptionsPanelLiveMountProbeTests
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>#380 (gate 4): exercises the PRODUCTION
|
||||
/// <see cref="ChatOptionsDatCaptions.TryRead"/> 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 (<c>ChatOptionsPageControllerTests</c>) only assume
|
||||
/// via a fake.</summary>
|
||||
[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));
|
||||
}
|
||||
|
||||
/// <summary>#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.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue