using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; namespace RR.Common.Settings { [Serializable] public class SettingsSaveData { public int version = 1; public List entries = new(); [Serializable] public class Entry { public string key; public SettingValue value; } } [DefaultExecutionOrder(-900)] public class SettingsManager : MonoBehaviour { [SerializeField] private SettingsDatabase database; public static SettingsManager Instance { get; private set; } private readonly Dictionary map = new(); private string path; private Coroutine saveRoutine; // События для совместимости со старым кодом public event Action OnSettingsLoaded; public event Action OnSettingsSaved; public event Action OnSettingChanged; private void Awake() { Debug.Log($"[SettingsManager] Awake called. Instance = {(Instance != null ? Instance.name : "null")}"); if (Instance) { Debug.Log("[SettingsManager] Instance already exists, destroying this one."); Destroy(gameObject); return; } Instance = this; Debug.Log($"[SettingsManager] Instance set to {name}"); DontDestroyOnLoad(gameObject); path = Path.Combine(Application.persistentDataPath, "settings.json"); if (!database) { Debug.LogError("[SettingsManager] Database is not assigned! Settings will not work."); } else { Debug.Log($"[SettingsManager] Initialized with database containing {database.entries.Count} entries."); } LoadOrDefaults(); ApplyAll(); } /// /// Инициализация из GameManager (альтернатива Awake) /// public void Initialize() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // Если database не назначена - пытаемся загрузить из Resources if (!database) { database = Resources.Load("SettingsDatabase"); if (!database) { // Пытаемся найти л��бую SettingsDatabase в проекте var databases = Resources.FindObjectsOfTypeAll(); if (databases.Length > 0) { database = databases[0]; } } } if (!database) { Debug.LogError("[SettingsManager] Database is not assigned and not found in Resources! Settings will not work."); } else { Debug.Log($"[SettingsManager] Initialized with database containing {database.entries.Count} entries."); } path = Path.Combine(Application.persistentDataPath, "settings.json"); LoadOrDefaults(); ApplyAll(); } public SettingEntry Define(string key) { if (!database) { Debug.LogWarning($"[SettingsManager] Database is null, cannot define key '{key}'"); return null; } return database.Find(key); } public SettingValue Get(string key) { if (map.TryGetValue(key, out SettingValue v)) return v; SettingEntry def = Define(key); return def?.DefaultValue ?? default; } // Удобные типизированные геттеры public bool GetBool(string key) => Get(key).b; public int GetInt(string key) => Get(key).i; public float GetFloat(string key) => Get(key).f; public string GetString(string key) => Get(key).s; public void Set(string key, SettingValue value) { // Проверяем, что объект не уничтожен if (this == null) return; SettingEntry def = Define(key); if (def == null) { Debug.LogWarning($"[SettingsManager] No definition found for key '{key}'. Check that SettingsDatabase has this entry."); return; } SettingValue clean = def.Sanitize(value); map[key] = clean; Debug.Log($"[SettingsManager] Set '{key}' = {value.type}: b={value.b}, i={value.i}, f={value.f}, s={value.s}"); // применяем СРАЗУ SettingsApplier.Apply(def, clean); // оповещаем подписчиков OnSettingChanged?.Invoke(key, clean); // сохраняем с небольшой задержкой (удобно для слайдеров) ScheduleSave(0.25f); } // Удобные типизированные сеттеры public void SetBool(string key, bool value) => Set(key, SettingValue.Bool(value)); public void SetInt(string key, int value) => Set(key, SettingValue.Int(value)); public void SetFloat(string key, float value) => Set(key, SettingValue.Float(value)); public void SetString(string key, string value) => Set(key, SettingValue.String(value)); /// /// Изменяет настройку извне (с проверкой флага exposed) /// public bool SetExternal(string key, SettingValue value) { if (this == null) return false; SettingEntry def = Define(key); if (def == null) { Debug.LogWarning($"[SettingsManager] No definition found for key '{key}'."); return false; } if (!def.exposed) { Debug.LogWarning($"[SettingsManager] Setting '{key}' is not exposed for external changes. Set 'exposed' flag to true in SettingsDatabase."); return false; } Set(key, value); return true; } // Удобные типизированные сеттеры для внешних изменений public bool SetExternalBool(string key, bool value) => SetExternal(key, SettingValue.Bool(value)); public bool SetExternalInt(string key, int value) => SetExternal(key, SettingValue.Int(value)); public bool SetExternalFloat(string key, float value) => SetExternal(key, SettingValue.Float(value)); public bool SetExternalString(string key, string value) => SetExternal(key, SettingValue.String(value)); /// /// Немедленное сохранение настроек /// public void SaveSettings() { SaveNow(); } /// /// Перезагружает настройки из файла (только exposed настройки) /// Полезно для hot-reload при изменении файла извне /// public void ReloadFromFile() { if (!File.Exists(path)) { Debug.LogWarning($"[SettingsManager] Settings file not found: {path}"); return; } try { string json = File.ReadAllText(path); SettingsSaveData data = JsonUtility.FromJson(json); if (data != null) { foreach (SettingsSaveData.Entry e in data.entries) { if (string.IsNullOrWhiteSpace(e.key)) continue; SettingEntry def = Define(e.key); if (def == null) { Debug.LogWarning($"[SettingsManager] Unknown setting key '{e.key}' in file."); continue; } // Проверяем флаг exposed if (!def.exposed) { Debug.LogWarning($"[SettingsManager] Skipping '{e.key}' - not exposed for external changes."); continue; } // Применяем настройку SettingValue clean = def.Sanitize(e.value); map[e.key] = clean; SettingsApplier.Apply(def, clean); OnSettingChanged?.Invoke(e.key, clean); } Debug.Log($"[SettingsManager] Reloaded settings from {path}"); } } catch (Exception ex) { Debug.LogError($"[SettingsManager] Failed to reload settings: {ex.Message}"); } } public void ApplyAll() { if (!database) return; foreach (SettingEntry def in database.entries) { if (def == null || string.IsNullOrWhiteSpace(def.key)) continue; SettingsApplier.Apply(def, Get(def.key)); } } public void ResetAllToDefaults() { if (!database) return; foreach (SettingEntry def in database.entries) { if (def == null || string.IsNullOrWhiteSpace(def.key)) continue; map[def.key] = def.DefaultValue; SettingsApplier.Apply(def, def.DefaultValue); } ScheduleSave(0.05f); } /// /// Сбрасывает настройки указанной группы к значениям по умолчанию /// /// Имя группы настроек public void ResetGroupToDefaults(string groupName) { if (!database || string.IsNullOrWhiteSpace(groupName)) return; foreach (SettingEntry def in database.entries) { if (def == null || string.IsNullOrWhiteSpace(def.key)) continue; if (!string.Equals(def.group, groupName, System.StringComparison.OrdinalIgnoreCase)) continue; map[def.key] = def.DefaultValue; SettingsApplier.Apply(def, def.DefaultValue); } ScheduleSave(0.05f); } private void LoadOrDefaults() { LoadOrDefaults(checkExposed: false); } private void LoadOrDefaults(bool checkExposed) { map.Clear(); if (File.Exists(path)) { try { string json = File.ReadAllText(path); SettingsSaveData data = JsonUtility.FromJson(json); if (data != null) { foreach (SettingsSaveData.Entry e in data.entries) { if (string.IsNullOrWhiteSpace(e.key)) continue; // Если checkExposed = true, проверяем флаг exposed if (checkExposed) { SettingEntry def = Define(e.key); if (def != null && !def.exposed) { Debug.LogWarning($"[SettingsManager] Skipping '{e.key}' from file - not exposed."); continue; } } map[e.key] = e.value; } } } catch { // если файл битый — просто упадём на дефолты ниже } } if (!database) return; foreach (SettingEntry def in database.entries) { if (def == null || string.IsNullOrWhiteSpace(def.key)) continue; if (!map.ContainsKey(def.key)) map[def.key] = def.DefaultValue; else map[def.key] = def.Sanitize(map[def.key]); } OnSettingsLoaded?.Invoke(); } private void ScheduleSave(float delay) { // Проверяем, что объект не уничтожен if (this == null || !gameObject) return; if (saveRoutine != null) StopCoroutine(saveRoutine); saveRoutine = StartCoroutine(SaveDelayed(delay)); } private IEnumerator SaveDelayed(float delay) { yield return new WaitForSecondsRealtime(delay); SaveNow(); saveRoutine = null; } private void SaveNow() { SettingsSaveData data = new(); foreach (var kv in map) data.entries.Add(new SettingsSaveData.Entry { key = kv.Key, value = kv.Value }); try { File.WriteAllText(path, JsonUtility.ToJson(data, true)); OnSettingsSaved?.Invoke(); } catch (Exception e) { Debug.LogWarning($"[SettingsManager] Save failed: {e.Message}"); } } private void OnApplicationQuit() => SaveNow(); } }