422 lines
18 KiB
C#
422 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using UnityEngine.Events;
|
|
using Ashwild.Core;
|
|
using Ashwild.Player;
|
|
|
|
namespace Ashwild.Settings
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// The authored settings data (defaults, clamps, label hints). Read by the settings
|
|
/// sub-panels to configure their widgets without hardcoding ranges or formats.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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 ─────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void Start()
|
|
{
|
|
InitInput();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flushes the settings cache to disk when the app is backgrounded (covers mobile/alt-tab).
|
|
/// </summary>
|
|
private void OnApplicationPause(bool paused)
|
|
{
|
|
if (paused) Flush();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flushes the settings cache to disk on quit.
|
|
/// </summary>
|
|
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<DisplayInfo> displays = new List<DisplayInfo>();
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the frame-rate cap. A negative value means "unlimited" (Unity treats
|
|
/// <c>targetFrameRate = -1</c> as no cap); any other value is clamped to a sane range.
|
|
/// </summary>
|
|
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 ─────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors every setting into the ES3 cache (a cheap memory write). Disk is only touched by
|
|
/// <see cref="Flush"/> on pause/quit.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the in-memory cache out to the save file.
|
|
/// </summary>
|
|
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<DisplayInfo> displays = new List<DisplayInfo>();
|
|
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 ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>(K_INPUT_OVERRIDES, cache));
|
|
|
|
if (!ES3.Load(K_LAYOUT_INIT, false, cache))
|
|
{
|
|
ApplyFirstLaunchLayout(input);
|
|
ES3.Save(K_LAYOUT_INIT, true, cache);
|
|
Flush();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|