(Feat) Add Remmaping

This commit is contained in:
2026-06-26 16:23:20 +02:00
parent 931bc9c9e2
commit 7bd2aa3120
249 changed files with 23712 additions and 6898 deletions
+181 -37
View File
@@ -3,19 +3,47 @@ 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;
@@ -41,7 +69,7 @@ namespace Ashwild.Settings
public bool InvertY { get; private set; }
public bool CrosshairEnabled { get; private set; }
// ── PlayerPrefs keys ────────────────────────────────────────
// ── 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";
@@ -56,6 +84,16 @@ namespace Ashwild.Settings
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()
{
@@ -67,10 +105,42 @@ namespace Ashwild.Settings
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)
@@ -105,14 +175,14 @@ namespace Ashwild.Settings
public void SetBrightness(float value)
{
Brightness = Mathf.Clamp(value, 0.5f, 1.5f);
Brightness = Mathf.Clamp(value, definition.Brightness.Min, definition.Brightness.Max);
Save();
onSettingsChanged?.Invoke();
}
public void SetMasterVolume(float value)
{
MasterVolume = Mathf.Clamp01(value);
MasterVolume = Mathf.Clamp(value, definition.MasterVolume.Min, definition.MasterVolume.Max);
ApplyMasterVolume();
Save();
onSettingsChanged?.Invoke();
@@ -120,7 +190,7 @@ namespace Ashwild.Settings
public void SetMusicVolume(float value)
{
MusicVolume = Mathf.Clamp01(value);
MusicVolume = Mathf.Clamp(value, definition.MusicVolume.Min, definition.MusicVolume.Max);
ApplyMusicVolume();
Save();
onSettingsChanged?.Invoke();
@@ -128,7 +198,7 @@ namespace Ashwild.Settings
public void SetSfxVolume(float value)
{
SfxVolume = Mathf.Clamp01(value);
SfxVolume = Mathf.Clamp(value, definition.SfxVolume.Min, definition.SfxVolume.Max);
ApplySfxVolume();
Save();
onSettingsChanged?.Invoke();
@@ -150,9 +220,13 @@ namespace Ashwild.Settings
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 = Mathf.Clamp(fps, 30, 360);
TargetFps = fps < 0 ? -1 : Mathf.Clamp(fps, 30, 360);
Application.targetFrameRate = TargetFps;
Save();
onSettingsChanged?.Invoke();
@@ -160,14 +234,14 @@ namespace Ashwild.Settings
public void SetFov(float fov)
{
Fov = Mathf.Clamp(fov, 60f, 110f);
Fov = Mathf.Clamp(fov, definition.Fov.Min, definition.Fov.Max);
Save();
onSettingsChanged?.Invoke();
}
public void SetSensitivity(float value)
{
Sensitivity = Mathf.Clamp(value, 0.05f, 2f);
Sensitivity = Mathf.Clamp(value, definition.Sensitivity.Min, definition.Sensitivity.Max);
Save();
onSettingsChanged?.Invoke();
}
@@ -186,52 +260,80 @@ namespace Ashwild.Settings
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 = PlayerPrefs.GetInt(K_RESOLUTION, defaultResolution);
FullscreenMode = (FullScreenMode)PlayerPrefs.GetInt(K_FULLSCREEN, (int)FullScreenMode.FullScreenWindow);
MonitorIndex = PlayerPrefs.GetInt(K_MONITOR, 0);
Brightness = PlayerPrefs.GetFloat(K_BRIGHTNESS, 1f);
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 = PlayerPrefs.GetFloat(K_MASTER, 1f);
MusicVolume = PlayerPrefs.GetFloat(K_MUSIC, 0.8f);
SfxVolume = PlayerPrefs.GetFloat(K_SFX, 1f);
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 = PlayerPrefs.GetInt(K_QUALITY, QualitySettings.GetQualityLevel());
VSync = PlayerPrefs.GetInt(K_VSYNC, 1) == 1;
TargetFps = PlayerPrefs.GetInt(K_TARGET_FPS, 144);
Fov = PlayerPrefs.GetFloat(K_FOV, 75f);
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 = PlayerPrefs.GetFloat(K_SENS, 0.15f);
InvertY = PlayerPrefs.GetInt(K_INVERT_Y, 0) == 1;
CrosshairEnabled = PlayerPrefs.GetInt(K_CROSSHAIR, 1) == 1;
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()
{
PlayerPrefs.SetInt(K_RESOLUTION, ResolutionIndex);
PlayerPrefs.SetInt(K_FULLSCREEN, (int)FullscreenMode);
PlayerPrefs.SetInt(K_MONITOR, MonitorIndex);
PlayerPrefs.SetFloat(K_BRIGHTNESS, Brightness);
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);
PlayerPrefs.SetFloat(K_MASTER, MasterVolume);
PlayerPrefs.SetFloat(K_MUSIC, MusicVolume);
PlayerPrefs.SetFloat(K_SFX, SfxVolume);
ES3.Save(K_MASTER, MasterVolume, cache);
ES3.Save(K_MUSIC, MusicVolume, cache);
ES3.Save(K_SFX, SfxVolume, cache);
PlayerPrefs.SetInt(K_QUALITY, QualityLevel);
PlayerPrefs.SetInt(K_VSYNC, VSync ? 1 : 0);
PlayerPrefs.SetInt(K_TARGET_FPS, TargetFps);
PlayerPrefs.SetFloat(K_FOV, Fov);
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);
PlayerPrefs.SetFloat(K_SENS, Sensitivity);
PlayerPrefs.SetInt(K_INVERT_Y, InvertY ? 1 : 0);
PlayerPrefs.SetInt(K_CROSSHAIR, CrosshairEnabled ? 1 : 0);
ES3.Save(K_SENS, Sensitivity, cache);
ES3.Save(K_INVERT_Y, InvertY, cache);
ES3.Save(K_CROSSHAIR, CrosshairEnabled, cache);
}
PlayerPrefs.Save();
/// <summary>
/// Writes the in-memory cache out to the save file.
/// </summary>
private void Flush()
{
if (cache != null) ES3.StoreCachedFile();
}
private void ApplyAll()
@@ -262,6 +364,48 @@ namespace Ashwild.Settings
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);