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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
129
src/AcDream.App/UI/Layout/ChatOptionsDatCaptions.cs
Normal file
129
src/AcDream.App/UI/Layout/ChatOptionsDatCaptions.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// #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 <c>UIOption::InqGameplayOptionNameAndTooltip
|
||||
/// @0x004ef750</c>'s own catalog lookup, the mechanism
|
||||
/// <c>UIOption_Slider::SetGameplayOptionProperty @0x00485030</c> calls for
|
||||
/// EVERY <c>UIOption_Slider</c> (both Chat opacity rows go through this path —
|
||||
/// see <c>gmChatOptionsUI::InitOptions</c>'s own <c>SetGameplayOptionProperty</c>
|
||||
/// calls at <c>0x0049fcc0</c>/<c>0x0049fd1a</c>). <c>PlayerOptionPage::AddSliderOption</c>
|
||||
/// itself never sets a row caption (byte-verified — no <c>StringInfo</c> write in
|
||||
/// its pseudo-C body), so the row's name-label child (element <c>0x1000021B</c>,
|
||||
/// shared by BOTH the unlabelled and range-captioned slider templates) is never
|
||||
/// populated by <c>AddSliderOption</c>, 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) — <see cref="ChatOptionsPageController"/>
|
||||
/// never wired ANY source for it, which is #380's root cause.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Byte-verified recipe</b> (a SECOND <c>DBCache::GetDIDFromEnumStatic</c>
|
||||
/// sub-map lookup, sibling to <see cref="ChatOptionsDatDefaults"/>'s own
|
||||
/// <c>(0x16, 2)</c> defaults catalog): <c>InqGameplayOptionNameAndTooltip</c>'s own
|
||||
/// call site is <c>GetDIDFromEnumStatic(&ret, 0x15, 2)</c> — SAME master-map
|
||||
/// key (<c>2</c>) as the defaults lookup, DIFFERENT sub-map key (<c>0x15</c> vs
|
||||
/// <c>0x16</c>) — so <see cref="RetailDataIdResolver.Resolve"/>'s own
|
||||
/// <c>(enumValue, enumCategory)</c> order is <c>(0x15, 2)</c>. Empirically
|
||||
/// confirmed against the installed DATs: resolves to DID <c>0x78000000</c> (a
|
||||
/// DIFFERENT object than the defaults catalog's <c>0x78000001</c> — the two
|
||||
/// enum values genuinely name separate DAT objects, not two views of one). That
|
||||
/// DBProperties has ONE top-level entry, property <c>0xD2</c>, an
|
||||
/// <c>ArrayBaseProperty</c> of <c>StructBaseProperty</c> entries — one per
|
||||
/// registered <c>GameplayOptionProperty</c> in the whole game (not opacity-
|
||||
/// specific). Each entry carries: <c>0xD4</c> = name <c>StringInfo</c>,
|
||||
/// <c>0xD5</c> = tooltip <c>StringInfo</c>, <c>0xD6</c> = the
|
||||
/// <c>GameplayOptionProperty</c> id this entry describes (an
|
||||
/// <c>EnumBaseProperty</c>), <c>0xD7</c>/<c>0xD8</c> = unrelated floats. The
|
||||
/// live array has exactly two entries, matching
|
||||
/// <see cref="ChatOptionsDatDefaults.DefaultOpacityPropertyId"/> (<c>0xD6</c>
|
||||
/// = <c>0x10000080</c>) and <c>ActiveOpacityPropertyId</c> (<c>0xD6</c> =
|
||||
/// <c>0x10000081</c>) — resolving via table <c>0x2300000D</c> (the SAME
|
||||
/// TextFilter-family table #372 already established as this campaign's
|
||||
/// runtime-string home, not the compiled-symbol table <c>0x23000003</c> the
|
||||
/// section headers use) to <b>"Inactive Opacity"</b> /
|
||||
/// <b>"Active Opacity"</b> — 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").
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ChatOptionsDatCaptions
|
||||
{
|
||||
/// <summary>DAT enum-category key for the FIRST (master-map) lookup — see
|
||||
/// the class doc's byte-verified recipe (shared with
|
||||
/// <see cref="ChatOptionsDatDefaults"/>'s own defaults lookup).</summary>
|
||||
private const uint EnumCategory = 2u;
|
||||
|
||||
/// <summary>DAT enum-value key for the SECOND (sub-map) lookup — the
|
||||
/// name/tooltip catalog, NOT <see cref="ChatOptionsDatDefaults"/>'s
|
||||
/// <c>0x16</c> defaults catalog.</summary>
|
||||
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;
|
||||
|
||||
/// <summary>One resolved name/tooltip pair. Either half may be
|
||||
/// <see langword="null"/> if its own <c>StringInfo</c> failed to resolve —
|
||||
/// the caller renders no text rather than inventing English (matching
|
||||
/// this codebase's uniform degrade discipline).</summary>
|
||||
public readonly record struct Caption(string? Name, string? Tooltip);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the Default/Active opacity sliders' row captions + tooltips
|
||||
/// from the live DAT. Returns <see langword="false"/> only when the
|
||||
/// catalog DID itself does not resolve or is not a <c>DBProperties</c> —
|
||||
/// a missing INDIVIDUAL entry still returns <see langword="true"/> with
|
||||
/// that pair's <see cref="Caption"/> left at its default (both null).
|
||||
/// </summary>
|
||||
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<DBProperties>(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);
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,25 @@ public static class ChatOptionsPageController
|
|||
/// <summary>The slider leaf inside either slider row template's subtree.</summary>
|
||||
private const uint SliderElementId = 0x1000021Cu;
|
||||
|
||||
/// <summary>
|
||||
/// #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
|
||||
/// <see cref="ConfigOptionsPageController"/> already cites for its own
|
||||
/// slider rows sharing this template pool). Retail's
|
||||
/// <c>PlayerOptionPage::AddSliderOption</c> never sets this element's
|
||||
/// text itself (byte-verified — no <c>StringInfo</c> write in its
|
||||
/// pseudo-C body); the caption comes from
|
||||
/// <c>UIOption_Slider::SetGameplayOptionProperty</c>'s own
|
||||
/// <c>InqGameplayOptionNameAndTooltip</c> catalog lookup instead — see
|
||||
/// <see cref="ChatOptionsDatCaptions"/>'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").
|
||||
/// </summary>
|
||||
private const uint SliderLabelElementId = 0x1000021Bu;
|
||||
|
||||
/// <summary>Labelled slider template's low/high range-caption children
|
||||
/// (research doc §1.5's template array; present only on template idx 6).</summary>
|
||||
private const uint SliderRangeMinElementId = 0x1000021Eu;
|
||||
|
|
@ -199,7 +218,17 @@ public static class ChatOptionsPageController
|
|||
float DefaultOpacityDatDefault,
|
||||
float ActiveOpacityDatDefault,
|
||||
Func<int, ulong> CurrentFilter,
|
||||
Action<int, ulong> SetFilter);
|
||||
Action<int, ulong> 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);
|
||||
|
||||
/// <summary>
|
||||
/// 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) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #380: sets one opacity slider row's own name-caption (element
|
||||
/// <see cref="SliderLabelElementId"/>) + tooltip from a pre-resolved
|
||||
/// <see cref="ChatOptionsDatCaptions.Caption"/> — the DAT-catalog
|
||||
/// counterpart of <see cref="SetRangeLabel"/> (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
|
||||
/// <see cref="ChatOptionsDatCaptions.Caption.Name"/> 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 <see cref="ConfigOptionsPageController"/>'s own slider
|
||||
/// rows use), not the caption text.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One per-window filter block: resolves the block template DIRECTLY through
|
||||
/// <paramref name="templateResolver"/> (NOT via
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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