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
|
|
@ -841,6 +841,33 @@ public sealed class RuntimeCharacterOptionsState
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read ONE character-option bit, by its linear <c>CharacterOptionId</c>,
|
||||
/// resolved through <see cref="CharacterOptionTable"/> (Campaign OP slice
|
||||
/// OP4, 2026-08-11) — the read counterpart to <see cref="SetOptionBit"/>.
|
||||
/// The single seam every OP4 Character-tab row and every re-pointed
|
||||
/// Group-C consumer reads live server truth through: the panel's
|
||||
/// checkbox seed at mount time, <c>ICombatGameplaySettingsSource</c>'s
|
||||
/// re-pointed AutoTarget/AutoRepeatAttack/ViewCombatTarget,
|
||||
/// VividTargetingIndicator/CoordinatesOnRadar/LockUI/AcceptLootPermits,
|
||||
/// the Distance-Fog poll, and the Run-as-Default-Movement poll all call
|
||||
/// this instead of holding their own stale copy. An id outside the
|
||||
/// table returns <see langword="false"/> (matches retail's own
|
||||
/// unmodeled-id-is-off default — there is no bit to test).
|
||||
/// </summary>
|
||||
public bool GetOptionBit(uint characterOptionId)
|
||||
{
|
||||
if (!CharacterOptionTable.TryGet(characterOptionId, out CharacterOptionTableEntry entry))
|
||||
return false;
|
||||
|
||||
uint word = entry.IsOptions1 ? Options1 : Options2;
|
||||
return (word & entry.Mask) != 0u;
|
||||
}
|
||||
|
||||
/// <summary>Typed overload of <see cref="GetOptionBit(uint)"/>.</summary>
|
||||
public bool GetOptionBit(CharacterOptionId characterOptionId) =>
|
||||
GetOptionBit((uint)characterOptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Set ONE character-option bit locally, by its linear
|
||||
/// <c>CharacterOptionId</c> (the same id carried on the wire by
|
||||
|
|
|
|||
|
|
@ -95,6 +95,22 @@ public sealed class RuntimeCommunicationState : IDisposable
|
|||
/// </summary>
|
||||
public SpewBoxState SpewBox { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP4 (2026-08-11): retail <c>PlayerOption
|
||||
/// DisplayTimeStamps</c> — <c>ClientSystem::AddTextToScroll
|
||||
/// @0x00563C50</c> (character-options-map.md §2.2, "Display
|
||||
/// Timestamps") prefixes each transcript line with
|
||||
/// <c>PlayerModule::m_TimeStampFormat</c> (ctor default the byte-
|
||||
/// verified CRT strftime string <c>"%#H:%M:%S "</c> — non-zero-padded
|
||||
/// 24h hour, then zero-padded minute:second, trailing space) when the
|
||||
/// option is on. Polled — see <see cref="AcDream.Core.World.
|
||||
/// WeatherSystem.DisableDistanceFogSource"/> for why. Applied only to
|
||||
/// the persisted <see cref="Chat"/> transcript (matching retail's own
|
||||
/// <c>ClientLocal</c> exemption in <see cref="AddText"/> below — the
|
||||
/// transient <see cref="SpewBox"/> is never timestamped).
|
||||
/// </summary>
|
||||
public Func<bool>? DisplayTimestampsSource { get; set; }
|
||||
|
||||
public ChatCommandTargetState CommandTargets { get; }
|
||||
public TurbineChatState TurbineChat { get; }
|
||||
public FriendsState Friends { get; }
|
||||
|
|
@ -200,6 +216,14 @@ public sealed class RuntimeCommunicationState : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
// OP4: DisplayTimeStamps — see DisplayTimestampsSource's doc
|
||||
// comment. Prefixed AFTER the trim above (retail's own order:
|
||||
// AddTextToScroll trims first, then ClientSystem's caller-side
|
||||
// timestamp prepend applies to the transcript line, never to the
|
||||
// ClientLocal/SpewBox branch already returned above).
|
||||
if (DisplayTimestampsSource?.Invoke() == true)
|
||||
text = DateTime.Now.ToString("H:mm:ss ") + text;
|
||||
|
||||
Chat.OnSystemMessage(text, (uint)type);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,26 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
}
|
||||
|
||||
public bool AutoRunActive => _autoRunActive;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP4 (2026-08-11): retail <c>PlayerOption
|
||||
/// RunAsDefaultMovement</c> (<c>ACCmdInterp::UITogglesRun</c>) —
|
||||
/// whether an ordinary held movement key runs by default (retail's own
|
||||
/// client default: <see langword="true"/>) or walks by default,
|
||||
/// requiring the walk-mode modifier to be held to invert. Distinct
|
||||
/// from <see cref="AutoRunActive"/>, which is the <c>ToggleRunLock</c>
|
||||
/// KEYBIND LATCH (auto-walk-forward-without-holding-W), an unrelated
|
||||
/// acdream feature with no retail <c>PlayerOption</c> behind it. Polled
|
||||
/// (not pushed) — see <see cref="AcDream.Core.World.WeatherSystem.
|
||||
/// DisableDistanceFogSource"/> for why. Null (the default) means the
|
||||
/// option was never bound; <see cref="RunAsDefaultMovement"/> then
|
||||
/// reports retail's own default (<see langword="true"/>), matching
|
||||
/// acdream's pre-OP4 hardcoded behavior exactly.
|
||||
/// </summary>
|
||||
public Func<bool>? RunAsDefaultMovementSource { get; set; }
|
||||
|
||||
public bool RunAsDefaultMovement => RunAsDefaultMovementSource?.Invoke() ?? true;
|
||||
|
||||
public bool HasCommandInput => _hasCommandInput;
|
||||
public MovementInput CommandInput => _commandInput;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue