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
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue