acdream/src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs
Erik f5ac1742ba feat(ui): Campaign OP slice OP6 — the Config tab
Binds the retail Options panel's Config tab (LayoutDesc 0x21000029, 27
authored rows across 6 sections) through OP2's template mechanism and
OP3's per-page OptionPage model, matching the Character/Chat tab
controllers' established pattern.

The row table is transcribed directly from two decompiled sources —
gmConfigUI::InitOptions @0x0049E400 (row order, widget shape, defaults)
and gmClient::InitUIPreferences @0x004035b0 (the complete
UIPreferences::AttachPreference registration: every label/tooltip key,
every slider's real-unit range, every menu's enum choices) — which
resolves the research docs' own "U4" unverified slider-caption pairing:
retail ships ZERO range captions on this tab (every SetSliderLabel call
passes literal string id 0).

Consumer disposition: LIVE — Sound/Ambient volume-trio sliders and their
toggle halves (AudioSettings.SfxDisabled/AmbientDisabled now gate the
already-live engine write; RuntimeSettingsController.SaveAudio newly
pushes into OpenAlAudioEngine on every change, not just at startup),
Resolution/Full Screen (immediate window resize on save). NEXT-LAUNCH
(pre-existing precedent): Sync To Refresh, Field of View. STORE-ONLY
(register rows AP-198/199/200, TS-74 extended): Sound Features/Interface
trio/Play-Only-When-Active, the nine Graphics/Rendering-Quality rows
(Vulkan has no per-feature render knobs), Camera/Input's six rows and
Use Mouse Turning (no persistent mouse-turning camera mode), Chat Font
Face/Size (distinct new fields from the existing live ChatSettings.FontSize).

AudioSettings/DisplaySettings/CameraTurningSettings/ChatSettings each
gain new fields for their slice of the 27 rows, backed by SettingsStore
round-trips. A real bug caught by testing: the scrollbar scope lookup
used the standalone-layout root id (0x100001FF), which does not survive
base-merge into the host-mounted tree — fixed to scope from the tab
host's own page-slot id (0x10000213), matching Chat's established
pattern for the same shared-scrollbar-id hazard (0x10000201, authored by
both the Chat and Config ListBoxes).

