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
|
|
@ -11,13 +11,12 @@ public sealed class GameplayInputCommandControllerTests
|
|||
{
|
||||
[Theory]
|
||||
[InlineData(InputAction.ToggleInventoryPanel, "inventory")]
|
||||
[InlineData(InputAction.AcdreamToggleDebugPanel, "debug")]
|
||||
[InlineData(InputAction.AcdreamToggleFlyMode, "fly-or-chase")]
|
||||
[InlineData(InputAction.AcdreamTogglePlayerMode, "player-mode")]
|
||||
[InlineData(InputAction.ToggleChatEntry, "chat")]
|
||||
// Campaign OP slice OP3 (D1): F11 now opens the retail Options panel
|
||||
// through IRetainedGameplayWindowCommands, not the retired
|
||||
// IDevToolsGameplayCommands.ToggleSettingsPanel() no-op.
|
||||
// through IRetainedGameplayWindowCommands, not the OP9-retired
|
||||
// IDevToolsGameplayCommands.ToggleSettingsPanel() no-op (which was
|
||||
// never dispatched from this switch in the first place).
|
||||
[InlineData(InputAction.ToggleOptionsPanel, "options")]
|
||||
[InlineData(InputAction.CombatToggleCombat, "combat")]
|
||||
[InlineData(InputAction.ToggleFloatingChatWindow1, "chat-window-1")]
|
||||
|
|
@ -36,6 +35,26 @@ public sealed class GameplayInputCommandControllerTests
|
|||
Assert.Equal([expected], harness.Calls);
|
||||
}
|
||||
|
||||
// OP9: AcdreamToggleDebugPanel/ToggleChatEntry retired the
|
||||
// IDevToolsGameplayCommands seam they used to forward to — both
|
||||
// targets (the ImGui-era DebugPanel/ChatPanel) were already gone
|
||||
// (Campaign V slice V11), so the seam's own body was an unconditional
|
||||
// no-op. The action is still consumed (handled == true, matching the
|
||||
// prior no-op's contract) but claims no typed-owner call.
|
||||
[Theory]
|
||||
[InlineData(InputAction.AcdreamToggleDebugPanel)]
|
||||
[InlineData(InputAction.ToggleChatEntry)]
|
||||
public void RetiredDevToolsCommand_IsConsumedWithoutClaimingATypedOwner(
|
||||
InputAction action)
|
||||
{
|
||||
var harness = new Harness();
|
||||
|
||||
bool handled = harness.Controller.Handle(action);
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Empty(harness.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiagnosticCommand_PrecedesRemainingCommandSwitch()
|
||||
{
|
||||
|
|
@ -89,7 +108,6 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public Harness()
|
||||
{
|
||||
Retained = new FakeRetained(Calls);
|
||||
DevTools = new FakeDevTools(Calls);
|
||||
Diagnostics = new FakeDiagnostics(Calls);
|
||||
Player = new FakePlayerMode(Calls);
|
||||
TargetMode = new FakeTargetMode(Calls);
|
||||
|
|
@ -99,7 +117,6 @@ public sealed class GameplayInputCommandControllerTests
|
|||
Window = new FakeWindow(Calls);
|
||||
Controller = new GameplayInputCommandController(
|
||||
Retained,
|
||||
DevTools,
|
||||
Diagnostics,
|
||||
Player,
|
||||
TargetMode,
|
||||
|
|
@ -111,7 +128,6 @@ public sealed class GameplayInputCommandControllerTests
|
|||
|
||||
public List<string> Calls { get; } = [];
|
||||
public FakeRetained Retained { get; }
|
||||
public FakeDevTools DevTools { get; }
|
||||
public FakeDiagnostics Diagnostics { get; }
|
||||
public FakePlayerMode Player { get; }
|
||||
public FakeTargetMode TargetMode { get; }
|
||||
|
|
@ -133,14 +149,6 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public void ToggleOptionsPanel() => calls.Add("options");
|
||||
}
|
||||
|
||||
private sealed class FakeDevTools(List<string> calls)
|
||||
: IDevToolsGameplayCommands
|
||||
{
|
||||
public void ToggleDebugPanel() => calls.Add("debug");
|
||||
public void FocusChatInput() => calls.Add("chat");
|
||||
public void ToggleSettingsPanel() => calls.Add("settings");
|
||||
}
|
||||
|
||||
private sealed class FakeDiagnostics(List<string> calls)
|
||||
: IRuntimeDiagnosticCommands
|
||||
{
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"_factory.CreateBackendNeutralTerrain(");
|
||||
AssertAppearsInOrder(
|
||||
shutdown,
|
||||
"Soft(\"settings view model\", () => ingress.Settings.UnbindViewModel())",
|
||||
"new ResourceShutdownStage(\"physical ingress cleanup\"",
|
||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||
"Hard(\"frame-root bindings\", () => frame.FrameBindings?.Dispose())",
|
||||
"Hard(\"retail UI\", () => DisposeRetailUi(live.RetailUi))",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ using AcDream.App.Rendering;
|
|||
using AcDream.App.Settings;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Settings;
|
||||
using Silk.NET.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Settings;
|
||||
|
||||
|
|
@ -39,14 +37,12 @@ public sealed class RuntimeSettingsControllerTests
|
|||
|
||||
Assert.Equal(1, storage.DisplayLoads);
|
||||
Assert.Equal(1, storage.AudioLoads);
|
||||
Assert.Equal(1, storage.GameplayLoads);
|
||||
Assert.Equal(1, storage.ChatLoads);
|
||||
Assert.Equal(1, storage.CharacterLoads);
|
||||
Assert.Equal(1, resolveCount);
|
||||
Assert.Equal("default", storage.LastLoadedCharacter);
|
||||
Assert.Same(storage.DisplayValue, controller.Startup.Display);
|
||||
Assert.Same(storage.AudioValue, controller.Startup.Audio);
|
||||
Assert.Same(storage.GameplayValue, controller.Startup.Gameplay);
|
||||
Assert.Same(storage.ChatValue, controller.Startup.Chat);
|
||||
Assert.Same(storage.DefaultCharacterValue, controller.Startup.Character);
|
||||
Assert.Equal(resolved, controller.Startup.Quality);
|
||||
|
|
@ -75,31 +71,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
controller.BindRuntimeTargets(new FakeRuntimeTargets(events)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewModelBindingReleasesOnlyItsExpectedInstance()
|
||||
{
|
||||
var controller = CreateController();
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
RuntimeSettingsViewModelBinding first = controller.CreateViewModelBinding(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
first.Dispose();
|
||||
RuntimeSettingsViewModelBinding second = controller.CreateViewModelBinding(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
first.Dispose();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.CreateViewModelBinding(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { }));
|
||||
second.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartupRetryResumesAfterLastSuccessfulStage()
|
||||
{
|
||||
|
|
@ -262,63 +233,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
Assert.False(cmd.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsViewModelSavePreservesSectionAndTargetOrder()
|
||||
{
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events);
|
||||
var resolved = new QualitySettings(5, 15, 4, 16, true, 6);
|
||||
var controller = new RuntimeSettingsController(
|
||||
storage,
|
||||
_ => resolved,
|
||||
static _ => { });
|
||||
storage.ClearEvents();
|
||||
var targets = new FakeRuntimeTargets(events);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
_ => events.Add("save-bindings"));
|
||||
|
||||
viewModel.SetDisplay(viewModel.DisplayDraft with
|
||||
{
|
||||
Resolution = "1920x1080",
|
||||
Quality = QualityPreset.Ultra,
|
||||
});
|
||||
viewModel.SetAudio(viewModel.AudioDraft with { Master = 0.25f });
|
||||
viewModel.SetGameplay(viewModel.GameplayDraft with { LockUI = true });
|
||||
viewModel.SetChat(viewModel.ChatDraft with { ShowTimestamps = true });
|
||||
viewModel.SetCharacter(viewModel.CharacterDraft with { AutoAttack = true });
|
||||
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"save-bindings",
|
||||
"save-display",
|
||||
"target-display",
|
||||
"target-quality",
|
||||
"save-audio",
|
||||
// Campaign OP slice OP6 (2026-08-11): SaveAudio now pushes the
|
||||
// saved snapshot into the live engine, same shape as
|
||||
// save-display's own target-display push a few lines up.
|
||||
"target-audio",
|
||||
"save-gameplay",
|
||||
"target-ui-lock:True",
|
||||
"save-chat",
|
||||
"target-chat-opacity:1:1",
|
||||
"save-character:default",
|
||||
],
|
||||
events);
|
||||
Assert.Equal("1920x1080", controller.Display.Resolution);
|
||||
Assert.Equal(0.25f, controller.Audio.Master);
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.True(controller.Chat.ShowTimestamps);
|
||||
Assert.True(controller.Character.AutoAttack);
|
||||
Assert.Equal(resolved, controller.ResolvedQuality);
|
||||
}
|
||||
|
||||
// OP6 rework (2026-08-11, review S5 / M2): pins the EFFECTIVE volume
|
||||
// ApplyAudio actually computes, not just that some target was called.
|
||||
// The FakeRuntimeTargets-based "target-audio" assertion above (and its
|
||||
|
|
@ -369,6 +283,10 @@ public sealed class RuntimeSettingsControllerTests
|
|||
// CH3 (2026-08-09): SaveChat must publish SetSingleCharacterOption
|
||||
// (0x0005) for exactly the Hear*Chat bits that actually changed —
|
||||
// touching one checkbox must not resend the other four.
|
||||
//
|
||||
// OP9 rework: SaveChat was widened to public at Campaign OP slice
|
||||
// OP6, so this test now calls it directly instead of routing
|
||||
// through the retired SettingsVM draft/Save() indirection.
|
||||
var storage = new FakeStorage();
|
||||
var controller = new RuntimeSettingsController(
|
||||
storage,
|
||||
|
|
@ -376,31 +294,24 @@ public sealed class RuntimeSettingsControllerTests
|
|||
static _ => { });
|
||||
var targets = new FakeRuntimeTargets([]);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
// N4 (CH3 Opus review): ChatSettings.Default now matches ACE's real
|
||||
// CharacterOptions2.Default — Roleplay/Society start FALSE (only
|
||||
// General/Trade/LFG start true). Flip Roleplay ON first so the
|
||||
// second edit below can flip it back off.
|
||||
Assert.False(viewModel.ChatDraft.HearRoleplayChat);
|
||||
viewModel.SetChat(viewModel.ChatDraft with { HearRoleplayChat = true });
|
||||
viewModel.Save();
|
||||
Assert.False(controller.Chat.HearRoleplayChat);
|
||||
controller.SaveChat(controller.Chat with { HearRoleplayChat = true });
|
||||
|
||||
Assert.Equal(
|
||||
[((uint)CharacterOptionId.ListenToRoleplayChat, true)],
|
||||
targets.SingleOptionCalls);
|
||||
|
||||
targets.SingleOptionCalls.Clear();
|
||||
viewModel.SetChat(viewModel.ChatDraft with
|
||||
controller.SaveChat(controller.Chat with
|
||||
{
|
||||
HearRoleplayChat = false,
|
||||
HearSocietyChat = true,
|
||||
});
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
|
|
@ -416,15 +327,9 @@ public sealed class RuntimeSettingsControllerTests
|
|||
var controller = CreateController();
|
||||
var targets = new FakeRuntimeTargets([]);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
// Touch a Chat field that is NOT a Hear*Chat membership bit.
|
||||
viewModel.SetChat(viewModel.ChatDraft with { ShowTimestamps = false });
|
||||
viewModel.Save();
|
||||
controller.SaveChat(controller.Chat with { ShowTimestamps = false });
|
||||
|
||||
Assert.Empty(targets.SingleOptionCalls);
|
||||
}
|
||||
|
|
@ -434,23 +339,17 @@ public sealed class RuntimeSettingsControllerTests
|
|||
{
|
||||
// Campaign CH slice CH6c: unlike the Hear* options (local-only, no
|
||||
// wire), opacity is ALWAYS pushed on Save (not diffed) so the linking
|
||||
// invariant self-heals; the point of this test is that clicking Save
|
||||
// is enough — no restart, no separate "apply" step.
|
||||
// invariant self-heals; the point of this test is that calling
|
||||
// SaveChat is enough — no restart, no separate "apply" step.
|
||||
var controller = CreateController();
|
||||
var targets = new FakeRuntimeTargets([]);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
viewModel.SetChat(viewModel.ChatDraft with
|
||||
controller.SaveChat(controller.Chat with
|
||||
{
|
||||
DefaultOpacity = 0.3f,
|
||||
ActiveOpacity = 0.6f,
|
||||
});
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Equal([(0.3f, 0.6f)], targets.ChatOpacityCalls);
|
||||
Assert.Equal(0.3f, controller.Chat.DefaultOpacity);
|
||||
|
|
@ -486,7 +385,7 @@ public sealed class RuntimeSettingsControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncChatFromServerOptionsReseedsPersistedAndDraft()
|
||||
public void SyncChatFromServerOptionsReseedsPersisted()
|
||||
{
|
||||
// Research doc §5.2: ACE's CharacterOptions2.Default omits
|
||||
// HearRoleplayChat/HearSocietyChat. N4 (CH3 Opus review) aligned
|
||||
|
|
@ -507,13 +406,7 @@ public sealed class RuntimeSettingsControllerTests
|
|||
storage,
|
||||
static preset => QualitySettings.From(preset),
|
||||
static _ => { });
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
Assert.True(controller.Chat.HearRoleplayChat);
|
||||
Assert.True(viewModel.ChatDraft.HearRoleplayChat);
|
||||
|
||||
controller.SyncChatFromServerOptions(0x00948700u); // ACE's real default
|
||||
|
||||
|
|
@ -522,28 +415,9 @@ public sealed class RuntimeSettingsControllerTests
|
|||
Assert.True(controller.Chat.HearLFGChat);
|
||||
Assert.False(controller.Chat.HearRoleplayChat);
|
||||
Assert.False(controller.Chat.HearSocietyChat);
|
||||
Assert.False(viewModel.ChatDraft.HearRoleplayChat);
|
||||
Assert.False(viewModel.ChatDraft.HearSocietyChat);
|
||||
Assert.Same(controller.Chat, storage.ChatValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncChatFromServerOptionsPreservesUnsavedUnrelatedDraftEdits()
|
||||
{
|
||||
var controller = CreateController();
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
viewModel.SetChat(viewModel.ChatDraft with { FontSize = 18f });
|
||||
|
||||
controller.SyncChatFromServerOptions(0x00948700u);
|
||||
|
||||
Assert.Equal(18f, viewModel.ChatDraft.FontSize);
|
||||
Assert.False(viewModel.ChatDraft.HearRoleplayChat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncChatFromServerOptionsIsANoOpWhenUnchanged()
|
||||
{
|
||||
|
|
@ -569,126 +443,31 @@ public sealed class RuntimeSettingsControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void DraftPreviewAndExternalCommandsShareCanonicalState()
|
||||
public void DraftPreviewAlwaysMirrorsCommittedState()
|
||||
{
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events);
|
||||
var controller = CreateController(storage, events: events);
|
||||
storage.ClearEvents();
|
||||
var targets = new FakeRuntimeTargets(events);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
viewModel.SetDisplay(viewModel.DisplayDraft with
|
||||
{
|
||||
FieldOfView = 91f,
|
||||
ParticleRange = ParticleRange.Retail,
|
||||
});
|
||||
viewModel.SetAudio(viewModel.AudioDraft with { Sfx = 0.33f });
|
||||
viewModel.SetGameplay(viewModel.GameplayDraft with { ShowTooltips = false });
|
||||
// OP9: the optional developer-tools draft-preview view model
|
||||
// (SettingsVM) was retired — it had zero production construction
|
||||
// sites. HasDraftPreview is now hardcoded false and
|
||||
// DisplayPreview/AudioPreview always mirror the committed
|
||||
// Display/Audio snapshot. This pins the NEW (trivial) contract that
|
||||
// WorldRenderFrameBuilder and SettingsParticleRangeSource still
|
||||
// consume through IRuntimeSettingsPreviewSource.
|
||||
var controller = CreateController();
|
||||
|
||||
Assert.True(controller.HasDraftPreview);
|
||||
Assert.False(controller.HasDraftPreview);
|
||||
Assert.Equal(controller.Display, controller.DisplayPreview);
|
||||
Assert.Equal(controller.Audio, controller.AudioPreview);
|
||||
|
||||
controller.SaveDisplay(controller.Display with { FieldOfView = 91f });
|
||||
controller.SaveAudio(controller.Audio with { Sfx = 0.33f });
|
||||
|
||||
Assert.False(controller.HasDraftPreview);
|
||||
Assert.Equal(91f, controller.DisplayPreview.FieldOfView);
|
||||
Assert.Equal(ParticleRange.Retail, controller.DisplayPreview.ParticleRange);
|
||||
Assert.Equal(0.33f, controller.AudioPreview.Sfx);
|
||||
|
||||
controller.ToggleFrameRate();
|
||||
controller.SetUiLocked(true);
|
||||
controller.SetAcceptLootPermits(true);
|
||||
|
||||
Assert.True(controller.Display.ShowFps);
|
||||
Assert.True(controller.DisplayPreview.ShowFps);
|
||||
Assert.Equal(91f, controller.DisplayPreview.FieldOfView);
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.True(controller.Gameplay.AcceptLootPermits);
|
||||
Assert.False(viewModel.GameplayDraft.ShowTooltips);
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.AcceptLootPermits);
|
||||
Assert.Contains("target-ui-lock:True", events);
|
||||
Assert.Equal(2, storage.GameplaySaves);
|
||||
Assert.Equal(1, storage.DisplaySaves);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DraftCancelRestoresPreviewAndCombatTogglePreservesUnrelatedDrafts()
|
||||
{
|
||||
var storage = new FakeStorage();
|
||||
var controller = CreateController(storage);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
viewModel.SetDisplay(viewModel.DisplayDraft with { FieldOfView = 99f });
|
||||
viewModel.SetGameplay(viewModel.GameplayDraft with
|
||||
{
|
||||
ShowTooltips = false,
|
||||
CoordinatesOnRadar = false,
|
||||
});
|
||||
|
||||
controller.SetUiLocked(true);
|
||||
controller.SetAcceptLootPermits(true);
|
||||
|
||||
Assert.False(viewModel.GameplayDraft.ShowTooltips);
|
||||
Assert.False(viewModel.GameplayDraft.CoordinatesOnRadar);
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.AcceptLootPermits);
|
||||
Assert.Equal(99f, controller.DisplayPreview.FieldOfView);
|
||||
|
||||
viewModel.Cancel();
|
||||
|
||||
Assert.Equal(controller.Display.FieldOfView, controller.DisplayPreview.FieldOfView);
|
||||
Assert.Equal(controller.Gameplay.ShowTooltips, viewModel.GameplayDraft.ShowTooltips);
|
||||
Assert.Equal(
|
||||
controller.Gameplay.CoordinatesOnRadar,
|
||||
viewModel.GameplayDraft.CoordinatesOnRadar);
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.AcceptLootPermits);
|
||||
|
||||
viewModel.SetGameplay(viewModel.GameplayDraft with { ShowHelm = false });
|
||||
viewModel.Save();
|
||||
|
||||
Assert.True(storage.GameplayValue.LockUI);
|
||||
Assert.True(storage.GameplayValue.AcceptLootPermits);
|
||||
Assert.False(storage.GameplayValue.ShowHelm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedExternalGameplayPersistenceDoesNotPromoteViewModelBaseline()
|
||||
{
|
||||
var storage = new FakeStorage { ThrowOnGameplaySave = true };
|
||||
var logs = new List<string>();
|
||||
var controller = new RuntimeSettingsController(
|
||||
storage,
|
||||
static preset => QualitySettings.From(preset),
|
||||
logs.Add);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
controller.SetUiLocked(true);
|
||||
Assert.Throws<IOException>(() => controller.SetAcceptLootPermits(true));
|
||||
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.AcceptLootPermits);
|
||||
|
||||
viewModel.Cancel();
|
||||
|
||||
Assert.Equal(GameplaySettings.Default.LockUI, viewModel.GameplayDraft.LockUI);
|
||||
Assert.Equal(
|
||||
GameplaySettings.Default.AcceptLootPermits,
|
||||
viewModel.GameplayDraft.AcceptLootPermits);
|
||||
Assert.Contains(logs, line =>
|
||||
line.Contains("radar lock save failed", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterContextLoadsWithoutViewModelAndSynchronizesWhenBound()
|
||||
public void CharacterContextSwitchesActiveToonAndReloadsSettings()
|
||||
{
|
||||
var storage = new FakeStorage();
|
||||
storage.Characters["Alice"] = CharacterSettings.Default with
|
||||
|
|
@ -707,76 +486,23 @@ public sealed class RuntimeSettingsControllerTests
|
|||
Assert.Equal("Alice", controller.ActiveToonKey);
|
||||
Assert.Equal("Trade", controller.Character.DefaultChatChannel);
|
||||
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
controller.LoadCharacterContext("Bob");
|
||||
|
||||
Assert.Equal("Bob", controller.ActiveToonKey);
|
||||
Assert.False(viewModel.CharacterDraft.ConfirmSalvage);
|
||||
Assert.False(controller.Character.ConfirmSalvage);
|
||||
|
||||
controller.RestoreDefaultCharacterContext();
|
||||
controller.ResetActiveCharacterKey();
|
||||
|
||||
Assert.Equal("default", controller.ActiveToonKey);
|
||||
Assert.Same(storage.DefaultCharacterValue, controller.Character);
|
||||
Assert.Same(storage.DefaultCharacterValue, viewModel.CharacterDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterSaveUsesActiveToonAndDefaultSaveBecomesResetContext()
|
||||
{
|
||||
var storage = new FakeStorage();
|
||||
var controller = CreateController(storage);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
var newDefault = CharacterSettings.Default with { AutoAttack = true };
|
||||
viewModel.SetCharacter(newDefault);
|
||||
viewModel.Save();
|
||||
|
||||
controller.LoadCharacterContext("Alice");
|
||||
var alice = CharacterSettings.Default with { DefaultChatChannel = "Trade" };
|
||||
viewModel.SetCharacter(alice);
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Same(newDefault, storage.Characters["default"]);
|
||||
Assert.Same(alice, storage.Characters["Alice"]);
|
||||
controller.RestoreDefaultCharacterContext();
|
||||
Assert.Same(newDefault, controller.Character);
|
||||
Assert.Same(newDefault, viewModel.CharacterDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewModelAndRuntimeTargetLoansCanBeWithdrawnAndReboundPrecisely()
|
||||
public void RuntimeTargetLoansCanBeWithdrawnAndReboundPrecisely()
|
||||
{
|
||||
var events = new List<string>();
|
||||
var controller = CreateController(events: events);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
SettingsVM foreign = CreateStandaloneViewModel(dispatcher);
|
||||
|
||||
controller.UnbindViewModel(foreign);
|
||||
Assert.True(controller.HasDraftPreview);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { }));
|
||||
|
||||
controller.UnbindViewModel(viewModel);
|
||||
Assert.False(controller.HasDraftPreview);
|
||||
controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
var first = new FakeRuntimeTargets(events);
|
||||
controller.BindRuntimeTargets(first);
|
||||
|
|
@ -822,20 +548,14 @@ public sealed class RuntimeSettingsControllerTests
|
|||
logs.Add);
|
||||
storage.ClearEvents();
|
||||
controller.BindRuntimeTargets(new FakeRuntimeTargets(events));
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
DisplaySettings original = controller.Display;
|
||||
viewModel.SetDisplay(viewModel.DisplayDraft with
|
||||
|
||||
controller.SaveDisplay(controller.Display with
|
||||
{
|
||||
Resolution = "2560x1440",
|
||||
Quality = QualityPreset.Ultra,
|
||||
});
|
||||
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Same(original, controller.Display);
|
||||
Assert.DoesNotContain("target-display", events);
|
||||
Assert.DoesNotContain("target-quality", events);
|
||||
|
|
@ -857,20 +577,14 @@ public sealed class RuntimeSettingsControllerTests
|
|||
{
|
||||
ThrowOnDisplay = true,
|
||||
});
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
DisplaySettings original = controller.Display;
|
||||
viewModel.SetDisplay(viewModel.DisplayDraft with
|
||||
|
||||
controller.SaveDisplay(controller.Display with
|
||||
{
|
||||
Resolution = "3840x2160",
|
||||
Quality = QualityPreset.Ultra,
|
||||
});
|
||||
|
||||
viewModel.Save();
|
||||
|
||||
Assert.Equal(1, storage.DisplaySaves);
|
||||
Assert.Same(original, controller.Display);
|
||||
Assert.DoesNotContain("target-quality", events);
|
||||
|
|
@ -883,9 +597,7 @@ public sealed class RuntimeSettingsControllerTests
|
|||
var storage = new FakeStorage
|
||||
{
|
||||
ThrowOnAudioSave = true,
|
||||
ThrowOnGameplaySave = true,
|
||||
ThrowOnChatSave = true,
|
||||
ThrowOnCharacterSave = true,
|
||||
};
|
||||
var logs = new List<string>();
|
||||
var controller = new RuntimeSettingsController(
|
||||
|
|
@ -893,29 +605,15 @@ public sealed class RuntimeSettingsControllerTests
|
|||
static preset => QualitySettings.From(preset),
|
||||
logs.Add);
|
||||
AudioSettings originalAudio = controller.Audio;
|
||||
GameplaySettings originalGameplay = controller.Gameplay;
|
||||
ChatSettings originalChat = controller.Chat;
|
||||
CharacterSettings originalCharacter = controller.Character;
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
viewModel.SetAudio(viewModel.AudioDraft with { Master = 0.1f });
|
||||
viewModel.SetGameplay(viewModel.GameplayDraft with { LockUI = true });
|
||||
viewModel.SetChat(viewModel.ChatDraft with { ShowTimestamps = true });
|
||||
viewModel.SetCharacter(viewModel.CharacterDraft with { AutoAttack = true });
|
||||
|
||||
viewModel.Save();
|
||||
controller.SaveAudio(controller.Audio with { Master = 0.1f });
|
||||
controller.SaveChat(controller.Chat with { ShowTimestamps = true });
|
||||
|
||||
Assert.Same(originalAudio, controller.Audio);
|
||||
Assert.Same(originalGameplay, controller.Gameplay);
|
||||
Assert.Same(originalChat, controller.Chat);
|
||||
Assert.Same(originalCharacter, controller.Character);
|
||||
Assert.Contains(logs, line => line.Contains("audio save failed", StringComparison.Ordinal));
|
||||
Assert.Contains(logs, line => line.Contains("gameplay save failed", StringComparison.Ordinal));
|
||||
Assert.Contains(logs, line => line.Contains("chat save failed", StringComparison.Ordinal));
|
||||
Assert.Contains(logs, line => line.Contains("character save failed", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -956,135 +654,74 @@ public sealed class RuntimeSettingsControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SetUiLocked_AppliesEvenWhenGameplayLockUIAlreadyMatches_IfNeverActuallyApplied()
|
||||
public void SetUiLocked_AppliesOnFirstCallThenNoOpsOnRepeatedSameValue()
|
||||
{
|
||||
// MUST-FIX 4 (OP4 review-fix round, 2026-08-11, blast M3): the
|
||||
// guard used to compare the requested value against
|
||||
// `Gameplay.LockUI` — valid only while `ToggleUiLock` derived
|
||||
// `locked` AS `!Gameplay.LockUI`. OP4 re-pointed `ToggleUiLock` to
|
||||
// read the SERVER bit (RuntimeCharacterOptionsState) instead, a
|
||||
// DIFFERENT store that can already equal a value this controller
|
||||
// never actually pushed to `_runtimeTargets`. A pre-existing save
|
||||
// seeds Gameplay.LockUI = true; the runtime target has never seen
|
||||
// `true` — the FIRST SetUiLocked(true) call must still apply.
|
||||
// OP9: SetUiLocked no longer reads or writes a persisted
|
||||
// GameplaySettings mirror (MUST-FIX 4, OP4 review-fix round,
|
||||
// 2026-08-11, blast M3, superseded) — RuntimeCharacterOptionsState
|
||||
// (the server bit) is the sole authority now, so there is no longer
|
||||
// a second store that could disagree with what was actually
|
||||
// applied. The guard's own idempotency (compare against the last
|
||||
// value ACTUALLY pushed to _runtimeTargets) is the only behavior
|
||||
// left to pin here.
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events)
|
||||
{
|
||||
GameplayValue = GameplaySettings.Default with { LockUI = true },
|
||||
};
|
||||
var controller = new RuntimeSettingsController(
|
||||
storage,
|
||||
static preset => QualitySettings.From(preset),
|
||||
static _ => { });
|
||||
Assert.True(controller.Gameplay.LockUI); // already true, but never applied
|
||||
var controller = CreateController(events: events);
|
||||
var targets = new FakeRuntimeTargets(events);
|
||||
controller.BindRuntimeTargets(targets);
|
||||
|
||||
controller.SetUiLocked(true);
|
||||
|
||||
Assert.Equal(1, targets.UiLockCalls);
|
||||
|
||||
// A second call with the SAME value now correctly no-ops — this
|
||||
// time it really was applied.
|
||||
controller.SetUiLocked(true);
|
||||
|
||||
Assert.Equal(1, targets.UiLockCalls);
|
||||
|
||||
controller.SetUiLocked(false);
|
||||
Assert.Equal(2, targets.UiLockCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiLockTargetFailureCanRetryTheSameRequestedValue()
|
||||
{
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events);
|
||||
var controller = CreateController(storage, events);
|
||||
storage.ClearEvents();
|
||||
var controller = CreateController(events: events);
|
||||
var targets = new FakeRuntimeTargets(events)
|
||||
{
|
||||
RemainingUiLockFailures = 1,
|
||||
};
|
||||
controller.BindRuntimeTargets(targets);
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => controller.SetUiLocked(true));
|
||||
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.Equal(GameplaySettings.Default.LockUI, viewModel.GameplayDraft.LockUI);
|
||||
Assert.Equal(0, storage.GameplaySaves);
|
||||
Assert.Equal(["target-ui-lock:True"], events);
|
||||
|
||||
// A retry with the SAME requested value must re-apply — the guard
|
||||
// only advances _lastAppliedUiLocked on SUCCESS (the field write in
|
||||
// SetUiLocked runs after the ApplyUiLock call, which threw above).
|
||||
events.Clear();
|
||||
controller.SetUiLocked(true);
|
||||
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
viewModel.Cancel();
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.Equal(1, storage.GameplaySaves);
|
||||
Assert.Equal(["target-ui-lock:True", "save-gameplay"], events);
|
||||
Assert.Equal(["target-ui-lock:True"], events);
|
||||
Assert.Equal(2, targets.UiLockCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UiLockPersistenceFailureCanRetryTheSameRequestedValue()
|
||||
public void UnboundRuntimeTargetsConsumeUiLockCallsSilently()
|
||||
{
|
||||
// OP9: SetUiLocked no longer persists anything of its own, so the
|
||||
// only observable effect of an unbound call is the ABSENCE of any
|
||||
// "target-" event — there is no longer a Gameplay-side write to
|
||||
// assert against.
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events)
|
||||
{
|
||||
RemainingGameplaySaveFailures = 1,
|
||||
};
|
||||
var logs = new List<string>();
|
||||
var controller = new RuntimeSettingsController(
|
||||
storage,
|
||||
static preset => QualitySettings.From(preset),
|
||||
logs.Add);
|
||||
storage.ClearEvents();
|
||||
controller.BindRuntimeTargets(new FakeRuntimeTargets(events));
|
||||
using InputDispatcher dispatcher = CreateDispatcher();
|
||||
SettingsVM viewModel = controller.CreateViewModel(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { });
|
||||
|
||||
controller.SetUiLocked(true);
|
||||
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.Equal(1, storage.GameplaySaves);
|
||||
Assert.Contains(logs, line =>
|
||||
line.Contains("radar lock save failed", StringComparison.Ordinal));
|
||||
|
||||
events.Clear();
|
||||
controller.SetUiLocked(true);
|
||||
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
viewModel.Cancel();
|
||||
Assert.True(viewModel.GameplayDraft.LockUI);
|
||||
Assert.Equal(2, storage.GameplaySaves);
|
||||
Assert.Equal(["target-ui-lock:True", "save-gameplay"], events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnbindingLoansStopsCallsButStateAndPersistenceContinue()
|
||||
{
|
||||
var events = new List<string>();
|
||||
var storage = new FakeStorage(events);
|
||||
var controller = CreateController(storage, events: events);
|
||||
storage.ClearEvents();
|
||||
var controller = CreateController(events: events);
|
||||
controller.BindRuntimeTargets(new FakeRuntimeTargets(events));
|
||||
controller.UnbindRuntimeTargets();
|
||||
|
||||
controller.SetUiLocked(true);
|
||||
controller.ReapplyQualityPreset(QualityPreset.Ultra);
|
||||
|
||||
Assert.True(controller.Gameplay.LockUI);
|
||||
Assert.Equal(
|
||||
QualitySettings.From(QualityPreset.Ultra),
|
||||
controller.ResolvedQuality);
|
||||
Assert.DoesNotContain(events, value => value.StartsWith("target-", StringComparison.Ordinal));
|
||||
Assert.Equal(1, storage.GameplaySaves);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -1121,32 +758,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
static _ => { });
|
||||
}
|
||||
|
||||
private static InputDispatcher CreateDispatcher()
|
||||
{
|
||||
InputDispatcher dispatcher = InputDispatcher.CreateDetached(
|
||||
new NullKeyboardSource(),
|
||||
new NullMouseSource(),
|
||||
new KeyBindings());
|
||||
dispatcher.Attach();
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
private static SettingsVM CreateStandaloneViewModel(InputDispatcher dispatcher) =>
|
||||
new(
|
||||
new KeyBindings(),
|
||||
dispatcher,
|
||||
static _ => { },
|
||||
DisplaySettings.Default,
|
||||
static _ => { },
|
||||
AudioSettings.Default,
|
||||
static _ => { },
|
||||
GameplaySettings.Default,
|
||||
static _ => { },
|
||||
ChatSettings.Default,
|
||||
static _ => { },
|
||||
CharacterSettings.Default,
|
||||
static _ => { });
|
||||
|
||||
private sealed class FakeStartupTarget(List<string> events)
|
||||
: IRuntimeSettingsStartupTarget
|
||||
{
|
||||
|
|
@ -1356,8 +967,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
|
||||
public AudioSettings AudioValue { get; set; } = AudioSettings.Default;
|
||||
|
||||
public GameplaySettings GameplayValue { get; set; } = GameplaySettings.Default;
|
||||
|
||||
public ChatSettings ChatValue { get; set; } = ChatSettings.Default;
|
||||
|
||||
public CharacterSettings DefaultCharacterValue { get; set; } =
|
||||
|
|
@ -1377,16 +986,12 @@ public sealed class RuntimeSettingsControllerTests
|
|||
|
||||
public int AudioLoads { get; private set; }
|
||||
|
||||
public int GameplayLoads { get; private set; }
|
||||
|
||||
public int ChatLoads { get; private set; }
|
||||
|
||||
public int CharacterLoads { get; private set; }
|
||||
|
||||
public int DisplaySaves { get; private set; }
|
||||
|
||||
public int GameplaySaves { get; private set; }
|
||||
|
||||
public int ChatSaves { get; private set; }
|
||||
|
||||
public string? LastLoadedCharacter { get; private set; }
|
||||
|
|
@ -1395,10 +1000,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
|
||||
public bool ThrowOnAudioSave { get; init; }
|
||||
|
||||
public bool ThrowOnGameplaySave { get; init; }
|
||||
|
||||
public int RemainingGameplaySaveFailures { get; set; }
|
||||
|
||||
public bool ThrowOnChatSave { get; init; }
|
||||
|
||||
public bool ThrowOnCharacterSave { get; init; }
|
||||
|
|
@ -1415,12 +1016,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
return AudioValue;
|
||||
}
|
||||
|
||||
public GameplaySettings LoadGameplay()
|
||||
{
|
||||
GameplayLoads++;
|
||||
return GameplayValue;
|
||||
}
|
||||
|
||||
public ChatSettings LoadChat()
|
||||
{
|
||||
ChatLoads++;
|
||||
|
|
@ -1453,20 +1048,6 @@ public sealed class RuntimeSettingsControllerTests
|
|||
AudioValue = audio;
|
||||
}
|
||||
|
||||
public void SaveGameplay(GameplaySettings gameplay)
|
||||
{
|
||||
GameplaySaves++;
|
||||
_events.Add("save-gameplay");
|
||||
if (RemainingGameplaySaveFailures > 0)
|
||||
{
|
||||
RemainingGameplaySaveFailures--;
|
||||
throw new IOException("gameplay persistence failed");
|
||||
}
|
||||
if (ThrowOnGameplaySave)
|
||||
throw new IOException("gameplay persistence failed");
|
||||
GameplayValue = gameplay;
|
||||
}
|
||||
|
||||
public void SaveChat(ChatSettings chat)
|
||||
{
|
||||
ChatSaves++;
|
||||
|
|
@ -1532,27 +1113,4 @@ public sealed class RuntimeSettingsControllerTests
|
|||
return _refreshRate is > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NullKeyboardSource : IKeyboardSource
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event Action<Key, ModifierMask>? KeyDown;
|
||||
public event Action<Key, ModifierMask>? KeyUp;
|
||||
#pragma warning restore CS0067
|
||||
public bool IsHeld(Key key) => false;
|
||||
public ModifierMask CurrentModifiers => ModifierMask.None;
|
||||
}
|
||||
|
||||
private sealed class NullMouseSource : IMouseSource
|
||||
{
|
||||
#pragma warning disable CS0067
|
||||
public event Action<MouseButton, ModifierMask>? MouseDown;
|
||||
public event Action<MouseButton, ModifierMask>? MouseUp;
|
||||
public event Action<float, float>? MouseMove;
|
||||
public event Action<float>? Scroll;
|
||||
#pragma warning restore CS0067
|
||||
public bool IsHeld(MouseButton button) => false;
|
||||
public bool WantCaptureMouse => false;
|
||||
public bool WantCaptureKeyboard => false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue