using System.Collections; using System.Linq; using Core.SettingsSystem; using Michsky.UI.Reach; using UnityEngine; using UI.PanelStack; using Core.InputLock; namespace UI.Settings { public class SettingsController : MonoBehaviour { private struct ResolutionOption { public int width; public int height; public int refreshRate; } [SerializeField] private bool debugLogs = true; [Header("Root")] [SerializeField] private GameObject rootObject; [SerializeField] private PanelManager panelManager; [Header("Hotkeys")] [SerializeField] private HotkeyEvent escapeHotkey; [Header("Buttons (Optional)")] [SerializeField] private ButtonManager backButton; [SerializeField] private ButtonManager applyButton; [SerializeField] private ButtonManager resetButton; [Header("Graphics - Dropdowns")] [SerializeField] private Dropdown resolutionDropdown; [SerializeField] private Dropdown displayModeDropdown; [SerializeField] private Dropdown qualityDropdown; [SerializeField] private Dropdown vSyncDropdown; [SerializeField] private Dropdown msaaDropdown; [SerializeField] private Dropdown textureQualityDropdown; [Header("Graphics - Sliders")] [SerializeField] private SliderManager targetFpsSlider; [SerializeField] private SliderManager renderScaleSlider; [SerializeField] private SliderManager shadowDistanceSlider; [Header("Audio - Sliders")] [SerializeField] private SliderManager masterVolumeSlider; [SerializeField] private SliderManager musicVolumeSlider; [SerializeField] private SliderManager sfxVolumeSlider; [Header("Gameplay - Sliders")] [SerializeField] private SliderManager mouseSensitivitySlider; [Header("Gameplay - Dropdowns")] [SerializeField] private Dropdown llmProviderDropdown; [Header("Auto Initialize")] [SerializeField] private bool initResolutionDropdown = true; [SerializeField] private bool initQualityDropdown = true; [SerializeField] private bool delayRefreshOneFrameOnEnable = true; [SerializeField] private bool startHidden = true; [Header("Dropdown Item Source")] [SerializeField] private bool rebuildResolutionItems = true; [SerializeField] private bool rebuildDisplayModeItems = true; [SerializeField] private bool rebuildQualityItems = true; [SerializeField] private bool rebuildVSyncItems = true; [SerializeField] private bool rebuildMsaaItems = true; [SerializeField] private bool rebuildTextureQualityItems = true; [SerializeField] private bool rebuildLlmProviderItems = true; [Header("Ranges")] [SerializeField] private Vector2Int targetFpsRange = new Vector2Int(1, 240); [SerializeField] private Vector2 renderScalePercentRange = new Vector2(0.1f, 200f); [SerializeField] private Vector2 shadowDistanceRange = new Vector2(0f, 150f); [SerializeField] private Vector2 sensitivityRange = new Vector2(0.1f, 10f); private SettingsData applied; private SettingsData working; private bool isDirty; private bool isWired; private bool isPopulating; private bool isClosing; private ResolutionOption[] resolutionOptions = System.Array.Empty(); private float RenderScalePercentMin => Mathf.Max(renderScalePercentRange.x, 10f); private float RenderScalePercentMax => Mathf.Max(RenderScalePercentMin, renderScalePercentRange.y); private void Awake() { WireOnce(); if (startHidden) { ShowRoot(false); } if (debugLogs) Debug.Log($"[SettingsController] Awake. startHidden={startHidden} rootActive={(rootObject!=null?rootObject.activeSelf:gameObject.activeSelf)}"); } private void OnEnable() { WireOnce(); ShowRoot(true); if (delayRefreshOneFrameOnEnable) { StartCoroutine(RefreshNextFrame()); } else { RefreshFromApplied(); } } private IEnumerator RefreshNextFrame() { yield return null; RefreshFromApplied(); } public void Show() { RefreshFromApplied(); ShowRoot(true); if (panelManager != null) { panelManager.ShowCurrentPanel(); } } public void SetResolutionIndex(int index) { OnResolutionChanged(index); } public void SetDisplayModeIndex(int index) { OnDisplayModeChanged(index); } public void SetQualityIndex(int index) { OnQualityChanged(index); } public void SetVSyncIndex(int index) { OnVSyncChanged(index); } public void SetMsaaIndex(int index) { OnMsaaChanged(index); } public void SetTextureQualityIndex(int index) { OnTextureQualityChanged(index); } public void SetTargetFps(float value) { OnTargetFpsChanged(value); } public void SetRenderScalePercent(float value) { OnRenderScalePercentChanged(value); } public void SetShadowDistance(float value) { OnShadowDistanceChanged(value); } public void SetMasterVolume(float value) { OnMasterChanged(value); } public void SetMusicVolume(float value) { OnMusicChanged(value); } public void SetSfxVolume(float value) { OnSfxChanged(value); } public void SetMouseSensitivity(float value) { OnSensitivityChanged(value); } public void SetLLMProviderIndex(int index) { OnLLMProviderChanged(index); } public void CloseOrBack() { if (isClosing) { return; } isClosing = true; if (debugLogs) Debug.Log($"[SettingsController] CloseOrBack called. panelManagerActive={(panelManager!=null && panelManager.gameObject.activeInHierarchy)}"); if (isDirty) { working = Clone(applied); PopulateUI(working); isDirty = false; } if (UIPanelStack.Instance != null) { UIPanelStack.Instance.Remove(rootObject != null ? rootObject : gameObject); } if (PlayerControlLockService.Instance != null) { PlayerControlLockService.Instance.Unlock(this); } float delay = 0f; if (panelManager != null && panelManager.isActiveAndEnabled && panelManager.gameObject.activeInHierarchy) { panelManager.HideCurrentPanel(); delay = panelManager.cachedStateLength * panelManager.animationSpeed; } if (rootObject != null) { StartCoroutine(HideAfter(delay)); } else { StartCoroutine(HideAfter(delay)); } } public void OpenAtIndex(int index) { isClosing = false; if (debugLogs) Debug.Log($"[SettingsController] OpenAtIndex({index}) called. panelManager={(panelManager!=null)}"); ShowRoot(true); if (panelManager != null && !panelManager.gameObject.activeInHierarchy) { panelManager.gameObject.SetActive(true); } if (panelManager != null) { panelManager.OpenPanelByIndex(index); } if (delayRefreshOneFrameOnEnable) { StartCoroutine(RefreshNextFrame()); } else { RefreshFromApplied(); } if (UIPanelStack.Instance != null) { UIPanelStack.Instance.Push(UIPanelKind.Settings, rootObject != null ? rootObject : gameObject, CloseOrBack); } if (PlayerControlLockService.Instance != null) { PlayerControlLockService.Instance.Lock(this); } } public void Apply() { SettingsService service = SettingsService.Instance; if (service == null) { return; } if (debugLogs) Debug.Log($"[SettingsController] Apply clicked. gameplay.mouseSensitivity={working?.gameplay?.mouseSensitivity}"); service.ApplyAndSave(Clone(working)); applied = Clone(service.Current); isDirty = false; } public void ResetToDefaults() { working = SettingsData.Default(); PopulateUI(working); isDirty = true; } private IEnumerator HideAfter(float seconds) { if (seconds > 0f) { yield return new WaitForSecondsRealtime(seconds); } ShowRoot(false); isClosing = false; } private void ShowRoot(bool show) { if (rootObject != null) { rootObject.SetActive(show); } else { gameObject.SetActive(show); } if (debugLogs) Debug.Log($"[SettingsController] ShowRoot({show})."); } private void RefreshFromApplied() { SettingsService service = SettingsService.Instance; applied = service != null ? Clone(service.Current) : SettingsData.Default(); working = Clone(applied); EnsureDropdownItems(); ApplyRanges(); PopulateUI(working); isDirty = false; } private void WireOnce() { if (isWired) { return; } if (escapeHotkey != null) { escapeHotkey.onHotkeyPress.AddListener(CloseOrBack); } if (backButton != null) { backButton.onClick.AddListener(CloseOrBack); } if (applyButton != null) { applyButton.onClick.AddListener(Apply); } if (resetButton != null) { resetButton.onClick.AddListener(ResetToDefaults); } if (resolutionDropdown != null) { resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); } if (displayModeDropdown != null) { displayModeDropdown.onValueChanged.AddListener(OnDisplayModeChanged); } if (qualityDropdown != null) { qualityDropdown.onValueChanged.AddListener(OnQualityChanged); } if (vSyncDropdown != null) { vSyncDropdown.onValueChanged.AddListener(OnVSyncChanged); } if (msaaDropdown != null) { msaaDropdown.onValueChanged.AddListener(OnMsaaChanged); } if (textureQualityDropdown != null) { textureQualityDropdown.onValueChanged.AddListener(OnTextureQualityChanged); } if (targetFpsSlider != null) { targetFpsSlider.onValueChanged.AddListener(OnTargetFpsChanged); } if (renderScaleSlider != null) { renderScaleSlider.onValueChanged.AddListener(OnRenderScalePercentChanged); } if (shadowDistanceSlider != null) { shadowDistanceSlider.onValueChanged.AddListener(OnShadowDistanceChanged); } if (masterVolumeSlider != null) { masterVolumeSlider.onValueChanged.AddListener(OnMasterChanged); } if (musicVolumeSlider != null) { musicVolumeSlider.onValueChanged.AddListener(OnMusicChanged); } if (sfxVolumeSlider != null) { sfxVolumeSlider.onValueChanged.AddListener(OnSfxChanged); } if (mouseSensitivitySlider != null) { mouseSensitivitySlider.onValueChanged.AddListener(OnSensitivityChanged); } if (llmProviderDropdown != null) { llmProviderDropdown.onValueChanged.AddListener(OnLLMProviderChanged); } isWired = true; } private void EnsureDropdownItems() { if (initResolutionDropdown && resolutionDropdown != null && (rebuildResolutionItems || resolutionDropdown.items == null || resolutionDropdown.items.Count == 0)) { resolutionDropdown.items.Clear(); var resolutions = Screen.resolutions; int currentWidth = Screen.width; int currentHeight = Screen.height; #if UNITY_2022_2_OR_NEWER int currentRefresh = Mathf.RoundToInt((float)Screen.currentResolution.refreshRateRatio.value); #else int currentRefresh = Screen.currentResolution.refreshRate; #endif var grouped = resolutions .GroupBy(r => new Vector2Int(r.width, r.height)) .Select(g => { var list = g.ToArray(); int bestIndex = 0; int bestRefresh = -1; for (int i = 0; i < list.Length; i++) { #if UNITY_2022_2_OR_NEWER int rr = Mathf.RoundToInt((float)list[i].refreshRateRatio.value); #else int rr = list[i].refreshRate; #endif bool prefer = rr == currentRefresh; if (prefer) { bestIndex = i; bestRefresh = rr; break; } if (rr > bestRefresh) { bestIndex = i; bestRefresh = rr; } } var chosen = list[bestIndex]; return new ResolutionOption { width = chosen.width, height = chosen.height, refreshRate = Mathf.Max(0, bestRefresh) }; }) .OrderBy(o => o.width) .ThenBy(o => o.height) .ToArray(); resolutionOptions = grouped; int currentIndex = 0; for (int i = 0; i < resolutionOptions.Length; i++) { var opt = resolutionOptions[i]; string label = opt.refreshRate > 0 ? $"{opt.width}x{opt.height} @ {opt.refreshRate}Hz" : $"{opt.width}x{opt.height}"; resolutionDropdown.CreateNewItem(label, false); if (opt.width == currentWidth && opt.height == currentHeight) { currentIndex = i; } } resolutionDropdown.selectedItemIndex = Mathf.Clamp(currentIndex, 0, Mathf.Max(0, resolutionDropdown.items.Count - 1)); resolutionDropdown.Initialize(); } else if (resolutionDropdown != null) { EnsureDropdownInitialized(resolutionDropdown); } if (displayModeDropdown != null && (rebuildDisplayModeItems || displayModeDropdown.items == null || displayModeDropdown.items.Count == 0)) { displayModeDropdown.items.Clear(); displayModeDropdown.CreateNewItem("Fullscreen", false); displayModeDropdown.CreateNewItem("Borderless", false); displayModeDropdown.CreateNewItem("Windowed", false); displayModeDropdown.Initialize(); } else if (displayModeDropdown != null) { EnsureDropdownInitialized(displayModeDropdown); } if (initQualityDropdown && qualityDropdown != null && (rebuildQualityItems || qualityDropdown.items == null || qualityDropdown.items.Count == 0)) { qualityDropdown.items.Clear(); foreach (string q in QualitySettings.names) { qualityDropdown.CreateNewItem(q, false); } qualityDropdown.Initialize(); } else if (qualityDropdown != null) { EnsureDropdownInitialized(qualityDropdown); } if (vSyncDropdown != null && (rebuildVSyncItems || vSyncDropdown.items == null || vSyncDropdown.items.Count == 0)) { vSyncDropdown.items.Clear(); vSyncDropdown.CreateNewItem("Off", false); vSyncDropdown.CreateNewItem("On", false); vSyncDropdown.Initialize(); } else if (vSyncDropdown != null) { EnsureDropdownInitialized(vSyncDropdown); } if (msaaDropdown != null && (rebuildMsaaItems || msaaDropdown.items == null || msaaDropdown.items.Count == 0)) { msaaDropdown.items.Clear(); msaaDropdown.CreateNewItem("Off", false); msaaDropdown.CreateNewItem("2x", false); msaaDropdown.CreateNewItem("4x", false); msaaDropdown.CreateNewItem("8x", false); msaaDropdown.Initialize(); } else if (msaaDropdown != null) { EnsureDropdownInitialized(msaaDropdown); } if (textureQualityDropdown != null && (rebuildTextureQualityItems || textureQualityDropdown.items == null || textureQualityDropdown.items.Count == 0)) { textureQualityDropdown.items.Clear(); textureQualityDropdown.CreateNewItem("Full", false); textureQualityDropdown.CreateNewItem("Half", false); textureQualityDropdown.CreateNewItem("Quarter", false); textureQualityDropdown.CreateNewItem("Eighth", false); textureQualityDropdown.Initialize(); } else if (textureQualityDropdown != null) { EnsureDropdownInitialized(textureQualityDropdown); } if (llmProviderDropdown != null && (rebuildLlmProviderItems || llmProviderDropdown.items == null || llmProviderDropdown.items.Count == 0)) { llmProviderDropdown.items.Clear(); llmProviderDropdown.CreateNewItem("DeepSeek", false); llmProviderDropdown.CreateNewItem("Doubao", false); llmProviderDropdown.Initialize(); } else if (llmProviderDropdown != null) { EnsureDropdownInitialized(llmProviderDropdown); } } private void EnsureDropdownInitialized(Dropdown dropdown) { if (dropdown.items == null || dropdown.items.Count == 0) { return; } for (int i = 0; i < dropdown.items.Count; i++) { if (dropdown.items[i].itemButton == null) { dropdown.Initialize(); break; } } } private void ApplyRanges() { if (targetFpsSlider != null && targetFpsSlider.mainSlider != null) { targetFpsSlider.mainSlider.minValue = targetFpsRange.x; targetFpsSlider.mainSlider.maxValue = targetFpsRange.y; targetFpsSlider.useRoundValue = true; } if (renderScaleSlider != null && renderScaleSlider.mainSlider != null) { renderScaleSlider.mainSlider.minValue = RenderScalePercentMin; renderScaleSlider.mainSlider.maxValue = RenderScalePercentMax; renderScaleSlider.usePercent = true; renderScaleSlider.useRoundValue = true; } if (shadowDistanceSlider != null && shadowDistanceSlider.mainSlider != null) { shadowDistanceSlider.mainSlider.minValue = shadowDistanceRange.x; shadowDistanceSlider.mainSlider.maxValue = shadowDistanceRange.y; shadowDistanceSlider.useRoundValue = false; } if (masterVolumeSlider != null && masterVolumeSlider.mainSlider != null) { masterVolumeSlider.mainSlider.minValue = 0f; masterVolumeSlider.mainSlider.maxValue = 1f; masterVolumeSlider.usePercent = true; } if (musicVolumeSlider != null && musicVolumeSlider.mainSlider != null) { musicVolumeSlider.mainSlider.minValue = 0f; musicVolumeSlider.mainSlider.maxValue = 1f; musicVolumeSlider.usePercent = true; } if (sfxVolumeSlider != null && sfxVolumeSlider.mainSlider != null) { sfxVolumeSlider.mainSlider.minValue = 0f; sfxVolumeSlider.mainSlider.maxValue = 1f; sfxVolumeSlider.usePercent = true; } if (mouseSensitivitySlider != null && mouseSensitivitySlider.mainSlider != null) { mouseSensitivitySlider.mainSlider.minValue = sensitivityRange.x; mouseSensitivitySlider.mainSlider.maxValue = sensitivityRange.y; mouseSensitivitySlider.useRoundValue = false; } } private void PopulateUI(SettingsData data) { if (data == null) { return; } isPopulating = true; if (resolutionDropdown != null) { int idx = -1; if (data.graphics.resolutionWidth > 0 && data.graphics.resolutionHeight > 0 && resolutionOptions.Length > 0) { int best = -1; int bestRefreshDiff = int.MaxValue; for (int i = 0; i < resolutionOptions.Length; i++) { var opt = resolutionOptions[i]; if (opt.width != data.graphics.resolutionWidth || opt.height != data.graphics.resolutionHeight) { continue; } if (data.graphics.resolutionRefreshRate > 0 && opt.refreshRate > 0) { int diff = Mathf.Abs(opt.refreshRate - data.graphics.resolutionRefreshRate); if (diff < bestRefreshDiff) { bestRefreshDiff = diff; best = i; } } else { best = i; break; } } idx = best; } else if (data.graphics.resolutionIndex >= 0) { var resolutions = Screen.resolutions; if (data.graphics.resolutionIndex >= 0 && data.graphics.resolutionIndex < resolutions.Length && resolutionOptions.Length > 0) { var r = resolutions[data.graphics.resolutionIndex]; for (int i = 0; i < resolutionOptions.Length; i++) { if (resolutionOptions[i].width == r.width && resolutionOptions[i].height == r.height) { idx = i; break; } } } } if (idx < 0) { idx = resolutionDropdown.selectedItemIndex; } idx = Mathf.Clamp(idx, 0, Mathf.Max(0, resolutionDropdown.items.Count - 1)); EnsureDropdownInitialized(resolutionDropdown); if (resolutionDropdown.items != null && resolutionDropdown.items.Count > 0) { resolutionDropdown.SetDropdownIndex(idx); } } if (displayModeDropdown != null) { int modeIndex = FullscreenModeToIndex(data.graphics.fullscreenMode); EnsureDropdownInitialized(displayModeDropdown); if (displayModeDropdown.items != null && displayModeDropdown.items.Count > 0) { displayModeDropdown.SetDropdownIndex(modeIndex); } } if (qualityDropdown != null) { int q = data.graphics.qualityLevel; if (q < 0) { q = QualitySettings.GetQualityLevel(); } q = Mathf.Clamp(q, 0, Mathf.Max(0, qualityDropdown.items.Count - 1)); EnsureDropdownInitialized(qualityDropdown); if (qualityDropdown.items != null && qualityDropdown.items.Count > 0) { qualityDropdown.SetDropdownIndex(q); } } if (vSyncDropdown != null) { EnsureDropdownInitialized(vSyncDropdown); if (vSyncDropdown.items != null && vSyncDropdown.items.Count > 0) { vSyncDropdown.SetDropdownIndex(data.graphics.vSync ? 1 : 0); } } if (msaaDropdown != null) { EnsureDropdownInitialized(msaaDropdown); if (msaaDropdown.items != null && msaaDropdown.items.Count > 0) { msaaDropdown.SetDropdownIndex(MsaaToIndex(data.graphics.msaaSampleCount)); } } if (textureQualityDropdown != null) { int t = Mathf.Clamp(data.graphics.textureMipmapLimit, 0, 3); EnsureDropdownInitialized(textureQualityDropdown); if (textureQualityDropdown.items != null && textureQualityDropdown.items.Count > 0) { textureQualityDropdown.SetDropdownIndex(t); } } if (targetFpsSlider != null && targetFpsSlider.mainSlider != null) { float v = data.graphics.targetFps <= 0 ? 60 : data.graphics.targetFps; targetFpsSlider.mainSlider.value = v; } if (renderScaleSlider != null && renderScaleSlider.mainSlider != null) { float percent = Mathf.Clamp(data.graphics.renderScale * 100f, RenderScalePercentMin, RenderScalePercentMax); renderScaleSlider.mainSlider.value = percent; } if (shadowDistanceSlider != null && shadowDistanceSlider.mainSlider != null) { shadowDistanceSlider.mainSlider.value = Mathf.Clamp(data.graphics.shadowDistance, shadowDistanceRange.x, shadowDistanceRange.y); } if (masterVolumeSlider != null && masterVolumeSlider.mainSlider != null) { masterVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.master); } if (musicVolumeSlider != null && musicVolumeSlider.mainSlider != null) { musicVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.music); } if (sfxVolumeSlider != null && sfxVolumeSlider.mainSlider != null) { sfxVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.sfx); } if (mouseSensitivitySlider != null && mouseSensitivitySlider.mainSlider != null) { mouseSensitivitySlider.mainSlider.value = Mathf.Clamp(data.gameplay.mouseSensitivity, sensitivityRange.x, sensitivityRange.y); } if (llmProviderDropdown != null) { int p = Mathf.Clamp(data.gameplay.llmProvider, 0, 1); EnsureDropdownInitialized(llmProviderDropdown); if (llmProviderDropdown.items != null && llmProviderDropdown.items.Count > 0) { llmProviderDropdown.SetDropdownIndex(p); } } isPopulating = false; } private int FullscreenModeToIndex(int fullscreenMode) { if (fullscreenMode == (int)FullScreenMode.ExclusiveFullScreen) { return 0; } if (fullscreenMode == (int)FullScreenMode.FullScreenWindow) { return 1; } return 2; } private int IndexToFullscreenMode(int index) { if (index == 0) { return (int)FullScreenMode.ExclusiveFullScreen; } if (index == 1) { return (int)FullScreenMode.FullScreenWindow; } return (int)FullScreenMode.Windowed; } private int MsaaToIndex(int msaa) { if (msaa >= 8) { return 3; } if (msaa >= 4) { return 2; } if (msaa >= 2) { return 1; } return 0; } private int IndexToMsaa(int index) { if (index == 3) { return 8; } if (index == 2) { return 4; } if (index == 1) { return 2; } return 1; } private void MarkDirty() { if (isPopulating) { return; } isDirty = true; } private void OnResolutionChanged(int index) { if (working == null) { return; } working.graphics.resolutionIndex = -1; if (resolutionOptions != null && index >= 0 && index < resolutionOptions.Length) { var opt = resolutionOptions[index]; working.graphics.resolutionWidth = opt.width; working.graphics.resolutionHeight = opt.height; working.graphics.resolutionRefreshRate = opt.refreshRate; } MarkDirty(); } private void OnDisplayModeChanged(int index) { if (working == null) { return; } working.graphics.fullscreenMode = IndexToFullscreenMode(index); MarkDirty(); } private void OnQualityChanged(int index) { if (working == null) { return; } working.graphics.qualityLevel = index; MarkDirty(); } private void OnVSyncChanged(int index) { if (working == null) { return; } working.graphics.vSync = index == 1; MarkDirty(); } private void OnMsaaChanged(int index) { if (working == null) { return; } working.graphics.msaaSampleCount = IndexToMsaa(index); MarkDirty(); } private void OnLLMProviderChanged(int index) { if (working == null) { return; } working.gameplay.llmProvider = Mathf.Clamp(index, 0, 1); MarkDirty(); } private void OnTextureQualityChanged(int index) { if (working == null) { return; } working.graphics.textureMipmapLimit = Mathf.Clamp(index, 0, 3); MarkDirty(); } private void OnTargetFpsChanged(float value) { if (working == null) { return; } int fps = Mathf.RoundToInt(value); working.graphics.targetFps = fps; MarkDirty(); } private void OnRenderScalePercentChanged(float value) { if (working == null) { return; } working.graphics.renderScale = Mathf.Clamp(value, RenderScalePercentMin, RenderScalePercentMax) / 100f; MarkDirty(); } private void OnShadowDistanceChanged(float value) { if (working == null) { return; } working.graphics.shadowDistance = value; MarkDirty(); } private void OnMasterChanged(float value) { if (working == null) { return; } working.audio.master = value; MarkDirty(); } private void OnMusicChanged(float value) { if (working == null) { return; } working.audio.music = value; MarkDirty(); } private void OnSfxChanged(float value) { if (working == null) { return; } working.audio.sfx = value; MarkDirty(); } private void OnSensitivityChanged(float value) { if (working == null) { return; } working.gameplay.mouseSensitivity = value; if (debugLogs) Debug.Log($"[SettingsController] Sensitivity changed -> {value}"); MarkDirty(); } private SettingsData Clone(SettingsData data) { if (data == null) { return SettingsData.Default(); } string json = JsonUtility.ToJson(data); SettingsData copy = JsonUtility.FromJson(json); return copy ?? SettingsData.Default(); } } }