30 new tests (27 authored rows register as 30 IOptionRow instances — the
three toggle+slider trios each register two). Full Release suite:
13,107 passed / 4 skipped / 0 failed (was 13,083/4/0 — net +24, the one
existing RuntimeSettingsControllerTests case updated for SaveAudio's new
live-apply call, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 07:16:35 +02:00

129 lines
7.5 KiB
C#

namespace AcDream.UI.Abstractions.Panels.Settings;
/// <summary>
/// Chat-related preferences persisted to <c>settings.json</c>. Mixes
/// retail's <c>CharacterOptions2</c> chat-channel filter bits (Hear*Chat
/// + TimeStamp + FilterLanguage + AppearOffline) with a few visual
/// preferences (font size) that don't have a retail bitfield.
/// See <c>docs/research/named-retail/acclient.h:3451+</c> for the
/// retail bit values.
///
/// <para>
/// <b>Campaign CH slice CH3 (2026-08-09):</b> these five Hear*Chat flags
/// are no longer local-only display filters — they ARE retail's server-side
/// Turbine room membership. The App host seeds this draft from
/// <c>RuntimeCharacterOptionsState.Options2</c> (already parsed out of
/// PlayerDescription) whenever a fresh description lands, and Save publishes
/// any changed bit through <c>SetSingleCharacterOption (0x0005)</c> —
/// <c>RuntimeSettingsController.SyncChatFromServerOptions</c> /
/// <c>SaveChat</c>. There is no sixth toggle for
/// <c>HearAllegianceChat (CharacterOptions1 0x40000000)</c>: retail's own
/// Settings UI does not expose one either — Allegiance chat membership rides
/// allegiance membership, not a standalone preference.
/// </para>
///
/// <para>
/// N5 (CH3 Opus review, 2026-08-09): the server-backed sync/publish
/// wiring above covers ONLY the five Hear*Chat fields.
/// <see cref="AppearOffline"/>, <see cref="ShowTimestamps"/>, and
/// <see cref="FilterProfanity"/> each correspond to a real retail
/// <c>CharacterOptions2</c> bit (per the field comments below) but are
/// deliberately NOT wired to <c>SyncChatFromServerOptions</c> or
/// <c>SaveChat</c>'s <c>SetSingleCharacterOption</c> publish — they stay
/// local-only display preferences. Do not extend the sync/publish pair to
/// them without a corresponding design decision; today they are read and
/// written from <c>settings.json</c> alone.
/// </para>
/// </summary>
public sealed record ChatSettings(
// CharacterOptions2 (32-bit) channel filters.
bool HearGeneralChat, // 0x100 — General channel
bool HearTradeChat, // 0x200 — Trade channel
bool HearLFGChat, // 0x400 — LFG channel
bool HearRoleplayChat, // 0x800 — RP channel
bool HearSocietyChat, // 0x80000 — Society chat (CD/EW/RB)
bool AppearOffline, // 0x1000 — hide /who status
bool ShowTimestamps, // 0x40 — TimeStamp prefix on chat lines
bool FilterProfanity, // 0x20000 — FilterLanguage (Turbine's profanity filter)
// Visual / UX (no retail bitfield).
float FontSize, // chat panel font, 10..20 pt
// Campaign CH slice CH6b: local-only persistence of the four floating
// chat windows' 64-bit text-type filters
// (AcDream.Core.Chat.ChatWindowState — retail's per-window
// 0x1000007F option, docs/research/2026-08-09-chat-retail-color-table.md
// §4). Retail persists these server-side inside the opaque
// 0x1000008C GameplayOptions blob (window-shell research doc §4.4);
// acdream has no writer for that blob yet, so these fields are the
// interim local store, with default values matching retail's own
// ChatInterface::PostInit @0x004F3DD0 seed exactly. Trailing with
// defaults so no existing positional/named ChatSettings construction
// site needed to change.
ulong ChatWindow1Filter = 0x0000101Cu, // Speech, Tell, Speech_Direct_Send, Emote
ulong ChatWindow2Filter = 0x00040C00u, // Social, Social_Send, Allegiance
ulong ChatWindow3Filter = 0x00080000u, // Fellowship
ulong ChatWindow4Filter = 0x78000000u, // Turbine General/Trade/LFG/Roleplay
// Campaign OP slice OP5: the MAIN chat window's own filter — CH6a/b's
// ChatWindowState.SetFilter already accepted window id 0 as a real write
// (its class doc: "Retail's main window IS user-settable through the
// options page; only the settings UI to drive this is future work") but
// nothing persisted it until the Chat tab's main-window filter block
// shipped. Default matches ChatWindowState.MainWindowDefaultFilter.
ulong ChatWindowMainFilter = 0xFBFFFFFFu,
// Campaign CH slice CH6c: retail's two GLOBAL window-opacity options
// (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property
// 0x10000081, docs/research/2026-08-09-chat-retail-window-shell.md §3).
// DefaultOpacity applies while a window's descendant does NOT have keyboard
// focus; ActiveOpacity while it does. Always active >= default — enforced by
// ChatOpacityLink at every setter, not by clamping here. acdream applies this
// GLOBALLY to every RetailWindowManager-registered window (register row
// AP-190), where retail scopes it to ChatInterface-derived windows only.
//
// CH6c review fix (2026-08-10): the base ChatInterface ctor's 0.5/1.0 pair
// is retail-correct ONLY for the four floating chat windows —
// gmMainChatUI overrides to 1.0/1.0 (0x004CD0F0), and every other
// RetailWindowManager-registered window (radar, vitals, toolbar, ...) has
// no retail opacity fade at all, so applying 0.5 to them out of the box
// rendered the whole registered UI half-transparent forever, including
// several windows that can never take keyboard focus and so were
// PERMANENTLY stuck at 0.5. DefaultOpacity now ships 1.0, matching
// retail-identical opaque presentation for the 11 non-chat windows and
// the main chat window; the four floating chat windows lose their
// retail 0.5-while-idle fade by default, but the Settings → Chat
// transparency slider remains fully user-settable (AP-190).
float DefaultOpacity = 1.0f,
float ActiveOpacity = 1.0f,
// Campaign OP slice OP6: the Config tab's "UI Options" section —
// UI_ChatFontFace (retail Windows TrueType face name, enum choices
// starting "Arial") and UI_ChatFontSize (index into
// Tiny/Small/Medium/Large/XLarge, gmClient::InitUIPreferences
// @0x0040387b/@0x00403a1a). Deliberately NOT the same field as
// FontSize above: FontSize is acdream's own live 10..20pt render
// knob with no verified index-to-point mapping to retail's five-tier
// enum, and acdream's text rendering has no arbitrary system-font-face
// swap (DAT-baked/bitmap fonts only) — both new fields are honest
// store-only round-trips (register row, OP6).
int ChatFontFace = 2,
int ChatFontSizeIndex = 1)
{
/// <summary>
/// N4 (CH3 Opus review): matches ACE's ACTUAL
/// <c>CharacterOptions2.Default (0x00948700)</c> stance for the five
/// server-backed Hear*Chat bits — General/Trade/LFG are on, but
/// Roleplay (<c>0x800</c>) and Society (<c>0x80000</c>) are OFF (research
/// doc §5.2). The prior "matching the retail 'all on' stance" comment
/// here was wrong: two of the five synced flags are off by default. The
/// server reseed at login (<c>SyncChatFromServerOptions</c>) remains
/// authoritative regardless of this constant — this is only the
/// pre-login / never-connected starting value.
/// </summary>
public static ChatSettings Default { get; } = new(
HearGeneralChat: true,
HearTradeChat: true,
HearLFGChat: true,
HearRoleplayChat: false,
HearSocietyChat: false,
AppearOffline: false,
ShowTimestamps: true,
FilterProfanity: true,
FontSize: 12f);
}