refactor(settings): own two-phase runtime settings

Move pre-window loading, startup application, live settings mutation, toon context, quality reapply, and SettingsVM loans behind one RuntimeSettingsController. Preserve retail command behavior, ordered target publication, draft semantics, and retryable failure convergence while removing duplicate GameWindow state and feature bodies.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 13:30:22 +02:00
parent 4eae9b4f5a
commit fec0d94148
24 changed files with 2379 additions and 599 deletions

View file

@ -0,0 +1,471 @@
using AcDream.App.Combat;
using AcDream.UI.Abstractions.Input;
using AcDream.UI.Abstractions.Panels.Settings;
using AcDream.UI.Abstractions.Settings;
namespace AcDream.App.Settings;
internal interface IRuntimeSettingsStorage
{
SettingsStore? LayoutStore { get; }
string Location { get; }
DisplaySettings LoadDisplay();
AudioSettings LoadAudio();
GameplaySettings LoadGameplay();
ChatSettings LoadChat();
CharacterSettings LoadCharacter(string toonKey);
void SaveDisplay(DisplaySettings display);
void SaveAudio(AudioSettings audio);
void SaveGameplay(GameplaySettings gameplay);
void SaveChat(ChatSettings chat);
void SaveCharacter(string toonKey, CharacterSettings character);
}
internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
{
private readonly SettingsStore _store;
public JsonRuntimeSettingsStorage(string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
Location = path;
_store = new SettingsStore(path);
}
public SettingsStore LayoutStore => _store;
public string Location { get; }
public DisplaySettings LoadDisplay() => _store.LoadDisplay();
public AudioSettings LoadAudio() => _store.LoadAudio();
public GameplaySettings LoadGameplay() => _store.LoadGameplay();
public ChatSettings LoadChat() => _store.LoadChat();
public CharacterSettings LoadCharacter(string toonKey) =>
_store.LoadCharacter(toonKey);
public void SaveDisplay(DisplaySettings display) => _store.SaveDisplay(display);
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) =>
_store.SaveCharacter(toonKey, character);
}
internal sealed record RuntimeSettingsSnapshot(
DisplaySettings Display,
AudioSettings Audio,
GameplaySettings Gameplay,
ChatSettings Chat,
CharacterSettings Character,
QualitySettings Quality);
internal interface IRuntimeSettingsStartupTarget
{
void ApplyDisplay(DisplaySettings display);
void ApplyAudio(AudioSettings audio);
}
internal interface IRuntimeSettingsTargets
{
void ApplyDisplayWindowState(DisplaySettings display);
void ApplyQuality(QualitySettings quality);
void ApplyUiLock(bool locked);
}
internal interface IRuntimeSettingsPreviewSource
{
bool HasDraftPreview { get; }
DisplaySettings DisplayPreview { get; }
AudioSettings AudioPreview { get; }
}
/// <summary>
/// Owns the one persisted settings snapshot, its live mutations, and the active
/// character context. Runtime objects are borrowed through typed targets and
/// are never constructed or disposed here.
/// </summary>
internal sealed class RuntimeSettingsController :
IRuntimeSettingsPreviewSource,
ICombatGameplaySettingsSource
{
private const string DefaultToonKey = "default";
private readonly IRuntimeSettingsStorage _storage;
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;
public RuntimeSettingsController(
IRuntimeSettingsStorage storage,
Func<QualityPreset, QualitySettings>? resolveQuality = null,
Action<string>? log = null)
{
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
_resolveQuality = resolveQuality ?? ResolveQuality;
_log = log ?? Console.WriteLine;
Display = _storage.LoadDisplay();
Audio = _storage.LoadAudio();
Gameplay = _storage.LoadGameplay();
Chat = _storage.LoadChat();
_defaultCharacter = _storage.LoadCharacter(DefaultToonKey);
Character = _defaultCharacter;
ResolvedQuality = _resolveQuality(Display.Quality);
Startup = new RuntimeSettingsSnapshot(
Display,
Audio,
Gameplay,
Chat,
Character,
ResolvedQuality);
}
public RuntimeSettingsSnapshot Startup { get; }
public SettingsStore? LayoutStore => _storage.LayoutStore;
public string ActiveToonKey { get; private set; } = DefaultToonKey;
public DisplaySettings Display { get; private set; }
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;
public DisplaySettings DisplayPreview => _viewModel?.DisplayDraft ?? Display;
public AudioSettings AudioPreview => _viewModel?.AudioDraft ?? Audio;
public bool AutoTarget => Gameplay.AutoTarget;
public bool AutoRepeatAttack => Gameplay.AutoRepeatAttack;
public bool ViewCombatTarget => Gameplay.ViewCombatTarget;
public void ApplyStartup(IRuntimeSettingsStartupTarget target)
{
ArgumentNullException.ThrowIfNull(target);
if (_startupApplied)
throw new InvalidOperationException("Runtime settings startup was already applied.");
if (!_startupDisplayApplied)
{
target.ApplyDisplay(Startup.Display);
_startupDisplayApplied = true;
}
if (!_startupAudioApplied)
{
target.ApplyAudio(Startup.Audio);
_startupAudioApplied = true;
}
_startupApplied = true;
QualitySettings baseQuality = QualitySettings.From(Startup.Display.Quality);
_log(Startup.Quality.Equals(baseQuality)
? $"[QUALITY] Preset {Startup.Display.Quality} -> {Startup.Quality}"
: $"[QUALITY] Preset {Startup.Display.Quality} overridden by env vars: {Startup.Quality}");
}
/// <summary>
/// Installs the complete future-change target. Deliberately does not replay
/// startup display, quality, or UI-lock values; those were consumed by the
/// factories that created the borrowed target objects.
/// </summary>
public void BindRuntimeTargets(IRuntimeSettingsTargets targets)
{
ArgumentNullException.ThrowIfNull(targets);
if (_runtimeTargets is not null)
throw new InvalidOperationException("Runtime settings targets are already bound.");
_runtimeTargets = targets;
}
public void UnbindRuntimeTargets() => _runtimeTargets = null;
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 void UnbindViewModel(SettingsVM? expected = null)
{
if (expected is null || ReferenceEquals(_viewModel, expected))
_viewModel = null;
}
public void SetUiLocked(bool locked)
{
if (Gameplay.LockUI == locked && _uiLockConverged)
return;
_uiLockConverged = false;
Gameplay = Gameplay with { LockUI = locked };
_runtimeTargets?.ApplyUiLock(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>
/// Retail <c>ClientCommunicationSystem::DoFrameRate @ 0x005707D0</c>
/// flips the live flag and sends the framerate-display UI notice. acdream
/// additionally persists that live value through its modern Display bag
/// (registered as AP-121).
/// </summary>
public void ToggleFrameRate()
{
Display = Display with { ShowFps = !Display.ShowFps };
_viewModel?.ApplyExternalDisplayChange(display => display with
{
ShowFps = Display.ShowFps,
});
try
{
_storage.SaveDisplay(Display);
}
catch (Exception ex)
{
_log($"settings: framerate display save failed: {ex.Message}");
}
}
public void SetCombatGameplay(GameplaySettings gameplay)
{
Gameplay = gameplay ?? throw new ArgumentNullException(nameof(gameplay));
if (_viewModel is not null)
{
_viewModel.SetGameplay(_viewModel.GameplayDraft with
{
AutoTarget = gameplay.AutoTarget,
AutoRepeatAttack = gameplay.AutoRepeatAttack,
ViewCombatTarget = gameplay.ViewCombatTarget,
});
}
try
{
_storage.SaveGameplay(gameplay);
_viewModel?.ApplyExternalGameplayChange(current => current with
{
AutoTarget = gameplay.AutoTarget,
AutoRepeatAttack = gameplay.AutoRepeatAttack,
ViewCombatTarget = gameplay.ViewCombatTarget,
});
}
catch (Exception ex)
{
_log($"settings: combat option save failed: {ex.Message}");
}
}
public void SetActiveCharacter(string characterName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
ActiveToonKey = characterName;
}
public void LoadCharacterContext(string characterName)
{
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;
public void ReapplyQualityPreset(QualityPreset preset)
{
QualitySettings resolved = _resolveQuality(preset);
_log($"[QUALITY] ReapplyQualityPreset: {preset} -> {resolved}");
if (resolved.MsaaSamples != ResolvedQuality.MsaaSamples)
{
_log(
$"[QUALITY] MSAA samples change ({ResolvedQuality.MsaaSamples} -> " +
$"{resolved.MsaaSamples}) requires a restart - skipped for this session.");
}
ResolvedQuality = resolved;
_runtimeTargets?.ApplyQuality(resolved);
}
private void SaveDisplay(DisplaySettings display)
{
try
{
_storage.SaveDisplay(display);
_log($"settings: display saved to {_storage.Location}");
_runtimeTargets?.ApplyDisplayWindowState(display);
Display = display;
ReapplyQualityPreset(display.Quality);
}
catch (Exception ex)
{
_log($"settings: display save failed: {ex.Message}");
}
}
private void SaveAudio(AudioSettings audio)
{
try
{
_storage.SaveAudio(audio);
Audio = audio;
_log($"settings: audio saved to {_storage.Location}");
}
catch (Exception ex)
{
_log($"settings: audio save failed: {ex.Message}");
}
}
private void SaveGameplay(GameplaySettings gameplay)
{
try
{
_storage.SaveGameplay(gameplay);
Gameplay = gameplay;
_uiLockConverged = false;
_runtimeTargets?.ApplyUiLock(gameplay.LockUI);
_uiLockConverged = true;
_log($"settings: gameplay saved to {_storage.Location}");
}
catch (Exception ex)
{
_log($"settings: gameplay save failed: {ex.Message}");
}
}
private void SaveChat(ChatSettings chat)
{
try
{
_storage.SaveChat(chat);
Chat = chat;
_log($"settings: chat saved to {_storage.Location}");
}
catch (Exception ex)
{
_log($"settings: chat save failed: {ex.Message}");
}
}
private void SaveCharacter(CharacterSettings character)
{
try
{
_storage.SaveCharacter(ActiveToonKey, character);
Character = character;
if (string.Equals(
ActiveToonKey,
DefaultToonKey,
StringComparison.OrdinalIgnoreCase))
{
_defaultCharacter = character;
}
_log($"settings: character[{ActiveToonKey}] saved to {_storage.Location}");
}
catch (Exception ex)
{
_log($"settings: character save failed: {ex.Message}");
}
}
private static QualitySettings ResolveQuality(QualityPreset preset) =>
QualitySettings.WithEnvOverrides(QualitySettings.From(preset));
}