refactor(settings): OP9 — retire the dead F11 settings surface + fully-superseded GameplaySettings
Campaign OP slice OP9 code retirement (docs/plans/2026-08-10-options-panel-campaign.md
§OP9). The retail four-tab Options panel (OP1-OP8) is now acdream's one
in-client settings surface (D1) — this commit deletes the pre-retail-UI
surfaces it fully superseded. Pure retirement: no behavior change to
anything live, verified by dispositioning every one of the 15 src files
and 8 test files that referenced the deleted types before touching any
of them.
1. SettingsVM + SettingsPanel (the old F11 IPanel surface, unrendered
since Campaign V slice V11's ImGui deletion) deleted outright, plus
their two dedicated test files. IPanel/IPanelRenderer stay — ChatPanel/
DebugPanel/VitalsPanel still implement IPanel, so the contract does NOT
become unreferenced.
2. RuntimeSettingsController's SettingsVM binding seam deleted:
CreateViewModel, CreateViewModelBinding, RuntimeSettingsViewModelBinding,
the _viewModel field, UnbindViewModel, and every _viewModel? notification
call (ToggleFrameRate, LoadCharacterContext, RestoreDefaultCharacterContext,
SyncChatFromServerOptions). CreateViewModelBinding had zero production
callers (test-only). HasDraftPreview/DisplayPreview/AudioPreview stay on
IRuntimeSettingsPreviewSource (WorldRenderFrameBuilder and
SettingsParticleRangeSource still consume it) but now trivially mirror
the committed Display/Audio snapshot — HasDraftPreview was already
always false in production. GameWindowLifetime's matching
"settings view model" shutdown stage is deleted.
3. IDevToolsGameplayCommands + DevToolsGameplayCommands deleted from
GameplayInputCommandController.cs. All three members were dead:
ToggleSettingsPanel() had ZERO dispatch sites (ToggleOptionsPanel always
routed to _retained, never to _devTools); ToggleDebugPanel()/
FocusChatInput() had dispatch sites (F1/Ctrl+F1, Tab) but empty no-op
bodies (their ImGui DebugPanel/ChatPanel targets were already gone).
The two live dispatch sites are kept as inert `return true;` cases
(still consuming the key, matching the prior no-op's "handled"
contract) instead of falling through to a lower-priority scope.
SessionPlayerComposition.cs's `new DevToolsGameplayCommands()`
construction is removed. No `ToggleSettingsPanel` InputAction exists
(only `ToggleOptionsPanel`, rebound at OP3) — nothing to remove there
or from KeyBindings.RetailDefaults()/keymap fixtures.
4. GameplaySettings deleted entirely (the type, SettingsStore's
LoadGameplay/SaveGameplay/BuildGameplayObject, RuntimeSettingsController's
Gameplay property/SetAcceptLootPermits). Verified all 13 remaining
members (ToggleRun, AdvancedCombatUI, ShowTooltips,
VividTargetingIndicator, SideBySideVitals, CoordinatesOnRadar,
SpellDuration, AllowGive, ShowHelm, ShowCloak, LockUI, UseMouseTurning,
AcceptLootPermits — the three combat ones already died at OP4/AP-196)
were ALREADY bound through CharacterOptionTable/
CharacterOptionsPageController's server-bit seam at OP4 before deleting
the client-local mirror — no (c)-case genuinely-client-local member
was found; disposition (b) covers 100% of the surface. SetUiLocked
rewritten to compare only against the last value actually pushed to
_runtimeTargets (MUST-FIX 4's guard), with no second store left to
read or write. LiveSessionRuntimeFactory's SetAcceptLootPermits binding
now sends the wire option only (the GameplaySettings write-behind call
removed as dead output). CharacterSettings/DisplaySettings/
AudioSettings/ChatSettings and their SettingsStore Load/Save surfaces
are UNTOUCHED per the campaign contract.
Per-file disposition (15 src + 8 test files that referenced
GameplaySettings before this commit):
- GameplaySettings.cs, SettingsVM.cs, SettingsPanel.cs: the types
themselves — deleted.
- SettingsStore.cs, RuntimeSettingsController.cs,
LiveSessionRuntimeFactory.cs: real usage — API deleted/rewritten.
- RetailUiRuntime.cs, InteractionRetainedUiComposition.cs,
SessionPlayerComposition.cs, CombatUiController.cs,
LiveCombatAttackOperations.cs, LivePresentationComposition.cs,
FrameRootComposition.cs, RuntimeCharacterState.cs,
CombatCameraTargetSource.cs: doc-comment-only or interface-name
substring matches (ICombatGameplaySettingsSource) — left as accurate
historical record, no forward reference to the deleted type.
- Tests: RuntimeSettingsControllerTests.cs and SettingsStoreTests.cs
rewritten (Gameplay-specific tests deleted; SaveDisplay/SaveAudio/
SaveChat tests re-targeted off the now-public methods instead of the
retired SettingsVM draft/Save() indirection); GameplaySettingsTests.cs/
SettingsVMTests.cs/SettingsPanelTests.cs deleted; the remaining three
(CharacterOptionCombatSettingsSourceTests.cs,
CombatCameraTargetSourceTests.cs, LiveCombatAttackOperationsTests.cs)
were comment/interface-name-only, untouched.
5. Register: AP-196 (OP4's partial GameplaySettings retirement, which left
five fields as write-behind mirrors) is fully retired now that the
record is gone outright — marked ~~AP-196~~ RETIRED with its retirement
note, active-row count 143 -> 142. No other row cited the deleted types
directly (AP-194/AP-193 cite CharacterOptionTable.cs, not
GameplaySettings.cs).
6. Settings.json migration honesty: SettingsStore no longer reads or
writes the "gameplay" top-level key, so an existing file carrying one
from a pre-OP9 build is neither parsed nor dropped — the existing
SaveSection raw-JSON-text preservation mechanism (unknown top-level
keys survive every subsequent save) carries it forward untouched.
Two new targeted tests
(LeftoverGameplaySection_FromAnOlderSettingsJson_DoesNotBreakOtherLoads,
LeftoverGameplaySection_SurvivesAnUnrelatedSave) pin this.
InputAction.ToggleOptionsPanel's stale doc comment (still describing the
retired ImGui SettingsPanel) and a handful of other dangling doc
references (DisplaySettings.cs, ChatOpacityLink.cs,
SettingsDevToolsComposition.cs, InputDispatcherCaptureTests.cs) are
reworded to point at the current retail Options panel / OP8
KeyboardConfigController.
Build: dotnet build -c Release green, 0 errors. Tests: dotnet test -c
Release --no-build — 13,075 passed / 4 skipped / 0 failed (13,079 total),
down from the stated baseline of 13,155 passed / 4 skipped / 0 failed
(13,159 total) — the -80 delta is exactly the deleted SettingsVM/
SettingsPanel/GameplaySettings test surface (three whole files plus the
Gameplay-specific cases trimmed from RuntimeSettingsControllerTests.cs/
SettingsStoreTests.cs), with zero regressions elsewhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1a57f96efe
commit
371197a345
22 changed files with 228 additions and 3260 deletions
|
|
@ -1194,7 +1194,6 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
var commands = new GameplayInputCommandController(
|
||||
new RetainedGameplayWindowCommands(
|
||||
interaction.RetainedUi?.Runtime),
|
||||
new DevToolsGameplayCommands(),
|
||||
runtimeDiagnostics,
|
||||
new PlayerModeGameplayCommands(
|
||||
d.PlayerMode,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ namespace AcDream.App.Composition;
|
|||
/// ImGui developer-tools frontend that used to compose here (VitalsPanel,
|
||||
/// ChatPanel, DebugPanel, SettingsPanel via <c>AcDream.UI.ImGui</c>) was
|
||||
/// removed at Campaign V slice V11 along with the OpenGL backend it
|
||||
/// required — see docs/plans/2026-07-27-vulkan-campaign.md. A follow-up
|
||||
/// re-homes the Settings and Debug panels onto the retained UI through a new
|
||||
/// <c>IPanelRenderer</c> implementation; until then keybind remapping falls
|
||||
/// back to editing keybinds.json.
|
||||
/// required — see docs/plans/2026-07-27-vulkan-campaign.md. The
|
||||
/// ImGui-era SettingsPanel's promised re-home landed as Campaign OP's retail
|
||||
/// Options panel (<c>OptionsPanelController</c> et al., the retained
|
||||
/// <c>UiHost</c>/<c>UiRoot</c> tree — D1) instead of a new
|
||||
/// <c>IPanelRenderer</c> implementation, and its OP9 closeout retired the
|
||||
/// unrendered ImGui-era SettingsPanel/SettingsVM outright. Keybind remapping
|
||||
/// is Campaign OP slice OP8's Configure Keyboard screen, persisting to
|
||||
/// keybinds.json (not retail's <c>.keymap</c> format — register row AP-202).
|
||||
/// </summary>
|
||||
internal sealed record SettingsDevToolsResult(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings ResolvedQuality);
|
||||
|
|
|
|||
|
|
@ -41,37 +41,6 @@ internal sealed class RetainedGameplayWindowCommands(RetailUiRuntime? runtime)
|
|||
_runtime?.ToggleWindow(WindowNames.Options);
|
||||
}
|
||||
|
||||
internal interface IDevToolsGameplayCommands
|
||||
{
|
||||
void ToggleDebugPanel();
|
||||
|
||||
void FocusChatInput();
|
||||
|
||||
void ToggleSettingsPanel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ImGui developer-tools frontend these commands used to forward to was
|
||||
/// removed at Campaign V slice V11. A follow-up re-homes the Settings and
|
||||
/// Debug panels onto the retained UI through a new <c>IPanelRenderer</c>
|
||||
/// implementation; until then these are no-ops so the frozen gameplay-action
|
||||
/// priority graph keeps a single, always-valid handoff target.
|
||||
/// </summary>
|
||||
internal sealed class DevToolsGameplayCommands : IDevToolsGameplayCommands
|
||||
{
|
||||
public void ToggleDebugPanel()
|
||||
{
|
||||
}
|
||||
|
||||
public void FocusChatInput()
|
||||
{
|
||||
}
|
||||
|
||||
public void ToggleSettingsPanel()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IPlayerModeGameplayCommands
|
||||
{
|
||||
bool IsPlayerMode { get; }
|
||||
|
|
@ -163,7 +132,6 @@ internal interface IGameplayInputCommandTarget
|
|||
internal sealed class GameplayInputCommandController : IGameplayInputCommandTarget
|
||||
{
|
||||
private readonly IRetainedGameplayWindowCommands _retained;
|
||||
private readonly IDevToolsGameplayCommands _devTools;
|
||||
private readonly IRuntimeDiagnosticCommands _diagnostics;
|
||||
private readonly IPlayerModeGameplayCommands _playerMode;
|
||||
private readonly IItemTargetModeCommands _targetMode;
|
||||
|
|
@ -175,7 +143,6 @@ internal sealed class GameplayInputCommandController : IGameplayInputCommandTarg
|
|||
|
||||
public GameplayInputCommandController(
|
||||
IRetainedGameplayWindowCommands retained,
|
||||
IDevToolsGameplayCommands devTools,
|
||||
IRuntimeDiagnosticCommands diagnostics,
|
||||
IPlayerModeGameplayCommands playerMode,
|
||||
IItemTargetModeCommands targetMode,
|
||||
|
|
@ -186,7 +153,6 @@ internal sealed class GameplayInputCommandController : IGameplayInputCommandTarg
|
|||
Action? toggleAudioMute = null)
|
||||
{
|
||||
_retained = retained ?? throw new ArgumentNullException(nameof(retained));
|
||||
_devTools = devTools ?? throw new ArgumentNullException(nameof(devTools));
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||
_targetMode = targetMode ?? throw new ArgumentNullException(nameof(targetMode));
|
||||
|
|
@ -224,7 +190,14 @@ internal sealed class GameplayInputCommandController : IGameplayInputCommandTarg
|
|||
_toggleAudioMute?.Invoke();
|
||||
return true;
|
||||
case InputAction.AcdreamToggleDebugPanel:
|
||||
_devTools.ToggleDebugPanel();
|
||||
// OP9: IDevToolsGameplayCommands.ToggleDebugPanel() retired —
|
||||
// its ImGui-era DebugPanel target was already gone (Campaign V
|
||||
// slice V11) and no retained-UI replacement exists, so the
|
||||
// no-op body it forwarded to carried no behavior to preserve.
|
||||
// The key is still consumed here (matching that no-op's
|
||||
// "handled" contract) rather than falling through to whatever
|
||||
// a lower-priority input scope would otherwise do with
|
||||
// F1/Ctrl+F1.
|
||||
return true;
|
||||
case InputAction.AcdreamToggleFlyMode:
|
||||
_playerMode.ToggleFlyOrChase();
|
||||
|
|
@ -233,15 +206,20 @@ internal sealed class GameplayInputCommandController : IGameplayInputCommandTarg
|
|||
_playerMode.TogglePlayerMode();
|
||||
return true;
|
||||
case InputAction.ToggleChatEntry:
|
||||
_devTools.FocusChatInput();
|
||||
// OP9: IDevToolsGameplayCommands.FocusChatInput() retired —
|
||||
// same shape as AcdreamToggleDebugPanel above (its ImGui
|
||||
// ChatPanel target was already gone). Tab is still consumed
|
||||
// here, matching the prior no-op's "handled" contract.
|
||||
return true;
|
||||
case InputAction.ToggleOptionsPanel:
|
||||
// Campaign OP slice OP3 (D1): F11 opens the RETAIL Options
|
||||
// panel now, not the old (unrendered since Campaign V slice
|
||||
// V11 — DevToolsGameplayCommands' own doc) ImGui-era Settings
|
||||
// panel. IDevToolsGameplayCommands.ToggleSettingsPanel() is a
|
||||
// SEPARATE action retired in OP9; its no-op wiring elsewhere
|
||||
// is untouched by this change (#358's lesson).
|
||||
// V11) ImGui-era Settings panel. That panel's own
|
||||
// IDevToolsGameplayCommands.ToggleSettingsPanel() member was
|
||||
// never dispatched from anywhere (this switch always routed
|
||||
// ToggleOptionsPanel to _retained, never to _devTools) and
|
||||
// was retired outright at OP9 along with the rest of
|
||||
// IDevToolsGameplayCommands (#358's lesson).
|
||||
_retained.ToggleOptionsPanel();
|
||||
return true;
|
||||
case InputAction.CombatToggleCombat:
|
||||
|
|
|
|||
|
|
@ -328,8 +328,8 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
// is a ONE-SHOT assignment taken pre-login, at composition
|
||||
// time, from RuntimeCharacterOptionsState's constructor-
|
||||
// default word. Routing the real value through SetUiLocked
|
||||
// here — the exact seam ToggleUiLock/SaveGameplay already
|
||||
// use to push RuntimeSettingsTargets.ApplyUiLock — converges
|
||||
// here — the exact seam ToggleUiLock already uses to push
|
||||
// RuntimeSettingsTargets.ApplyUiLock — converges
|
||||
// the retained window lock to server truth on every fresh
|
||||
// PlayerDescription, matching the radar's own convergence
|
||||
// instead of only updating on the next manual /lockui toggle.
|
||||
|
|
@ -411,8 +411,8 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
// immediately, exactly like the Character-tab panel row.
|
||||
// SetUiLocked still runs too, for its existing
|
||||
// _runtimeTargets?.ApplyUiLock(locked) immediate visual push
|
||||
// (host.Root.UiLocked) and its legacy GameplaySettings mirror
|
||||
// (retired in OP9).
|
||||
// (host.Root.UiLocked) — the legacy GameplaySettings mirror it
|
||||
// used to also maintain was retired outright at OP9.
|
||||
ToggleUiLock: () =>
|
||||
{
|
||||
bool locked = !_domain.Character.Options.GetOptionBit(
|
||||
|
|
@ -491,16 +491,15 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
// wire, even though retail auto-saves this id (0x0005
|
||||
// immediately). Now routes through the SAME
|
||||
// SendSingleCharacterOption seam the Character-tab panel row
|
||||
// uses.
|
||||
// uses. OP9 retired the GameplaySettings mirror this used to
|
||||
// ALSO maintain (RuntimeSettingsController.SetAcceptLootPermits)
|
||||
// — the server bit read below is the sole authority now.
|
||||
AcceptLootPermits: () =>
|
||||
_domain.Character.Options.GetOptionBit(
|
||||
CharacterOptionId.AcceptLootPermits),
|
||||
SetAcceptLootPermits: value =>
|
||||
{
|
||||
SendSingleCharacterOption(
|
||||
(uint)CharacterOptionId.AcceptLootPermits, value);
|
||||
_interaction.Settings.SetAcceptLootPermits(value);
|
||||
},
|
||||
(uint)CharacterOptionId.AcceptLootPermits, value),
|
||||
DisplayConsent: session.SendDisplayConsent,
|
||||
ClearConsent: session.SendClearConsent,
|
||||
RemoveConsent: session.SendRemoveConsent,
|
||||
|
|
|
|||
|
|
@ -414,7 +414,6 @@ internal static class GameWindowShutdownManifest
|
|||
]),
|
||||
new ResourceShutdownStage("physical ingress cleanup",
|
||||
[
|
||||
Soft("settings view model", () => ingress.Settings.UnbindViewModel()),
|
||||
Soft("retained gameplay", () => DisposeRetainedGameplay(ingress.RetainedGameplay)),
|
||||
Soft("gameplay actions", () => DisposeGameplayActions(ingress.GameplayActions)),
|
||||
Soft("retained UI input", ingress.RetailUi.DeactivateInput),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Settings;
|
||||
|
||||
|
|
@ -15,8 +14,6 @@ internal interface IRuntimeSettingsStorage
|
|||
|
||||
AudioSettings LoadAudio();
|
||||
|
||||
GameplaySettings LoadGameplay();
|
||||
|
||||
ChatSettings LoadChat();
|
||||
|
||||
CharacterSettings LoadCharacter(string toonKey);
|
||||
|
|
@ -29,8 +26,6 @@ internal interface IRuntimeSettingsStorage
|
|||
|
||||
void SaveAudio(AudioSettings audio);
|
||||
|
||||
void SaveGameplay(GameplaySettings gameplay);
|
||||
|
||||
void SaveChat(ChatSettings chat);
|
||||
|
||||
void SaveCharacter(string toonKey, CharacterSettings character);
|
||||
|
|
@ -57,8 +52,6 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
|
||||
public AudioSettings LoadAudio() => _store.LoadAudio();
|
||||
|
||||
public GameplaySettings LoadGameplay() => _store.LoadGameplay();
|
||||
|
||||
public ChatSettings LoadChat() => _store.LoadChat();
|
||||
|
||||
public CharacterSettings LoadCharacter(string toonKey) =>
|
||||
|
|
@ -70,9 +63,6 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
|
||||
public void SaveAudio(AudioSettings audio) => _store.SaveAudio(audio);
|
||||
|
||||
public void SaveGameplay(GameplaySettings gameplay) =>
|
||||
_store.SaveGameplay(gameplay);
|
||||
|
||||
public void SaveChat(ChatSettings chat) => _store.SaveChat(chat);
|
||||
|
||||
public void SaveCharacter(string toonKey, CharacterSettings character) =>
|
||||
|
|
@ -85,39 +75,10 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
internal sealed record RuntimeSettingsSnapshot(
|
||||
DisplaySettings Display,
|
||||
AudioSettings Audio,
|
||||
GameplaySettings Gameplay,
|
||||
ChatSettings Chat,
|
||||
CharacterSettings Character,
|
||||
QualitySettings Quality);
|
||||
|
||||
/// <summary>
|
||||
/// Expected-owner lease for the optional developer settings view model.
|
||||
/// Failed optional composition can withdraw only the instance it installed.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeSettingsViewModelBinding : IDisposable
|
||||
{
|
||||
private readonly RuntimeSettingsController _owner;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeSettingsViewModelBinding(
|
||||
RuntimeSettingsController owner,
|
||||
SettingsVM viewModel)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
ViewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
|
||||
}
|
||||
|
||||
public SettingsVM ViewModel { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_owner.UnbindViewModel(ViewModel);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IRuntimeSettingsStartupTarget
|
||||
{
|
||||
void ApplyDisplay(DisplaySettings display);
|
||||
|
|
@ -186,16 +147,15 @@ internal sealed class RuntimeSettingsController :
|
|||
private readonly Func<QualityPreset, QualitySettings> _resolveQuality;
|
||||
private readonly Action<string> _log;
|
||||
private IRuntimeSettingsTargets? _runtimeTargets;
|
||||
private SettingsVM? _viewModel;
|
||||
private CharacterSettings _defaultCharacter;
|
||||
private bool _startupDisplayApplied;
|
||||
private bool _startupAudioApplied;
|
||||
private bool _startupApplied;
|
||||
private bool _uiLockConverged = true;
|
||||
// MUST-FIX 4 (OP4 review-fix round, 2026-08-11, blast M3): the last
|
||||
// `locked` value actually pushed to `_runtimeTargets.ApplyUiLock` —
|
||||
// the guard `SetUiLocked` compares against, decoupled from whatever
|
||||
// `Gameplay.LockUI`'s own persisted/draft snapshot currently holds.
|
||||
// the guard `SetUiLocked` compares against. Originally decoupled from
|
||||
// the retired client-local `GameplaySettings.LockUI` mirror (OP9
|
||||
// deleted that record outright); this is now the ONLY store.
|
||||
private bool? _lastAppliedUiLocked;
|
||||
|
||||
public RuntimeSettingsController(
|
||||
|
|
@ -209,7 +169,6 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
Display = _storage.LoadDisplay();
|
||||
Audio = _storage.LoadAudio();
|
||||
Gameplay = _storage.LoadGameplay();
|
||||
Chat = _storage.LoadChat();
|
||||
_defaultCharacter = _storage.LoadCharacter(DefaultToonKey);
|
||||
Character = _defaultCharacter;
|
||||
|
|
@ -217,7 +176,6 @@ internal sealed class RuntimeSettingsController :
|
|||
Startup = new RuntimeSettingsSnapshot(
|
||||
Display,
|
||||
Audio,
|
||||
Gameplay,
|
||||
Chat,
|
||||
Character,
|
||||
ResolvedQuality);
|
||||
|
|
@ -233,19 +191,24 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
public AudioSettings Audio { get; private set; }
|
||||
|
||||
public GameplaySettings Gameplay { get; private set; }
|
||||
|
||||
public ChatSettings Chat { get; private set; }
|
||||
|
||||
public CharacterSettings Character { get; private set; }
|
||||
|
||||
public QualitySettings ResolvedQuality { get; private set; }
|
||||
|
||||
public bool HasDraftPreview => _viewModel is not null;
|
||||
// OP9: the optional developer-tools draft-preview view model
|
||||
// (SettingsVM) was retired — it had zero production construction
|
||||
// sites (only tests ever called CreateViewModel). HasDraftPreview is
|
||||
// therefore always false in production and DisplayPreview/AudioPreview
|
||||
// always mirror the committed Display/Audio snapshot; the properties
|
||||
// stay on IRuntimeSettingsPreviewSource because WorldRenderFrameBuilder
|
||||
// and SettingsParticleRangeSource still consume the interface.
|
||||
public bool HasDraftPreview => false;
|
||||
|
||||
public DisplaySettings DisplayPreview => _viewModel?.DisplayDraft ?? Display;
|
||||
public DisplaySettings DisplayPreview => Display;
|
||||
|
||||
public AudioSettings AudioPreview => _viewModel?.AudioDraft ?? Audio;
|
||||
public AudioSettings AudioPreview => Audio;
|
||||
|
||||
public void ApplyStartup(IRuntimeSettingsStartupTarget target)
|
||||
{
|
||||
|
|
@ -316,94 +279,23 @@ internal sealed class RuntimeSettingsController :
|
|||
.UnbindRuntimeTargets(_expected);
|
||||
}
|
||||
|
||||
public SettingsVM CreateViewModel(
|
||||
KeyBindings persistedBindings,
|
||||
InputDispatcher dispatcher,
|
||||
Action<KeyBindings> saveBindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(persistedBindings);
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(saveBindings);
|
||||
if (_viewModel is not null)
|
||||
throw new InvalidOperationException("A settings view model is already bound.");
|
||||
|
||||
_viewModel = new SettingsVM(
|
||||
persistedBindings,
|
||||
dispatcher,
|
||||
saveBindings,
|
||||
Display,
|
||||
SaveDisplay,
|
||||
Audio,
|
||||
SaveAudio,
|
||||
Gameplay,
|
||||
SaveGameplay,
|
||||
Chat,
|
||||
SaveChat,
|
||||
Character,
|
||||
SaveCharacter);
|
||||
return _viewModel;
|
||||
}
|
||||
|
||||
public RuntimeSettingsViewModelBinding CreateViewModelBinding(
|
||||
KeyBindings persistedBindings,
|
||||
InputDispatcher dispatcher,
|
||||
Action<KeyBindings> saveBindings) =>
|
||||
new(
|
||||
this,
|
||||
CreateViewModel(persistedBindings, dispatcher, saveBindings));
|
||||
|
||||
public void UnbindViewModel(SettingsVM? expected = null)
|
||||
{
|
||||
if (expected is null || ReferenceEquals(_viewModel, expected))
|
||||
_viewModel = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OP9: rewritten off the retired client-local <c>GameplaySettings.LockUI</c>
|
||||
/// mirror (MUST-FIX 4, OP4 review-fix round, 2026-08-11, blast M3) — the
|
||||
/// guard now compares directly against <see cref="_lastAppliedUiLocked"/>,
|
||||
/// the last value actually pushed to <see cref="_runtimeTargets"/>, with
|
||||
/// no persisted store of its own left to read or write: the server bit
|
||||
/// (<c>RuntimeCharacterOptionsState</c>, read through
|
||||
/// <c>CharacterOptionId.LockUI</c>) is the sole authority, exactly as
|
||||
/// D7's Group-C re-point already made it at OP4.
|
||||
/// </summary>
|
||||
public void SetUiLocked(bool locked)
|
||||
{
|
||||
// MUST-FIX 4 (OP4 review-fix round, 2026-08-11, blast M3): the
|
||||
// guard used to compare `locked` against `Gameplay.LockUI` — valid
|
||||
// only while `ToggleUiLock` computed `locked` AS `!Gameplay.LockUI`
|
||||
// (pre-OP4). OP4 re-pointed `ToggleUiLock` to derive `locked` from
|
||||
// the SERVER bit (`RuntimeCharacterOptionsState.GetOptionBit`)
|
||||
// instead, a DIFFERENT store that can already equal the persisted
|
||||
// `Gameplay.LockUI` without this method ever having pushed that
|
||||
// value to `_runtimeTargets` — the guard must compare against what
|
||||
// was ACTUALLY applied, not a value from an unrelated store.
|
||||
if (_lastAppliedUiLocked == locked && _uiLockConverged)
|
||||
if (_lastAppliedUiLocked == locked)
|
||||
return;
|
||||
|
||||
_uiLockConverged = false;
|
||||
Gameplay = Gameplay with { LockUI = locked };
|
||||
_runtimeTargets?.ApplyUiLock(locked);
|
||||
_lastAppliedUiLocked = locked;
|
||||
_viewModel?.SetGameplay(
|
||||
_viewModel.GameplayDraft with { LockUI = locked });
|
||||
|
||||
try
|
||||
{
|
||||
_storage.SaveGameplay(Gameplay);
|
||||
_viewModel?.ApplyExternalGameplayChange(gameplay => gameplay with
|
||||
{
|
||||
LockUI = locked,
|
||||
});
|
||||
_uiLockConverged = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"settings: radar lock save failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAcceptLootPermits(bool enabled)
|
||||
{
|
||||
Gameplay = Gameplay with { AcceptLootPermits = enabled };
|
||||
_viewModel?.SetGameplay(
|
||||
_viewModel.GameplayDraft with { AcceptLootPermits = enabled });
|
||||
_storage.SaveGameplay(Gameplay);
|
||||
_viewModel?.ApplyExternalGameplayChange(gameplay => gameplay with
|
||||
{
|
||||
AcceptLootPermits = enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -415,10 +307,6 @@ internal sealed class RuntimeSettingsController :
|
|||
public void ToggleFrameRate()
|
||||
{
|
||||
Display = Display with { ShowFps = !Display.ShowFps };
|
||||
_viewModel?.ApplyExternalDisplayChange(display => display with
|
||||
{
|
||||
ShowFps = Display.ShowFps,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -434,7 +322,7 @@ internal sealed class RuntimeSettingsController :
|
|||
/// Campaign OP slice OP3: the five client-local preferences the "Use
|
||||
/// Mouse Turning Settings" Gameplay-tab macro reads/writes. Read
|
||||
/// directly through storage (no startup-snapshot cache, unlike
|
||||
/// <see cref="Display"/>/<see cref="Gameplay"/>/<see cref="Chat"/>) —
|
||||
/// <see cref="Display"/>/<see cref="Chat"/>) —
|
||||
/// this section has no UI surface of its own yet (Campaign OP slice
|
||||
/// OP6's Config tab), so there is nothing today that needs a cached,
|
||||
/// change-notified copy.
|
||||
|
|
@ -468,14 +356,12 @@ internal sealed class RuntimeSettingsController :
|
|||
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
|
||||
ActiveToonKey = characterName;
|
||||
Character = _storage.LoadCharacter(characterName);
|
||||
_viewModel?.LoadCharacterContext(Character);
|
||||
_log($"settings: loaded character[{characterName}] preferences");
|
||||
}
|
||||
|
||||
public void RestoreDefaultCharacterContext()
|
||||
{
|
||||
Character = _defaultCharacter;
|
||||
_viewModel?.LoadCharacterContext(Character);
|
||||
}
|
||||
|
||||
public void ResetActiveCharacterKey() => ActiveToonKey = DefaultToonKey;
|
||||
|
|
@ -500,7 +386,7 @@ internal sealed class RuntimeSettingsController :
|
|||
/// same shape as the already-public <see cref="SaveCameraTurning"/>,
|
||||
/// now also called directly by <c>ConfigOptionsPageController</c>'s
|
||||
/// Display-backed rows (resolution/fullscreen/vsync/FOV/gamma/quality
|
||||
/// family) rather than only through the (soon-retired) SettingsVM
|
||||
/// family) rather than only through the (OP9-retired) SettingsVM
|
||||
/// callback wiring.</summary>
|
||||
public void SaveDisplay(DisplaySettings display)
|
||||
{
|
||||
|
|
@ -540,24 +426,6 @@ internal sealed class RuntimeSettingsController :
|
|||
}
|
||||
}
|
||||
|
||||
private void SaveGameplay(GameplaySettings gameplay)
|
||||
{
|
||||
try
|
||||
{
|
||||
_storage.SaveGameplay(gameplay);
|
||||
Gameplay = gameplay;
|
||||
_uiLockConverged = false;
|
||||
_runtimeTargets?.ApplyUiLock(gameplay.LockUI);
|
||||
_lastAppliedUiLocked = gameplay.LockUI;
|
||||
_uiLockConverged = true;
|
||||
_log($"settings: gameplay saved to {_storage.Location}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"settings: gameplay save failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Widened from private to public at Campaign OP slice OP6,
|
||||
/// same reason as <see cref="SaveDisplay"/> — the Config tab's Chat
|
||||
/// Font Face/Size rows (store-only, no diff against the five wired
|
||||
|
|
@ -580,7 +448,7 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
// CH3 (2026-08-09): retail toggles a Hear*Chat option and pushes
|
||||
// SetSingleCharacterOption (0x0005) in the same step (mirrors
|
||||
// SaveGameplay's ApplyUiLock push above) — ACE's handler both flips
|
||||
// SetUiLocked's ApplyUiLock push above) — ACE's handler both flips
|
||||
// the option AND joins/leaves the matching Turbine room.
|
||||
PublishHearOptionChange(
|
||||
previous.HearGeneralChat, chat.HearGeneralChat,
|
||||
|
|
@ -627,11 +495,10 @@ internal sealed class RuntimeSettingsController :
|
|||
/// </summary>
|
||||
public void SyncChatFromServerOptions(uint options2)
|
||||
{
|
||||
// Applied identically to BOTH the persisted snapshot and the live
|
||||
// draft (mirrors ApplyExternalGameplayChange's own idempotent-update
|
||||
// shape) so an unsaved draft edit to an unrelated field (font size,
|
||||
// timestamps, ...) survives the reseed instead of being clobbered by
|
||||
// a value computed once against the persisted snapshot.
|
||||
// Reseed function applied to the persisted snapshot below so an
|
||||
// unsaved edit to an unrelated field (font size, timestamps, ...)
|
||||
// survives the reseed instead of being clobbered by a value
|
||||
// computed once against a stale snapshot.
|
||||
ChatSettings Reseed(ChatSettings current) => current with
|
||||
{
|
||||
HearGeneralChat = (options2
|
||||
|
|
@ -651,7 +518,6 @@ internal sealed class RuntimeSettingsController :
|
|||
return;
|
||||
|
||||
Chat = synced;
|
||||
_viewModel?.ApplyExternalChatChange(Reseed);
|
||||
try
|
||||
{
|
||||
_storage.SaveChat(synced);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue