Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлена система ввода текста через модальное окно с ячейками для аркадного автомата - Реализован TextInputModal с поддержкой кастомных алфавитов и навигации - Добавлен TextInputCell для ввода отдельных символов с циклической навигацией - Создан TextInputSettingsBinder для интеграции ввода текста с системой настроек - Реализована компактная сериализация настроек (только нужные поля в JSON) - Внедрена система баланса игрока (BalanceManager) - Баланс начисляется при оплате импульсом, списывается при начале/продолжении игры - Добавлен InactivityManager для выхода в меню при отсутствии активности (5 минут) - Улучшена система настроек - Добавлены флаги shownInSettings и exposed для управления видимостью настроек - Реализована автоматическая регистрация/отписка SettingsButtonBinder при активации/деактивации - Исправлена загрузка настроек с проверкой типов через Sanitize() - SettingsApplier упрощен - оставлена только поддержка AudioManager - Добавлен ValueSelectorRow для выбора целочисленных значений с шагом - Удалены примеры и документация из production кода (QUICK_SETUP.md, TextInputExample, AdvancedLoggingExample) - Убран весь отладочный вывод Debug.Log из системы настроек и ввода текста - Очищены неразрывные пробелы из файлов проекта - Добавлены префабы UI (InputCell)
403 lines
14 KiB
C#
403 lines
14 KiB
C#
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<Entry> entries = new();
|
||
|
||
[Serializable]
|
||
public class Entry
|
||
{
|
||
public string key;
|
||
public SettingValueCompact value; // Используем компактную версию
|
||
}
|
||
}
|
||
|
||
[DefaultExecutionOrder(-900)]
|
||
public class SettingsManager : MonoBehaviour
|
||
{
|
||
[SerializeField] private SettingsDatabase database;
|
||
|
||
public static SettingsManager Instance { get; private set; }
|
||
|
||
private readonly Dictionary<string, SettingValue> map = new();
|
||
private string path;
|
||
private Coroutine saveRoutine;
|
||
|
||
// События для совместимости со старым кодом
|
||
public event Action OnSettingsLoaded;
|
||
public event Action OnSettingsSaved;
|
||
public event Action<string, SettingValue> OnSettingChanged;
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
|
||
path = Path.Combine(Application.persistentDataPath, "settings.json");
|
||
|
||
if (!database)
|
||
{
|
||
Debug.LogError("[SettingsManager] Database is not assigned! Settings will not work.");
|
||
return;
|
||
}
|
||
|
||
LoadOrDefaults();
|
||
ApplyAll();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Инициализация из GameManager (альтернатива Awake)
|
||
/// </summary>
|
||
public void Initialize()
|
||
{
|
||
if (Instance != null && Instance != this)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
|
||
// Если database не назначена - пытаемся загрузить из Resources
|
||
if (!database)
|
||
{
|
||
database = Resources.Load<SettingsDatabase>("SettingsDatabase");
|
||
if (!database)
|
||
{
|
||
// Пытаемся найти л<><D0BB>бую SettingsDatabase в проекте
|
||
var databases = Resources.FindObjectsOfTypeAll<SettingsDatabase>();
|
||
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.");
|
||
return;
|
||
}
|
||
|
||
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;
|
||
|
||
// применяем СРАЗУ
|
||
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));
|
||
|
||
/// <summary>
|
||
/// Изменяет настройку извне (с проверкой флага exposed)
|
||
/// </summary>
|
||
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));
|
||
|
||
/// <summary>
|
||
/// Немедленное сохранение настроек
|
||
/// </summary>
|
||
public void SaveSettings()
|
||
{
|
||
SaveNow();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Перезагружает настройки из файла (только exposed настройки)
|
||
/// Полезно для hot-reload при изменении файла извне
|
||
/// </summary>
|
||
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<SettingsSaveData>(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 value = SettingValue.FromCompact(e.value);
|
||
SettingValue clean = def.Sanitize(value);
|
||
map[e.key] = clean;
|
||
SettingsApplier.Apply(def, clean);
|
||
OnSettingChanged?.Invoke(e.key, clean);
|
||
}
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сбрасывает настройки указанной группы к значениям по умолчанию
|
||
/// </summary>
|
||
/// <param name="groupName">Имя группы настроек</param>
|
||
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, 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<SettingsSaveData>(json);
|
||
if (data != null)
|
||
{
|
||
foreach (SettingsSaveData.Entry e in data.entries)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(e.key)) continue;
|
||
|
||
// Получаем определение
|
||
SettingEntry def = Define(e.key);
|
||
|
||
// Если checkExposed = true, проверяем флаг exposed
|
||
if (checkExposed)
|
||
{
|
||
if (def != null && !def.exposed)
|
||
{
|
||
Debug.LogWarning($"[SettingsManager] Skipping '{e.key}' from file - not exposed.");
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Конвертируем из компактного формата
|
||
SettingValue value = SettingValue.FromCompact(e.value);
|
||
|
||
// Применяем Sanitize если есть определение
|
||
if (def != null)
|
||
{
|
||
map[e.key] = def.Sanitize(value);
|
||
}
|
||
else
|
||
{
|
||
map[e.key] = 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.ToCompact() // Конвертация в компактный формат
|
||
});
|
||
}
|
||
|
||
try
|
||
{
|
||
File.WriteAllText(path, JsonUtility.ToJson(data, true));
|
||
OnSettingsSaved?.Invoke();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[SettingsManager] Save failed: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void OnApplicationQuit() => SaveNow();
|
||
}
|
||
} |