using AcDream.App.Diagnostics; using AcDream.App.Rendering; using AcDream.App.Settings; 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; public sealed class RuntimeSettingsControllerTests { [Fact] public void ConstructionLoadsEachBagOnceAndPublishesOneStartupSnapshot() { var storage = new FakeStorage { DisplayValue = DisplaySettings.Default with { VSync = false, Quality = QualityPreset.Ultra, }, GameplayValue = GameplaySettings.Default with { AutoTarget = true, AutoRepeatAttack = true, ViewCombatTarget = true, }, }; var resolved = new QualitySettings(7, 18, 8, 16, true, 9); int resolveCount = 0; var controller = new RuntimeSettingsController( storage, preset => { resolveCount++; Assert.Equal(QualityPreset.Ultra, preset); return resolved; }, static _ => { }); 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); Assert.Equal(resolved, controller.ResolvedQuality); Assert.True(controller.AutoTarget); Assert.True(controller.AutoRepeatAttack); Assert.True(controller.ViewCombatTarget); Assert.Equal("default", controller.ActiveToonKey); } [Fact] public void StartupApplyIsOrderedExactlyOnceAndRuntimeBindingDoesNotReplay() { var events = new List(); var controller = CreateController(events: events); var startup = new FakeStartupTarget(events); controller.ApplyStartup(startup); Assert.Equal(["startup-display", "startup-audio"], events); Assert.Throws(() => controller.ApplyStartup(startup)); events.Clear(); var runtime = new FakeRuntimeTargets(events); controller.BindRuntimeTargets(runtime); Assert.Empty(events); Assert.Throws(() => 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(() => controller.CreateViewModelBinding( new KeyBindings(), dispatcher, static _ => { })); second.Dispose(); } [Fact] public void StartupRetryResumesAfterLastSuccessfulStage() { var displayEvents = new List(); var displayController = CreateController(); var displayTarget = new FakeStartupTarget(displayEvents) { RemainingDisplayFailures = 1, }; Assert.Throws(() => displayController.ApplyStartup(displayTarget)); displayController.ApplyStartup(displayTarget); Assert.Equal( ["startup-display", "startup-display", "startup-audio"], displayEvents); var audioEvents = new List(); var audioController = CreateController(); var audioTarget = new FakeStartupTarget(audioEvents) { RemainingAudioFailures = 1, }; Assert.Throws(() => audioController.ApplyStartup(audioTarget)); audioController.ApplyStartup(audioTarget); Assert.Equal( ["startup-display", "startup-audio", "startup-audio"], audioEvents); } [Fact] public void ConcreteStartupTargetAppliesPacingThenWindowThenPersistedFov() { using var profiler = new FrameProfiler(); using var pacing = new DisplayFramePacingController( uncappedRendering: false, profiler, new FramePacingController(new FakeClock(), new NullWaiter())); var surface = new FakePacingSurface { VSync = true, ActiveMonitorRefreshHz = 144, }; pacing.InitializeStartup(requestedVSync: true); pacing.BindSurface(surface); var cameras = new CameraController(new OrbitCamera(), new FlyCamera()); float originalFov = cameras.Orbit.FovY; var displayWindow = new InspectingDisplayWindowTarget(display => { Assert.False(pacing.RequestedVSync); Assert.Equal(originalFov, cameras.Orbit.FovY); Assert.Equal("1600x900", display.Resolution); }); var target = new RuntimeSettingsStartupTargets( displayWindow, pacing, cameras, audio: null); target.ApplyDisplay(DisplaySettings.Default with { Resolution = "1600x900", VSync = false, FieldOfView = 83f, }); Assert.Equal(1, displayWindow.ApplyCount); Assert.Equal(1, surface.RefreshReadCount); Assert.Equal(new FramePacingPolicy(false, 144d), pacing.Policy); Assert.Equal(83f * (MathF.PI / 180f), cameras.Orbit.FovY, precision: 5); Assert.Equal(83f * (MathF.PI / 180f), cameras.Fly.FovY, precision: 5); } [Fact] public void ConcreteRuntimeTargetAppliesEveryQualityDimensionInOrder() { var events = new List(); var qualityTarget = new RecordingQualityApplicationTarget(events); var displayTarget = new InspectingDisplayWindowTarget( _ => events.Add("display")); var uiTarget = new RecordingUiLockTarget(events); var target = new RuntimeSettingsTargets( displayTarget, qualityTarget, uiTarget, static _ => { }); var quality = new QualitySettings(6, 17, 4, 12, true, 7); target.ApplyQuality(quality); Assert.Equal( [ "a2c:True", "aniso:12", "range:6:17", "stream:6:17", "budget:7", ], events); Assert.Equal(quality, qualityTarget.Observed); } [Fact] public void ConcreteRuntimeQualityTargetStopsAtTheThrowingStep() { string[] allSteps = [ "a2c", "aniso", "range", "stream", "budget", ]; for (int failureIndex = 0; failureIndex < allSteps.Length; failureIndex++) { var events = new List(); var target = new RuntimeSettingsTargets( new InspectingDisplayWindowTarget(static _ => { }), new FailingQualityApplicationTarget(events, failureIndex), new RecordingUiLockTarget(events), static _ => { }); Assert.Throws(() => target.ApplyQuality(QualitySettings.From(QualityPreset.High))); Assert.Equal(allSteps[..(failureIndex + 1)], events); } } [Fact] public void SettingsViewModelSavePreservesSectionAndTargetOrder() { var events = new List(); 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", "save-gameplay", "target-ui-lock:True", "save-chat", "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); } [Fact] public void DraftPreviewAndExternalCommandsShareCanonicalState() { var events = new List(); 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 { AutoTarget = true }); Assert.True(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.True(viewModel.GameplayDraft.AutoTarget); 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.SetCombatGameplay(controller.Gameplay with { AutoTarget = false }); controller.SetUiLocked(true); controller.SetAcceptLootPermits(true); Assert.False(controller.Gameplay.AutoTarget); Assert.False(viewModel.GameplayDraft.AutoTarget); 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.False(viewModel.GameplayDraft.AutoTarget); Assert.True(viewModel.GameplayDraft.LockUI); Assert.True(viewModel.GameplayDraft.AcceptLootPermits); viewModel.SetGameplay(viewModel.GameplayDraft with { ShowHelm = false }); viewModel.Save(); Assert.False(storage.GameplayValue.AutoTarget); 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(); 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); controller.SetCombatGameplay(controller.Gameplay with { AutoTarget = false }); Assert.Throws(() => controller.SetAcceptLootPermits(true)); Assert.True(viewModel.GameplayDraft.LockUI); Assert.False(viewModel.GameplayDraft.AutoTarget); Assert.True(viewModel.GameplayDraft.AcceptLootPermits); viewModel.Cancel(); Assert.Equal(GameplaySettings.Default.LockUI, viewModel.GameplayDraft.LockUI); Assert.Equal( GameplaySettings.Default.AutoTarget, viewModel.GameplayDraft.AutoTarget); Assert.Equal( GameplaySettings.Default.AcceptLootPermits, viewModel.GameplayDraft.AcceptLootPermits); Assert.Contains(logs, line => line.Contains("radar lock save failed", StringComparison.Ordinal)); Assert.Contains(logs, line => line.Contains("combat option save failed", StringComparison.Ordinal)); } [Fact] public void CharacterContextLoadsWithoutViewModelAndSynchronizesWhenBound() { var storage = new FakeStorage(); storage.Characters["Alice"] = CharacterSettings.Default with { DefaultChatChannel = "Trade", }; storage.Characters["Bob"] = CharacterSettings.Default with { ConfirmSalvage = false, }; var controller = CreateController(storage); controller.SetActiveCharacter("Alice"); controller.LoadCharacterContext("Alice"); 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); 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() { var events = new List(); 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(() => 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); controller.UnbindRuntimeTargets(); var second = new FakeRuntimeTargets(events); controller.BindRuntimeTargets(second); controller.SetUiLocked(true); Assert.Equal(0, first.UiLockCalls); Assert.Equal(1, second.UiLockCalls); } [Fact] public void DisplayPersistenceFailureDoesNotPublishStateOrTargets() { var events = new List(); var storage = new FakeStorage(events) { ThrowOnDisplaySave = true }; var logs = new List(); 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 _ => { }); DisplaySettings original = controller.Display; viewModel.SetDisplay(viewModel.DisplayDraft with { Resolution = "2560x1440", Quality = QualityPreset.Ultra, }); viewModel.Save(); Assert.Same(original, controller.Display); Assert.DoesNotContain("target-display", events); Assert.DoesNotContain("target-quality", events); Assert.Contains(logs, line => line.Contains("display save failed", StringComparison.Ordinal)); } [Fact] public void DisplayTargetFailurePreservesEstablishedStoreThenPublishBoundary() { var events = new List(); var storage = new FakeStorage(events); var logs = new List(); var controller = new RuntimeSettingsController( storage, static preset => QualitySettings.From(preset), logs.Add); storage.ClearEvents(); controller.BindRuntimeTargets(new FakeRuntimeTargets(events) { ThrowOnDisplay = true, }); using InputDispatcher dispatcher = CreateDispatcher(); SettingsVM viewModel = controller.CreateViewModel( new KeyBindings(), dispatcher, static _ => { }); DisplaySettings original = controller.Display; viewModel.SetDisplay(viewModel.DisplayDraft with { Resolution = "3840x2160", Quality = QualityPreset.Ultra, }); viewModel.Save(); Assert.Equal(1, storage.DisplaySaves); Assert.Same(original, controller.Display); Assert.DoesNotContain("target-quality", events); Assert.Contains(logs, line => line.Contains("display save failed", StringComparison.Ordinal)); } [Fact] public void NonDisplayPersistenceFailuresContinueAndPreserveControllerState() { var storage = new FakeStorage { ThrowOnAudioSave = true, ThrowOnGameplaySave = true, ThrowOnChatSave = true, ThrowOnCharacterSave = true, }; var logs = new List(); var controller = new RuntimeSettingsController( storage, 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(); 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] public void MsaaChangeIsRestartRequiredWhileOtherQualityStateAdvances() { var logs = new List(); var events = new List(); var controller = new RuntimeSettingsController( new FakeStorage(), static preset => QualitySettings.From(preset), logs.Add); controller.BindRuntimeTargets(new FakeRuntimeTargets(events)); controller.ReapplyQualityPreset(QualityPreset.Low); Assert.Equal(QualitySettings.From(QualityPreset.Low), controller.ResolvedQuality); Assert.Equal(["target-quality"], events); Assert.Contains(logs, line => line.Contains("MSAA samples change (4 -> 0) requires a restart", StringComparison.Ordinal)); } [Fact] public void QualityTargetFailurePublishesResolvedQualityThenPropagates() { var events = new List(); var controller = CreateController(events: events); controller.BindRuntimeTargets(new FakeRuntimeTargets(events) { ThrowOnQuality = true, }); QualitySettings requested = QualitySettings.From(QualityPreset.Ultra); Assert.Throws(() => controller.ReapplyQualityPreset(QualityPreset.Ultra)); Assert.Equal(requested, controller.ResolvedQuality); Assert.Equal(["target-quality"], events); } [Fact] public void UiLockTargetFailureCanRetryTheSameRequestedValue() { var events = new List(); var storage = new FakeStorage(events); var controller = CreateController(storage, events); storage.ClearEvents(); var targets = new FakeRuntimeTargets(events) { RemainingUiLockFailures = 1, }; controller.BindRuntimeTargets(targets); using InputDispatcher dispatcher = CreateDispatcher(); SettingsVM viewModel = controller.CreateViewModel( new KeyBindings(), dispatcher, static _ => { }); Assert.Throws(() => 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); 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); } [Fact] public void UiLockPersistenceFailureCanRetryTheSameRequestedValue() { var events = new List(); var storage = new FakeStorage(events) { RemainingGameplaySaveFailures = 1, }; var logs = new List(); 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(); var storage = new FakeStorage(events); var controller = CreateController(storage, events: events); storage.ClearEvents(); 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] [InlineData("1920x1080", true, 1920, 1080)] [InlineData(" 800x600 ", true, 800, 600)] [InlineData("", false, 0, 0)] [InlineData("1920", false, 0, 0)] [InlineData("0x1080", false, 0, 1080)] [InlineData("1920x-1", false, 1920, -1)] public void ResolutionParserMatchesWindowTargetPolicy( string spec, bool expected, int expectedWidth, int expectedHeight) { bool parsed = SilkRuntimeDisplayWindowTarget.TryParseResolution( spec, out int width, out int height); Assert.Equal(expected, parsed); Assert.Equal(expectedWidth, width); Assert.Equal(expectedHeight, height); } private static RuntimeSettingsController CreateController( FakeStorage? storage = null, List? events = null) { storage ??= new FakeStorage(events); return new RuntimeSettingsController( storage, static preset => QualitySettings.From(preset), 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 events) : IRuntimeSettingsStartupTarget { public int RemainingDisplayFailures { get; set; } public int RemainingAudioFailures { get; set; } public void ApplyDisplay(DisplaySettings display) { events.Add("startup-display"); if (RemainingDisplayFailures > 0) { RemainingDisplayFailures--; throw new InvalidOperationException("display startup failed"); } } public void ApplyAudio(AudioSettings audio) { events.Add("startup-audio"); if (RemainingAudioFailures > 0) { RemainingAudioFailures--; throw new InvalidOperationException("audio startup failed"); } } } private sealed class FakeRuntimeTargets(List events) : IRuntimeSettingsTargets { public bool ThrowOnDisplay { get; init; } public bool ThrowOnQuality { get; init; } public int RemainingUiLockFailures { get; set; } public int UiLockCalls { get; private set; } public void ApplyDisplayWindowState(DisplaySettings display) { events.Add("target-display"); if (ThrowOnDisplay) throw new InvalidOperationException("display target failed"); } public void ApplyQuality(QualitySettings quality) { events.Add("target-quality"); if (ThrowOnQuality) throw new InvalidOperationException("quality target failed"); } public void ApplyUiLock(bool locked) { UiLockCalls++; events.Add($"target-ui-lock:{locked}"); if (RemainingUiLockFailures > 0) { RemainingUiLockFailures--; throw new InvalidOperationException("UI-lock target failed"); } } } private sealed class InspectingDisplayWindowTarget( Action apply) : IRuntimeDisplayWindowTarget { public int ApplyCount { get; private set; } public void Apply(DisplaySettings display) { ApplyCount++; apply(display); } } private sealed class RecordingQualityApplicationTarget(List events) : IRuntimeQualityApplicationTarget { private bool _alphaToCoverage; private int _anisotropic; private int _near; private int _far; private int _streamNear; private int _streamFar; private int _budget; public QualitySettings Observed => new( _near, _far, 4, _anisotropic, _alphaToCoverage, _budget); public void SetAlphaToCoverage(bool enabled) { _alphaToCoverage = enabled; events.Add($"a2c:{enabled}"); } public void SetAnisotropic(int level) { _anisotropic = level; events.Add($"aniso:{level}"); } public void PublishRenderRange(int nearRadius, int farRadius) { _near = nearRadius; _far = farRadius; events.Add($"range:{nearRadius}:{farRadius}"); } public void ReconfigureStreamingRadii(int nearRadius, int farRadius) { _streamNear = nearRadius; _streamFar = farRadius; events.Add($"stream:{nearRadius}:{farRadius}"); } public void SetCompletionBudget(int maxCompletionsPerFrame) { Assert.Equal(_near, _streamNear); Assert.Equal(_far, _streamFar); _budget = maxCompletionsPerFrame; events.Add($"budget:{maxCompletionsPerFrame}"); } } private sealed class FailingQualityApplicationTarget( List events, int failureIndex) : IRuntimeQualityApplicationTarget { private int _step; public void SetAlphaToCoverage(bool enabled) => Record("a2c"); public void SetAnisotropic(int level) => Record("aniso"); public void PublishRenderRange(int nearRadius, int farRadius) => Record("range"); public void ReconfigureStreamingRadii(int nearRadius, int farRadius) => Record("stream"); public void SetCompletionBudget(int maxCompletionsPerFrame) => Record("budget"); private void Record(string step) { events.Add(step); if (_step++ == failureIndex) throw new InvalidOperationException($"{step} failed"); } } private sealed class RecordingUiLockTarget(List events) : IRuntimeUiLockTarget { public void Apply(bool locked) => events.Add($"ui:{locked}"); } private sealed class FakeStorage(List? events = null) : IRuntimeSettingsStorage { private readonly List _events = events ?? []; public DisplaySettings DisplayValue { get; set; } = DisplaySettings.Default; 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; } = CharacterSettings.Default; public Dictionary Characters { get; } = new(StringComparer.OrdinalIgnoreCase); public SettingsStore? LayoutStore => null; public string Location => "memory://settings"; public int DisplayLoads { get; private set; } 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 string? LastLoadedCharacter { get; private set; } public bool ThrowOnDisplaySave { get; init; } 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; } public DisplaySettings LoadDisplay() { DisplayLoads++; return DisplayValue; } public AudioSettings LoadAudio() { AudioLoads++; return AudioValue; } public GameplaySettings LoadGameplay() { GameplayLoads++; return GameplayValue; } public ChatSettings LoadChat() { ChatLoads++; return ChatValue; } public CharacterSettings LoadCharacter(string toonKey) { CharacterLoads++; LastLoadedCharacter = toonKey; return Characters.TryGetValue(toonKey, out CharacterSettings? value) ? value : DefaultCharacterValue; } public void SaveDisplay(DisplaySettings display) { DisplaySaves++; _events.Add("save-display"); if (ThrowOnDisplaySave) throw new IOException("display persistence failed"); DisplayValue = display; } public void SaveAudio(AudioSettings audio) { _events.Add("save-audio"); if (ThrowOnAudioSave) throw new IOException("audio persistence failed"); 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) { _events.Add("save-chat"); if (ThrowOnChatSave) throw new IOException("chat persistence failed"); ChatValue = chat; } public void SaveCharacter(string toonKey, CharacterSettings character) { _events.Add($"save-character:{toonKey}"); if (ThrowOnCharacterSave) throw new IOException("character persistence failed"); Characters[toonKey] = character; if (string.Equals(toonKey, "default", StringComparison.OrdinalIgnoreCase)) DefaultCharacterValue = character; } public void ClearEvents() => _events.Clear(); } private sealed class FakeClock : IFramePacingClock { public long Frequency => 1_000; public long GetTimestamp() => 0; } private sealed class NullWaiter : IFramePacingWaiter { public void Wait(long durationTicks, long clockFrequency) { } } private sealed class FakePacingSurface : IDisplayFramePacingSurface { private int? _refreshRate; public bool VSync { get; set; } public int RefreshReadCount { get; private set; } public int? ActiveMonitorRefreshHz { get { RefreshReadCount++; return _refreshRate; } set => _refreshRate = value; } } private sealed class NullKeyboardSource : IKeyboardSource { #pragma warning disable CS0067 public event Action? KeyDown; public event Action? 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? MouseDown; public event Action? MouseUp; public event Action? MouseMove; public event Action? Scroll; #pragma warning restore CS0067 public bool IsHeld(MouseButton button) => false; public bool WantCaptureMouse => false; public bool WantCaptureKeyboard => false; } }