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:
Erik 2026-08-11 04:31:34 +02:00
parent 7e9f372ce8
commit 22b86b9ff4
25 changed files with 1674 additions and 41 deletions

View file

@ -596,7 +596,11 @@ internal sealed class FrameRootCompositionPhase
session.LocalPlayerFrame,
session.LiveSpatialReconciler,
new AcDream.App.Combat.CombatCameraTargetSource(
d.Settings,
// D7 Group-C re-point (Campaign OP OP4): server bit, not
// the client-local GameplaySettings record — see
// CharacterOptionCombatSettingsSource's doc comment.
new AcDream.App.Combat.CharacterOptionCombatSettingsSource(
d.Runtime.CharacterOwner.Options),
d.Combat,
d.Selection,
live.SelectionQuery));

View file

@ -283,7 +283,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
InteractionRetainedUiDependencies d,
DeferredSelectionUiAuthority selection) =>
d.CombatTargetOperations.BindOwned(new LiveCombatTargetOperations(
autoTarget: () => d.Settings.Gameplay.AutoTarget,
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11): server
// bit — see CharacterOptionCombatSettingsSource's doc comment.
autoTarget: () => d.Character.Options.GetOptionBit(
CharacterOptionId.AutoTarget),
selectClosestTarget: () =>
selection.SelectClosestCombatTarget(showToast: false)));
@ -474,7 +477,15 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
inputCapture = d.RetainedInputCapture.Bind(host.Root);
checkpoint(InteractionRetainedUiCompositionPoint.InputCaptureBound);
host.Root.UiLocked = d.Settings.Gameplay.LockUI;
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11): server
// bit at mount time — see CharacterOptionCombatSettingsSource's
// doc comment. Live toggling afterward still flows through
// RuntimeSettingsController.SetUiLocked's existing
// _runtimeTargets?.ApplyUiLock push (ToggleUiLock's Bindings
// site, LiveSessionRuntimeFactory.cs, now also sends the wire
// bit — see its own comment).
host.Root.UiLocked = d.Character.Options.GetOptionBit(
CharacterOptionId.LockUI);
var cursorFeedback = new CursorFeedbackController(
itemInteraction,
@ -673,7 +684,14 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
VividTarget: new VividTargetRuntimeBindings(
d.Actions.Selection,
() => d.PlayerIdentity.ServerGuid,
() => d.Settings.Gameplay.VividTargetingIndicator,
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11):
// server bit, not the client-local GameplaySettings
// record — CH3 precedent (server-authoritative,
// local-write-then-send; the Character-tab panel row
// writes through RuntimeCharacterOptionsState.
// TrySetOption before this read can observe it).
() => d.Character.Options.GetOptionBit(
CharacterOptionId.VividTargetingIndicator),
late.Selection.ResolveVividTargetInfo,
late.SelectionCamera.UiSnapshot),
Indicators: new IndicatorRuntimeBindings(
@ -838,7 +856,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
DisplayMouseTurningMacroLine: text =>
d.Communication.AddText(text, RetailLogTextType.Magic),
LoadCameraTurning: d.Settings.LoadCameraTurning,
SaveCameraTurning: d.Settings.SaveCameraTurning),
SaveCameraTurning: d.Settings.SaveCameraTurning,
// Campaign OP slice OP4 (2026-08-11): the Character-tab
// panel's row-seed source.
CurrentCharacterOption: id => d.Character.Options.GetOptionBit(id)),
StackSplitQuantity: d.StackSplitQuantity,
Plugins: d.UiRegistry,
Persistence: persistence,

View file

@ -19,6 +19,7 @@ using AcDream.App.World;
using AcDream.Core.Audio;
using AcDream.Core.Items;
using AcDream.Core.Lighting;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
@ -27,6 +28,7 @@ using AcDream.Core.Vfx;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.World;
using AcDream.UI.Abstractions.Panels.SpewBox;
using DatReaderWriter;
@ -83,6 +85,8 @@ internal sealed record LivePresentationDependencies(
public RuntimeLocalPlayerMovementState PlayerController =>
Runtime.MovementOwner;
public RuntimeCharacterState Character => Runtime.CharacterOwner;
}
internal sealed record LivePresentationResult(
@ -833,8 +837,14 @@ internal sealed class LivePresentationCompositionPhase
playerYawRadians: () => d.PlayerController.Controller?.Yaw ?? 0f,
playerCellId: () => d.PlayerController.Controller?.CellId ?? 0u,
selectedGuid: () => d.Selection.SelectedObjectId,
coordinatesOnRadar: () => d.Settings.Gameplay.CoordinatesOnRadar,
uiLocked: () => d.Settings.Gameplay.LockUI,
// D7 Group-C re-point (Campaign OP OP4, 2026-08-11): server
// bit, not the client-local GameplaySettings record — see
// CharacterOptionCombatSettingsSource's doc comment
// (AcDream.App.Combat).
coordinatesOnRadar: () => d.Character.Options.GetOptionBit(
CharacterOptionId.CoordinatesOnRadar),
uiLocked: () => d.Character.Options.GetOptionBit(
CharacterOptionId.LockUI),
spatialQuery: () => worldState);
bindings.Adopt(
"radar snapshot",

View file

@ -767,7 +767,10 @@ internal sealed class SessionPlayerCompositionPhase
live.LiveEntities,
d.EntityObjects.Objects,
d.PlayerIdentity),
d.Settings,
// D7 Group-C re-point (Campaign OP OP4): server bit,
// not the client-local GameplaySettings record — see
// CharacterOptionCombatSettingsSource's doc comment.
new CharacterOptionCombatSettingsSource(d.Character.Options),
d.PlayerController,
d.PlayerOutbound,
liveSessionSource,