using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; using UnityEngine.Events; using Ashwild.Core; using Ashwild.Player; namespace Ashwild.Settings { /// /// Owns every persisted setting and is the single place that touches disk (via Easy Save 3). /// Values live in memory and are mirrored into the ES3 cache on change; the cache is flushed to /// disk on pause/quit so dragging a slider doesn't hammer the file. Also owns input concerns /// that are settings, not runtime: it persists the keybinding overrides and, on first launch, /// detects the keyboard layout and asks the InputManager to apply the matching profile. /// public class SettingsManager : MonoBehaviour { public static SettingsManager Instance { get; private set; } [Header("Definition")] [Tooltip("Authored defaults, clamp ranges and label hints. Must be assigned — read in Awake.")] [SerializeField] private SettingsDefinition definition; /// /// The authored settings data (defaults, clamps, label hints). Read by the settings /// sub-panels to configure their widgets without hardcoding ranges or formats. /// public SettingsDefinition Definition => definition; [Header("Audio Mixer")] [SerializeField] private AudioMixer audioMixer; [SerializeField] private string masterParam = "MasterVol"; [SerializeField] private string musicParam = "MusicVol"; [SerializeField] private string sfxParam = "SfxVol"; [Header("Input")] [Tooltip("Single input config asset: rebindable bindings, key glyphs, first-launch layout overrides.")] [SerializeField] private InputConfig inputConfig; /// /// The shared input config (rebindable bindings, key glyphs, layout overrides). Read by the /// keybindings sub-panel so the asset is assigned here only, not duplicated on the panel. /// public InputConfig InputConfig => inputConfig; [Header("Events")] public UnityEvent onSettingsChanged; // ── Display ───────────────────────────────────────────────── public int ResolutionIndex { get; private set; } public FullScreenMode FullscreenMode { get; private set; } public int MonitorIndex { get; private set; } public float Brightness { get; private set; } // ── Audio ─────────────────────────────────────────────────── public float MasterVolume { get; private set; } public float MusicVolume { get; private set; } public float SfxVolume { get; private set; } // ── Graphics ──────────────────────────────────────────────── public int QualityLevel { get; private set; } public bool VSync { get; private set; } public int TargetFps { get; private set; } public float Fov { get; private set; } // ── Controls ──────────────────────────────────────────────── public float Sensitivity { get; private set; } public bool InvertY { get; private set; } public bool CrosshairEnabled { get; private set; } // ── Save keys ─────────────────────────────────────────────── private const string K_RESOLUTION = "settings.display.resolutionIndex"; private const string K_FULLSCREEN = "settings.display.fullscreenMode"; private const string K_MONITOR = "settings.display.monitorIndex"; private const string K_BRIGHTNESS = "settings.display.brightness"; private const string K_MASTER = "settings.audio.master"; private const string K_MUSIC = "settings.audio.music"; private const string K_SFX = "settings.audio.sfx"; private const string K_QUALITY = "settings.graphics.qualityLevel"; private const string K_VSYNC = "settings.graphics.vsync"; private const string K_TARGET_FPS = "settings.graphics.targetFps"; private const string K_FOV = "settings.graphics.fov"; private const string K_SENS = "settings.controls.sensitivity"; private const string K_INVERT_Y = "settings.controls.invertY"; private const string K_CROSSHAIR = "settings.controls.crosshair"; private const string K_INPUT_OVERRIDES = "settings.input.overrides"; private const string K_LAYOUT_INIT = "settings.input.layoutInitialized"; // ── Persistence ───────────────────────────────────────────── /// /// ES3 settings pointing at the in-memory cache, so every Save is a cheap memory write; /// the cache is flushed to the file on pause/quit (and immediately after keybind changes). /// private ES3Settings cache; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; Persistence.Persist(gameObject); if (definition == null) { Debug.LogError($"[SettingsManager] No SettingsDefinition assigned on '{name}' — cannot load defaults.", this); return; } cache = new ES3Settings(ES3.Location.Cache); if (ES3.FileExists()) ES3.CacheFile(); LoadAll(); ApplyAll(); } /// /// Loads and applies the persisted keybindings once the InputManager exists, then runs the /// one-time keyboard-layout detection. Done in Start so InputManager.Awake has resolved the /// asset first. /// private void Start() { InitInput(); } /// /// Flushes the settings cache to disk when the app is backgrounded (covers mobile/alt-tab). /// private void OnApplicationPause(bool paused) { if (paused) Flush(); } /// /// Flushes the settings cache to disk on quit. /// private void OnApplicationQuit() => Flush(); // ── Public setters ────────────────────────────────────────── public void SetResolution(int index) { Resolution[] resolutions = Screen.resolutions; if (index < 0 || index >= resolutions.Length) return; ResolutionIndex = index; Resolution r = resolutions[index]; Screen.SetResolution(r.width, r.height, FullscreenMode, r.refreshRateRatio); Save(); onSettingsChanged?.Invoke(); } public void SetFullscreenMode(FullScreenMode mode) { FullscreenMode = mode; Screen.fullScreenMode = mode; Save(); onSettingsChanged?.Invoke(); } public void SetMonitor(int index) { List displays = new List(); Screen.GetDisplayLayout(displays); if (index < 0 || index >= displays.Count) return; MonitorIndex = index; Screen.MoveMainWindowTo(displays[index], Vector2Int.zero); Save(); onSettingsChanged?.Invoke(); } public void SetBrightness(float value) { Brightness = Mathf.Clamp(value, definition.Brightness.Min, definition.Brightness.Max); Save(); onSettingsChanged?.Invoke(); } public void SetMasterVolume(float value) { MasterVolume = Mathf.Clamp(value, definition.MasterVolume.Min, definition.MasterVolume.Max); ApplyMasterVolume(); Save(); onSettingsChanged?.Invoke(); } public void SetMusicVolume(float value) { MusicVolume = Mathf.Clamp(value, definition.MusicVolume.Min, definition.MusicVolume.Max); ApplyMusicVolume(); Save(); onSettingsChanged?.Invoke(); } public void SetSfxVolume(float value) { SfxVolume = Mathf.Clamp(value, definition.SfxVolume.Min, definition.SfxVolume.Max); ApplySfxVolume(); Save(); onSettingsChanged?.Invoke(); } public void SetQualityLevel(int level) { QualityLevel = Mathf.Clamp(level, 0, QualitySettings.names.Length - 1); QualitySettings.SetQualityLevel(QualityLevel, true); Save(); onSettingsChanged?.Invoke(); } public void SetVSync(bool enabled) { VSync = enabled; QualitySettings.vSyncCount = enabled ? 1 : 0; Save(); onSettingsChanged?.Invoke(); } /// /// Sets the frame-rate cap. A negative value means "unlimited" (Unity treats /// targetFrameRate = -1 as no cap); any other value is clamped to a sane range. /// public void SetTargetFps(int fps) { TargetFps = fps < 0 ? -1 : Mathf.Clamp(fps, 30, 360); Application.targetFrameRate = TargetFps; Save(); onSettingsChanged?.Invoke(); } public void SetFov(float fov) { Fov = Mathf.Clamp(fov, definition.Fov.Min, definition.Fov.Max); Save(); onSettingsChanged?.Invoke(); } public void SetSensitivity(float value) { Sensitivity = Mathf.Clamp(value, definition.Sensitivity.Min, definition.Sensitivity.Max); Save(); onSettingsChanged?.Invoke(); } public void SetInvertY(bool enabled) { InvertY = enabled; Save(); onSettingsChanged?.Invoke(); } public void SetCrosshairEnabled(bool enabled) { CrosshairEnabled = enabled; Save(); onSettingsChanged?.Invoke(); } // ── Keybindings persistence ───────────────────────────────── /// /// Persists the InputManager's current binding overrides (called by the rebind panel after a /// rebind or reset). Flushes immediately since rebinds are infrequent and worth keeping safe. /// public void SaveKeybinds() { InputManager input = InputManager.Instance; if (input == null) return; ES3.Save(K_INPUT_OVERRIDES, input.GetOverridesJson(), cache); Flush(); } // ── Load / Save / Apply ───────────────────────────────────── private void LoadAll() { int defaultResolution = Mathf.Max(0, Screen.resolutions.Length - 1); int defaultQuality = definition.DefaultQualityLevel < 0 ? QualitySettings.GetQualityLevel() : definition.DefaultQualityLevel; ResolutionIndex = ES3.Load(K_RESOLUTION, defaultResolution, cache); FullscreenMode = (FullScreenMode)ES3.Load(K_FULLSCREEN, (int)definition.DefaultFullscreenMode, cache); MonitorIndex = ES3.Load(K_MONITOR, 0, cache); Brightness = ES3.Load(K_BRIGHTNESS, definition.Brightness.DefaultValue, cache); MasterVolume = ES3.Load(K_MASTER, definition.MasterVolume.DefaultValue, cache); MusicVolume = ES3.Load(K_MUSIC, definition.MusicVolume.DefaultValue, cache); SfxVolume = ES3.Load(K_SFX, definition.SfxVolume.DefaultValue, cache); QualityLevel = ES3.Load(K_QUALITY, defaultQuality, cache); VSync = ES3.Load(K_VSYNC, definition.DefaultVSync, cache); TargetFps = ES3.Load(K_TARGET_FPS, definition.TargetFps.DefaultValue, cache); Fov = ES3.Load(K_FOV, definition.Fov.DefaultValue, cache); Sensitivity = ES3.Load(K_SENS, definition.Sensitivity.DefaultValue, cache); InvertY = ES3.Load(K_INVERT_Y, definition.DefaultInvertY, cache); CrosshairEnabled = ES3.Load(K_CROSSHAIR, definition.DefaultCrosshairEnabled, cache); } /// /// Mirrors every setting into the ES3 cache (a cheap memory write). Disk is only touched by /// on pause/quit. /// private void Save() { ES3.Save(K_RESOLUTION, ResolutionIndex, cache); ES3.Save(K_FULLSCREEN, (int)FullscreenMode, cache); ES3.Save(K_MONITOR, MonitorIndex, cache); ES3.Save(K_BRIGHTNESS, Brightness, cache); ES3.Save(K_MASTER, MasterVolume, cache); ES3.Save(K_MUSIC, MusicVolume, cache); ES3.Save(K_SFX, SfxVolume, cache); ES3.Save(K_QUALITY, QualityLevel, cache); ES3.Save(K_VSYNC, VSync, cache); ES3.Save(K_TARGET_FPS, TargetFps, cache); ES3.Save(K_FOV, Fov, cache); ES3.Save(K_SENS, Sensitivity, cache); ES3.Save(K_INVERT_Y, InvertY, cache); ES3.Save(K_CROSSHAIR, CrosshairEnabled, cache); } /// /// Writes the in-memory cache out to the save file. /// private void Flush() { if (cache != null) ES3.StoreCachedFile(); } private void ApplyAll() { // Display Resolution[] resolutions = Screen.resolutions; if (ResolutionIndex >= 0 && ResolutionIndex < resolutions.Length) { Resolution r = resolutions[ResolutionIndex]; Screen.SetResolution(r.width, r.height, FullscreenMode, r.refreshRateRatio); } if (MonitorIndex > 0) { List displays = new List(); Screen.GetDisplayLayout(displays); if (MonitorIndex < displays.Count) Screen.MoveMainWindowTo(displays[MonitorIndex], Vector2Int.zero); } // Audio ApplyMasterVolume(); ApplyMusicVolume(); ApplySfxVolume(); // Graphics QualitySettings.SetQualityLevel(QualityLevel, true); QualitySettings.vSyncCount = VSync ? 1 : 0; Application.targetFrameRate = TargetFps; } // ── Input init ────────────────────────────────────────────── /// /// Restores saved keybindings onto the shared input asset, then applies the first-launch /// layout profile if this is the very first run and the player has no saved overrides. /// private void InitInput() { InputManager input = InputManager.Instance; if (input == null) { Debug.LogWarning("[SettingsManager] InputManager not found — keybindings not restored.", this); return; } if (ES3.KeyExists(K_INPUT_OVERRIDES, cache)) input.LoadOverridesJson(ES3.Load(K_INPUT_OVERRIDES, cache)); if (!ES3.Load(K_LAYOUT_INIT, false, cache)) { ApplyFirstLaunchLayout(input); ES3.Save(K_LAYOUT_INIT, true, cache); Flush(); } } /// /// Detects the keyboard layout and asks the InputManager to apply the matching profile. /// Skipped when the player already has saved overrides (a returning user is never stomped). /// Note: physical key paths are already correct on AZERTY, so profiles are usually empty — /// the real layout adaptation happens through the layout-aware key labels in the UI. /// private void ApplyFirstLaunchLayout(InputManager input) { if (ES3.KeyExists(K_INPUT_OVERRIDES, cache)) return; if (inputConfig == null) return; KeyboardLayout layout = KeyboardLayoutDetector.Detect(); if (input.ApplyLayoutOverrides(inputConfig.OverridesFor(layout))) ES3.Save(K_INPUT_OVERRIDES, input.GetOverridesJson(), cache); } // ── Audio helpers ─────────────────────────────────────────── private void ApplyMasterVolume() => SetMixer(masterParam, MasterVolume); private void ApplyMusicVolume() => SetMixer(musicParam, MusicVolume); private void ApplySfxVolume() => SetMixer(sfxParam, SfxVolume); private void SetMixer(string param, float linear) { if (audioMixer == null || string.IsNullOrEmpty(param)) return; audioMixer.SetFloat(param, Mathf.Log10(Mathf.Max(linear, 0.0001f)) * 20f); } } }