using System; using System.IO; using UnityEngine; namespace ProjectM.Client { /// /// Client-local settings persistence + application. Loads settings.json from /// Application.persistentDataPath at boot () and applies /// Graphics (Screen / QualitySettings / targetFrameRate) + Audio (AudioListener.volume = Master; /// Music/Sfx bus trims). The single source of truth for the UITK /// SettingsScreen shared by the main menu + the in-game pause overlay. Saves are atomic /// (temp file + File.Replace). JsonUtility keeps it asmdef-ref-free. Never replicated. /// public static class SettingsService { public static GameSettings Current { get; private set; } = GameSettings.Defaults(); static string FilePath => Path.Combine(Application.persistentDataPath, "settings.json"); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void Boot() { Load(); Apply(Current); } /// Read settings from disk (or defaults if absent/corrupt). Returns the loaded value. public static GameSettings Load() { try { if (File.Exists(FilePath)) { var json = File.ReadAllText(FilePath); var s = JsonUtility.FromJson(json); if (s.Version != GameSettings.CurrentVersion) s = Migrate(s); Current = s.Clamped(); } else { Current = GameSettings.Defaults(); } } catch (Exception e) { Debug.LogWarning($"[SettingsService] Load failed ({e.Message}); using defaults."); Current = GameSettings.Defaults(); } return Current; } /// Clamp + version-stamp + atomically write to disk; updates . public static void Save(GameSettings settings) { settings = settings.Clamped(); settings.Version = GameSettings.CurrentVersion; Current = settings; try { var json = JsonUtility.ToJson(settings, true); var tmp = FilePath + ".tmp"; File.WriteAllText(tmp, json); if (File.Exists(FilePath)) File.Replace(tmp, FilePath, null); else File.Move(tmp, FilePath); } catch (Exception e) { Debug.LogWarning($"[SettingsService] Save failed: {e.Message}"); } } /// Apply settings to the live engine. Master rides AudioListener.volume; buses are per-call trims. public static void Apply(GameSettings s) { s = s.Clamped(); // Graphics QualitySettings.SetQualityLevel(s.QualityLevel, true); QualitySettings.vSyncCount = s.VSync; var mode = (FullScreenMode)s.FullScreenMode; if (s.RefreshHz > 0) Screen.SetResolution(s.ResWidth, s.ResHeight, mode, new RefreshRate { numerator = (uint)s.RefreshHz, denominator = 1 }); else Screen.SetResolution(s.ResWidth, s.ResHeight, mode); Application.targetFrameRate = s.TargetFps; // -1 = uncapped (ignored while vSync > 0) // Audio AudioListener.volume = s.Master; GameVolume.Music = s.Music; GameVolume.Sfx = s.Sfx; } /// Convenience for the settings UI: persist then apply. public static void SaveAndApply(GameSettings s) { Save(s); Apply(Current); } // Forward-compatible migration: fill from current defaults, preserve any recognizable old values. // Additive-only as the schema grows (never throws on an unknown version). static GameSettings Migrate(GameSettings old) { var def = GameSettings.Defaults(); if (old.ResWidth > 0) def.ResWidth = old.ResWidth; if (old.ResHeight > 0) def.ResHeight = old.ResHeight; if (old.Master > 0f) def.Master = old.Master; if (old.Music > 0f) def.Music = old.Music; if (old.Sfx > 0f) def.Sfx = old.Sfx; def.Version = GameSettings.CurrentVersion; return def; } } }