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); } }