using AcDream.App.Combat; using AcDream.Core.Net.Messages; 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); /// /// Expected-owner lease for the optional developer settings view model. /// Failed optional composition can withdraw only the instance it installed. /// 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); void ApplyAudio(AudioSettings audio); } internal interface IRuntimeSettingsTargets { void ApplyDisplayWindowState(DisplaySettings display); void ApplyQuality(QualitySettings quality); void ApplyUiLock(bool locked); /// /// Campaign CH slice CH3 (2026-08-09): the generation-gated seam /// (matching J4.4's pattern) that publishes retail's /// SetSingleCharacterOption (0x0005) for one Settings Chat /// toggle. is an ACE /// CharacterOption id (e.g. ListenToGeneralChat = 0x23). /// void SetSingleCharacterOption(uint optionId, bool value); } internal interface IRuntimeSettingsPreviewSource { bool HasDraftPreview { get; } DisplaySettings DisplayPreview { get; } AudioSettings AudioPreview { get; } } /// /// 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. /// internal sealed class RuntimeSettingsController : IRuntimeSettingsPreviewSource, ICombatGameplaySettingsSource { private const string DefaultToonKey = "default"; private readonly IRuntimeSettingsStorage _storage; private readonly Func _resolveQuality; private readonly Action _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? resolveQuality = null, Action? 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}"); } /// /// 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. /// 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 IDisposable BindRuntimeTargetsOwned(IRuntimeSettingsTargets targets) { BindRuntimeTargets(targets); return new RuntimeTargetBinding(this, targets); } private void UnbindRuntimeTargets(IRuntimeSettingsTargets expected) { if (ReferenceEquals(_runtimeTargets, expected)) _runtimeTargets = null; } public void UnbindRuntimeTargets() => _runtimeTargets = null; private sealed class RuntimeTargetBinding : IDisposable { private RuntimeSettingsController? _owner; private readonly IRuntimeSettingsTargets _expected; public RuntimeTargetBinding( RuntimeSettingsController owner, IRuntimeSettingsTargets expected) { _owner = owner; _expected = expected; } public void Dispose() => Interlocked.Exchange(ref _owner, null)? .UnbindRuntimeTargets(_expected); } public SettingsVM CreateViewModel( KeyBindings persistedBindings, InputDispatcher dispatcher, Action 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 saveBindings) => new( this, CreateViewModel(persistedBindings, dispatcher, saveBindings)); 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, }); } /// /// Retail ClientCommunicationSystem::DoFrameRate @ 0x005707D0 /// 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). /// 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) { ChatSettings previous = Chat; try { _storage.SaveChat(chat); Chat = chat; _log($"settings: chat saved to {_storage.Location}"); } catch (Exception ex) { _log($"settings: chat save failed: {ex.Message}"); return; } // 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 // the option AND joins/leaves the matching Turbine room. PublishHearOptionChange( previous.HearGeneralChat, chat.HearGeneralChat, (uint)CharacterOptionId.ListenToGeneralChat); PublishHearOptionChange( previous.HearTradeChat, chat.HearTradeChat, (uint)CharacterOptionId.ListenToTradeChat); PublishHearOptionChange( previous.HearLFGChat, chat.HearLFGChat, (uint)CharacterOptionId.ListenToLFGChat); PublishHearOptionChange( previous.HearRoleplayChat, chat.HearRoleplayChat, (uint)CharacterOptionId.ListenToRoleplayChat); PublishHearOptionChange( previous.HearSocietyChat, chat.HearSocietyChat, (uint)CharacterOptionId.ListenToSocietyChat); } private void PublishHearOptionChange(bool previous, bool current, uint optionId) { if (previous == current) return; _runtimeTargets?.SetSingleCharacterOption(optionId, current); } /// /// CH3 (2026-08-09): reseed the persisted + draft Chat snapshot from the /// server's own CharacterOptions2 bitfield (already parsed out of /// PlayerDescription) — called whenever a fresh description lands. N4 /// (CH3 Opus review, 2026-08-09) aligned /// to ACE's real default (Roleplay/Society start OFF server-side), but /// this sync remains the only way the checkbox reflects truth for a /// character whose PERSISTED settings.json diverges from the server — /// an older save, or a character whose allegiance/society membership /// changed since the file was last written. The server is always /// authoritative, regardless of what the local default or a stale save /// says. /// 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. ChatSettings Reseed(ChatSettings current) => current with { HearGeneralChat = (options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearGeneralChat) != 0u, HearTradeChat = (options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearTradeChat) != 0u, HearLFGChat = (options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearLFGChat) != 0u, HearRoleplayChat = (options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearRoleplayChat) != 0u, HearSocietyChat = (options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearSocietyChat) != 0u, }; ChatSettings synced = Reseed(Chat); if (synced == Chat) return; Chat = synced; _viewModel?.ApplyExternalChatChange(Reseed); try { _storage.SaveChat(synced); _log($"settings: chat synced from server options2=0x{options2:X8}"); } catch (Exception ex) { _log($"settings: chat sync 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)); }