using System; using System.IO; using BriarQueen.Data.Identifiers; using BriarQueen.Data.IO; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Events.Save; using BriarQueen.Framework.Managers.Audio; using BriarQueen.Framework.Services.Settings.Data; using Cysharp.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; using VContainer; using AudioSettings = BriarQueen.Framework.Services.Settings.Data.AudioSettings; namespace BriarQueen.Framework.Services.Settings { public class SettingsService { private const string AUDIO_FILE = "Audio.json"; private const string VISUAL_FILE = "Visual.json"; private const string GAME_FILE = "Game.json"; private readonly AudioManager _audioManager; private readonly EventCoordinator _eventCoordinator; [Inject] public SettingsService(EventCoordinator eventCoordinator, AudioManager audioManager) { _eventCoordinator = eventCoordinator; _audioManager = audioManager; } public AudioSettings Audio { get; private set; } public VisualSettings Visual { get; private set; } public GameSettings Game { get; private set; } public async UniTask InitializeAsync() { #if UNITY_EDITOR Debug.Log($"[{nameof(SettingsService)}] Initializing..."); #endif EnsureDirectories(); await LoadAllSettings(); Apply(Audio, Visual, Game, false, false); #if UNITY_EDITOR Debug.Log($"[{nameof(SettingsService)}] Initialized."); #endif } private void EnsureDirectories() { Directory.CreateDirectory(FilePaths.ConfigFolder); Directory.CreateDirectory(FilePaths.SaveDataFolder); Directory.CreateDirectory(FilePaths.SaveBackupDataFolder); } public void Apply( AudioSettings audio, VisualSettings visual, GameSettings game, bool saveToDisk = true, bool fireEvent = true) { Audio = audio ?? new AudioSettings(); Visual = visual ?? new VisualSettings(); Game = game ?? new GameSettings(); ApplyAudio(Audio); ApplyVisual(Visual); ApplyGame(Game); if (saveToDisk) { SaveSettingsFile(Audio, AUDIO_FILE).Forget(); SaveSettingsFile(Visual, VISUAL_FILE).Forget(); SaveSettingsFile(Game, GAME_FILE).Forget(); } if (fireEvent) _eventCoordinator.Publish(new SettingsChangedEvent()); } public float GetVolume(string mixerGroup) { return mixerGroup switch { AudioMixerGroups.MASTER_GROUP => Audio.MasterVolume, AudioMixerGroups.MUSIC_GROUP => Audio.MusicVolume, AudioMixerGroups.SFX_GROUP => Audio.SfxVolume, AudioMixerGroups.UI_GROUP => Audio.UIVolume, AudioMixerGroups.VOICE_GROUP => Audio.VoiceVolume, _ => Audio.MasterVolume }; } public bool AreTutorialsEnabled() { return Game != null && Game.TutorialsEnabled; } public bool AreTooltipsEnabled() { return Game != null && Game.TooltipsEnabled; } private void ApplyAudio(AudioSettings a) { _audioManager.SetVolume(AudioMixerParameters.MASTER_VOLUME, a.MasterVolume); _audioManager.SetVolume(AudioMixerParameters.MUSIC_VOLUME, a.MusicVolume); _audioManager.SetVolume(AudioMixerParameters.SFX_VOLUME, a.SfxVolume); _audioManager.SetVolume(AudioMixerParameters.VOICE_VOLUME, a.VoiceVolume); _audioManager.SetVolume(AudioMixerParameters.AMBIENCE_VOLUME, a.AmbienceVolume); _audioManager.SetVolume(AudioMixerParameters.UI_VOLUME, a.UIVolume); Application.runInBackground = !a.MuteWhenUnfocused; } private void ApplyVisual(VisualSettings v) { SanitizeVisualSettings(v); var currentRes = Screen.currentResolution; var rr = new RefreshRate { numerator = currentRes.refreshRateRatio.numerator, denominator = currentRes.refreshRateRatio.denominator }; Screen.SetResolution(currentRes.width, currentRes.height, v.FullScreenMode, rr); QualitySettings.vSyncCount = v.VSyncCount; // On many desktop platforms, VSync will take precedence over targetFrameRate. Application.targetFrameRate = v.MaxFramerate; } private void ApplyGame(GameSettings g) { if (g.PopupDisplayDuration <= 0f) g.PopupDisplayDuration = 3f; // TutorialsEnabled and TooltipsEnabled are gameplay/UI preferences. // They do not need an immediate engine-level apply step here. // Systems that show tutorials/tooltips should query SettingsManager.Game // or use the helper methods above. } private async UniTask LoadAllSettings() { Audio = await LoadOrCreateSettingsFiles(AUDIO_FILE); Visual = await LoadOrCreateSettingsFiles(VISUAL_FILE); Game = await LoadOrCreateSettingsFiles(GAME_FILE); Audio ??= new AudioSettings(); Visual ??= new VisualSettings(); Game ??= new GameSettings(); SanitizeVisualSettings(Visual); if (Game.PopupDisplayDuration <= 0f) Game.PopupDisplayDuration = 3f; } private void SanitizeVisualSettings(VisualSettings v) { if (v == null) return; v.VSyncCount = Mathf.Clamp(v.VSyncCount, 0, 2); // Treat 0 or any negative value other than -1 as invalid and fall back to 120. // If you want to support -1 as "platform default", keep it. Otherwise, normalize it too. if (v.MaxFramerate == 0 || v.MaxFramerate < -1) v.MaxFramerate = 120; if (v.MaxFramerate > 0) v.MaxFramerate = Mathf.Clamp(v.MaxFramerate, 30, 240); } private async UniTask LoadOrCreateSettingsFiles(string fileName) where T : class, new() { var filePath = Path.Combine(FilePaths.ConfigFolder, fileName); T settings; if (File.Exists(filePath)) { try { var jsonString = await File.ReadAllTextAsync(filePath); settings = JsonConvert.DeserializeObject(jsonString); } catch (Exception e) { Debug.LogError($"[SettingsManager] Failed to load {fileName}. Creating new. Error: {e.Message}"); settings = new T(); await SaveSettingsFile(settings, fileName); } } else { settings = new T(); await SaveSettingsFile(settings, fileName); } return settings ?? new T(); } private async UniTask SaveSettingsFile(T settings, string fileName) where T : class { if (settings == null) return; var filePath = Path.Combine(FilePaths.ConfigFolder, fileName); Directory.CreateDirectory(FilePaths.ConfigFolder); try { var jsonString = JsonConvert.SerializeObject(settings, Formatting.Indented); await File.WriteAllTextAsync(filePath, jsonString); } catch (Exception e) { Debug.LogError($"[SettingsManager] Failed to save {fileName}. Error: {e.Message}"); } } } }