feat(ui): Campaign OP slice OP4 — the Character tab
Binds LayoutDesc 0x21000028 (gmCharacterSettingsUI) through OP2's template-list mechanism and OP3's OptionPage model: 6 authored group headers + 50 toggle rows (49 from the 2013 build + D3's "Listen to PK death messages", AP-193) in research doc §2's authored order, each row resolved by PlayerOption id through CharacterOptionTable, seeded from live RuntimeCharacterOptionsState, defaulted from CharacterOptionTable. ClientDefault (byte-verified against UIOption_Checkbox::SetPlayerOption @0x00486e80's own GetDefaultOptionValue call — AP-194 updated to confirm the directive was followed), labels/tooltips resolved by name from string table 0x23000003 (never hard-coded English), and registered with OptionsPanelController.CharacterPage. Apply/Reset/Defaults (0x100001FC/FD/FE) are now wired per-page via a scoped subtree search (UiElement.FindDescendant, promoted from UiTabPanel) since Character/ Chat/Config each author their own physical instance under the SAME element ids. Consumers: Group A (29 ids) wire+store only via the existing SetSingleCharacterOptionRuntimeCmd/TrySetOption seam. Group B: Display Timestamps prefixes new transcript lines (RuntimeCommunicationState. DisplayTimestampsSource); Disable Distance Fog forces FogMode.Off (WeatherSystem.DisableDistanceFogSource, retiring half of TS-73); Run as Default Movement inverts the walk-mode modifier's default (RuntimeLocalPlayerMovementState.RunAsDefaultMovementSource). Group C re-points AutoTarget/AutoRepeatAttack/ViewCombatTarget (CharacterOptionCombatSettingsSource), VividTargetingIndicator/ CoordinatesOnRadar/LockUI/AcceptLootPermits from the client-local GameplaySettings record to the canonical server bit — closing two previously-unfiled divergences where AutoRepeatAttack and AcceptCorpseLootingPermissions never reached the wire despite being retail auto-save ids. TS-73 narrowed to its two still-open cases; TS-75..TS-80 file the genuine gaps (no day/night force, no weather- particle/profanity-filter/salvage/housing/pickup-preference subsystem, fellowship-create's unaudited client-sourced field) rather than inventing stand-ins. Conformance: CharacterOptionsPageControllerTests pins all 50 rows against CharacterOptionTable in both directions (an invented or dropped row fails the build), the authored group/order row-by-row, and the build/seed/Apply/Reset/Defaults/wire-publish behavior end-to-end against the committed fixture. 52 new tests; full solution suite 13,008 passed / 4 skipped / 0 failed (was 12,956/4/0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e9f372ce8
commit
22b86b9ff4
25 changed files with 1674 additions and 41 deletions
|
|
@ -67,6 +67,17 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
private const uint UseMouseTurningSettingsId = 0x100005CCu;
|
||||
private const uint ExitGameId = 0x10000617u;
|
||||
|
||||
// Apply/Reset/Defaults — research doc §3.1/§10.1: identical geometry
|
||||
// AND identical element ids on every page that has them (Character,
|
||||
// Chat, Config — NOT Gameplay). Each page's own LayoutDesc authors its
|
||||
// OWN physical button instances under these SAME numeric ids, so a
|
||||
// flat layout.FindElement lookup cannot reliably pick one page's
|
||||
// instance — Campaign OP OP4 wires each page's copy from a SCOPED
|
||||
// search rooted at that page's own slot (UiElement.FindDescendant).
|
||||
private const uint ApplyButtonId = 0x100001FCu;
|
||||
private const uint ResetButtonId = 0x100001FDu;
|
||||
private const uint DefaultsButtonId = 0x100001FEu;
|
||||
|
||||
/// <summary>Callback delegates this controller wires the seven Gameplay
|
||||
/// buttons and the close button to. Every field maps to exactly one
|
||||
/// button; a null field leaves that button INERT (authored, clickable,
|
||||
|
|
@ -191,9 +202,39 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
BindButton(layout, ReportAbuseId,
|
||||
() => callbacks.DisplaySystemMessage(callbacks.ReportAbuseMessage));
|
||||
|
||||
// Apply/Reset/Defaults — retail's gmCharacterSettingsUI /
|
||||
// gmChatOptionsUI / gmConfigUI ::ListenToElementMessage
|
||||
// @0x0049E3A0 (COMDAT-folded — literally the SAME handler body on
|
||||
// all three pages, structure doc §3.1): idElement == 0x100001FC ->
|
||||
// SaveCurrentValues (Apply); 0x100001FD -> RestoreSavedValues
|
||||
// (Reset); 0x100001FE -> RestoreDefaultValues (Defaults). Each
|
||||
// page's OWN OptionPage model owns the actual semantics
|
||||
// (OptionPageModel.cs); this loop only wires each page's physical
|
||||
// button instances to its own model.
|
||||
foreach (uint pageId in new[] { CharacterPageId, ChatPageId, ConfigPageId })
|
||||
{
|
||||
OptionPage page = controller._pages[pageId];
|
||||
UiElement? pageRoot = UiElement.FindDescendant(tabPanel, pageId);
|
||||
if (pageRoot is null) continue;
|
||||
|
||||
BindPageButton(pageRoot, ApplyButtonId, page.Apply);
|
||||
BindPageButton(pageRoot, ResetButtonId, page.Reset);
|
||||
BindPageButton(pageRoot, DefaultsButtonId, page.Defaults);
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static void BindPageButton(UiElement pageRoot, uint elementId, Action onClick)
|
||||
{
|
||||
if (UiElement.FindDescendant(pageRoot, elementId) is UiButton button)
|
||||
button.OnClick = onClick;
|
||||
else
|
||||
Console.WriteLine(
|
||||
$"[D.2b] OptionsPanelController: page 0x{pageRoot.DatElementId:X8}'s button "
|
||||
+ $"0x{elementId:X8} not found — its handler was not wired.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the tab-switching behavior (idempotent — safe even if
|
||||
/// already active). Must run AFTER <see cref="Bind"/> so this
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue