Files
Gen_Hack-and-Slash-Roguelite/Client/Assets/Scripts/Configs/ConfigManager.cs

369 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Managers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using Utils;
namespace Configs
{
/// <summary>
/// 配置管理器,采用单例模式,负责读取、修改、存储应用程序配置。
/// 继承自 Utils.Singleton&lt;ConfigManager&gt; 和 ILaunchManager。
/// </summary>
public class ConfigManager : Singleton<ConfigManager>, ILaunchManager
{
/// <summary>
/// 配置文件名
/// </summary>
private const string CONFIG_FILE_NAME = "app_config.json";
/// <summary>
/// 完整的配置文件路径,文件位于程序的可执行文件同级目录。
/// 在编辑器中:通常是 Assets 文件夹的父目录。
/// 在构建后:与可执行文件(例如 .exe在同一目录下。
/// </summary>
private readonly string _configFilePath;
/// <summary>
/// 存储程序级的默认配置数据,这些值是硬编码在程序中的。
/// </summary>
private readonly Dictionary<string, JToken> _programmaticDefaults;
/// <summary>
/// 存储配置数据的字典。使用 JToken 允许存储任意 JSON 类型(基本类型、对象、数组)。
/// 这些是用户实际加载和修改的配置数据。
/// </summary>
private Dictionary<string, JToken> _configData;
/// <summary>
/// 标记配置数据是否被修改,需要保存。
/// </summary>
private bool _isDirty;
/// <summary>
/// 私有构造函数,由 Singleton 基类调用。
/// </summary>
public ConfigManager()
{
var appDirectory = Path.GetDirectoryName(Application.dataPath);
if (string.IsNullOrEmpty(appDirectory))
// Fallback for editor if GetDirectoryName returns null for some reason
appDirectory = Application.dataPath;
_configFilePath = Path.Combine(appDirectory, CONFIG_FILE_NAME);
_configData = new Dictionary<string, JToken>();
_programmaticDefaults = new Dictionary<string, JToken>(); // 初始化默认配置字典
_isDirty = false; // 初始状态为未修改
InitializeProgrammaticDefaults(); // 调用新方法来填充默认配置
Debug.Log($"ConfigManager 初始化。配置文件路径: {_configFilePath}");
}
public bool Completed { get; set; }
public string StepDescription => "载入配置文件";
/// <summary>
/// 初始化配置管理器,加载配置文件。
/// 如果配置文件不存在,将创建一个默认配置。
/// </summary>
public Task Init()
{
LoadConfig();
Completed = true;
return Task.CompletedTask;
}
/// <summary>
/// 只是用于热重载,这里不适用重新加载
/// </summary>
public void Clear()
{
}
/// <summary>
/// 初始化程序内置的默认配置项。
/// </summary>
private void InitializeProgrammaticDefaults()
{
// 使用 JToken.FromObject 直接填充 _programmaticDefaults
_programmaticDefaults["playerAffiliation"] = JToken.FromObject("player");
_programmaticDefaults["OutsideDimension"] = JToken.FromObject("DefaultOutsideDimension");
_programmaticDefaults["InsideDimension"] = JToken.FromObject("DefaultInsideDimension");
_programmaticDefaults["BaseDimension"] = JToken.FromObject("DefaultBaseDimension");
_programmaticDefaults["CoinItem"] = JToken.FromObject("Coin");
_programmaticDefaults["configVersion"] = JToken.FromObject(0.2);
_programmaticDefaults["OpenShopCost"] = JToken.FromObject(5);
_programmaticDefaults["ShoppingCost"] = JToken.FromObject(100);
_programmaticDefaults["NanorobotsAffiliation"]=JToken.FromObject("nanorobot");
// 可以在此处添加更多默认配置项
}
/// <summary>
/// 从配置文件加载配置数据到内存。
/// 如果文件不存在或无效,则创建默认配置并保存。
/// 如果文件存在且有效,则在加载后合并程序内置的默认配置。
/// </summary>
private void LoadConfig()
{
var needsToInitializeDefaults = false;
string jsonContent = null;
if (File.Exists(_configFilePath))
{
try
{
jsonContent = File.ReadAllText(_configFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
Debug.LogWarning($"ConfigManager: 配置文件 '{_configFilePath}' 为空或仅包含空白字符。正在用程序默认配置初始化。");
needsToInitializeDefaults = true;
}
else
{
_configData = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(jsonContent);
if (_configData == null) // 如果 JSON 是 "null" 或其他无效结构
{
Debug.LogWarning(
$"ConfigManager: 配置文件 '{_configFilePath}' 包含无效结构或反序列化结果为 null。正在用程序默认配置初始化。");
needsToInitializeDefaults = true;
}
}
}
catch (JsonException ex)
{
Debug.LogError($"ConfigManager: 反序列化配置文件 '{_configFilePath}' 失败。正在用程序默认配置初始化。错误: {ex.Message}");
needsToInitializeDefaults = true;
}
catch (IOException ex)
{
Debug.LogError($"ConfigManager: 读取配置文件 '{_configFilePath}' 失败。正在用程序默认配置初始化。错误: {ex.Message}");
needsToInitializeDefaults = true;
}
}
else
{
Debug.Log($"ConfigManager: 配置文件 '{_configFilePath}' 未找到。正在用程序默认配置初始化。");
needsToInitializeDefaults = true;
}
if (needsToInitializeDefaults)
{
// 如果需要从头创建配置,则将程序默认配置复制到 _configData
_configData.Clear(); // 清空,确保是全新的
foreach (var pair in
_programmaticDefaults)
_configData[pair.Key] = pair.Value.DeepClone(); // 使用 DeepClone 确保 JToken 实例是独立的
_isDirty = true; // 既然是新建的,肯定需要保存
Debug.Log("ConfigManager: 已从程序默认配置初始化配置数据。");
SaveConfig(); // 立刻保存到文件
}
else
{
// 如果配置文件成功加载,则合并程序默认配置中缺失的项
MergeProgrammaticDefaultsIntoConfigData();
}
}
/// <summary>
/// 合并程序内置的默认配置到当前内存中的配置数据中。
/// 如果有新的默认配置项被添加_isDirty 将被设置为 true并会调用 SaveConfig()。
/// </summary>
private void MergeProgrammaticDefaultsIntoConfigData()
{
var defaultsAdded = false;
foreach (var pair in _programmaticDefaults)
// 只添加不存在的键,已存在的键保持不变(用户配置优先)
if (!_configData.ContainsKey(pair.Key))
{
_configData[pair.Key] = pair.Value.DeepClone(); // 确保 JToken 实例是独立的
defaultsAdded = true;
Debug.Log($"ConfigManager: 添加新的默认配置项 '{pair.Key}'。");
}
if (defaultsAdded)
{
_isDirty = true; // 至少有一个默认值被添加,需要保存
Debug.Log("ConfigManager: 发现并添加了新的默认配置项,正在保存。");
SaveConfig(); // 保存合并后的配置
}
}
/// <summary>
/// 将内存中的配置数据保存到配置文件。
/// 只有当配置数据被修改时_isDirty 为 true才会执行实际的写入操作。
/// </summary>
public void SaveConfig()
{
if (!_isDirty)
// Debug.Log($"ConfigManager: 配置数据未修改,无需保存。"); // 可以根据需要开启
return;
try
{
var json = JsonConvert.SerializeObject(_configData, Formatting.Indented);
File.WriteAllText(_configFilePath, json);
_isDirty = false; // 成功保存后,重置脏标记
Debug.Log($"ConfigManager: 配置已保存到 '{_configFilePath}'。");
}
catch (JsonException ex)
{
Debug.LogError($"ConfigManager: 序列化配置数据失败。错误: {ex.Message}");
}
catch (IOException ex)
{
Debug.LogError($"ConfigManager: 写入配置文件 '{_configFilePath}' 失败。请检查权限。错误: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
Debug.LogError($"ConfigManager: 写入配置文件 '{_configFilePath}' 权限被拒绝。请以管理员身份运行或更改文件夹权限。错误: {ex.Message}");
}
}
/// <summary>
/// 获取指定键的配置值。
/// 优先从用户配置读取,如果不存在,则从程序内置默认配置中查找并添加。
/// 如果两者都不存在,则返回传入的默认值。
/// </summary>
/// <typeparam name="T">值的目标类型。</typeparam>
/// <param name="key">配置项的键。</param>
/// <param name="defaultValue">如果键在用户配置和内置默认配置中都不存在,返回的默认值。</param>
/// <returns>配置值或默认值。</returns>
public T GetValue<T>(string key, T defaultValue = default)
{
if (_configData.TryGetValue(key, out var userJToken))
// 1. 在用户配置中找到
try
{
// Debug.Log($"ConfigManager: 键 '{key}' 从用户配置中获取。"); // 避免日志过多
return userJToken.ToObject<T>();
}
catch (JsonException ex)
{
Debug.LogWarning(
$"ConfigManager: 键 '{key}' 的用户配置值 '{userJToken}' 转换为类型 '{typeof(T).Name}' 失败。" +
$"正在尝试获取程序默认值。错误: {ex.Message}");
// fall through to programmatic defaults
}
catch (FormatException ex)
{
Debug.LogWarning(
$"ConfigManager: 键 '{key}' 的用户配置值 '{userJToken}' 转换为类型 '{typeof(T).Name}' 时格式错误。" +
$"正在尝试获取程序默认值。错误: {ex.Message}");
// fall through to programmatic defaults
}
// else 走到这里,说明用户配置中没有,或者转换失败,尝试从程序默认配置中查找
if (_programmaticDefaults.TryGetValue(key, out var defaultJToken))
{
// 2. 在程序默认配置中找到
_configData[key] = defaultJToken.DeepClone(); // 将默认值添加到用户配置中,以便下次保存
_isDirty = true;
Debug.Log($"ConfigManager: 键 '{key}' 未在用户配置中找到,已从程序默认配置中获取并添加到用户配置。");
try
{
return defaultJToken.ToObject<T>();
}
catch (JsonException ex)
{
Debug.LogWarning(
$"ConfigManager: 键 '{key}' 的程序默认值 '{defaultJToken}' 转换为类型 '{typeof(T).Name}' 失败。" +
$"返回传入的默认值 '{defaultValue}'。错误: {ex.Message}");
return defaultValue;
}
catch (FormatException ex)
{
Debug.LogWarning(
$"ConfigManager: 键 '{key}' 的程序默认值 '{defaultJToken}' 转换为类型 '{typeof(T).Name}' 时格式错误。" +
$"返回传入的默认值 '{defaultValue}'。错误: {ex.Message}");
return defaultValue;
}
}
// 3. 在任何地方都未找到
Debug.LogWarning($"ConfigManager: 未在用户配置或程序默认配置中找到键 '{key}'。返回传入的默认值 '{defaultValue}'。");
return defaultValue;
}
/// <summary>
/// 设置指定键的配置值。
/// </summary>
/// <typeparam name="T">值的类型。</typeparam>
/// <param name="key">配置项的键。</param>
/// <param name="value">要设置的值。</param>
public void SetValue<T>(string key, T value)
{
if (string.IsNullOrEmpty(key))
{
Debug.LogError("ConfigManager: 键不能为空。");
return;
}
try
{
var newValue = JToken.FromObject(value);
// 只有当值实际发生变化时才标记为脏,避免不必要的脏标记和保存
if (!_configData.TryGetValue(key, out var existingValue) || !JToken.DeepEquals(existingValue, newValue))
{
_configData[key] = newValue;
_isDirty = true;
Debug.Log($"ConfigManager: 键 '{key}' 的值设置为 '{value}'。请调用 SaveConfig() 持久化更改。");
}
// Debug.Log($"ConfigManager: 键 '{key}' 的值未改变。"); // 避免日志过多
}
catch (JsonException ex)
{
Debug.LogError(
$"ConfigManager: 键 '{key}' 的值 '{value}' 转换为 JToken 失败。错误: {ex.Message}");
}
}
/// <summary>
/// 检查配置中是否存在某个键。
/// </summary>
/// <param name="key">要检查的键。</param>
/// <returns>如果存在该键,则为 true否则为 false。</returns>
public bool ContainsKey(string key)
{
// 检查用户配置和程序默认配置
return _configData.ContainsKey(key) || _programmaticDefaults.ContainsKey(key);
}
/// <summary>
/// 移除指定键的配置项。
/// </summary>
/// <param name="key">要移除的键。</param>
/// <returns>如果成功移除,则为 true否则为 false。</returns>
public bool RemoveKey(string key)
{
if (_configData.Remove(key))
{
_isDirty = true; // 配置被修改,需要保存
Debug.Log($"ConfigManager: 键 '{key}' 已移除。请调用 SaveConfig() 持久化更改。");
return true;
}
Debug.LogWarning($"ConfigManager: 尝试移除不存在的键 '{key}'。");
return false;
}
/// <summary>
/// 直接获取原始的 JToken 值,适用于处理复杂或嵌套的 JSON 结构。
/// 优先从用户配置获取,其次从程序默认配置获取。
/// </summary>
/// <param name="key">配置项的键。</param>
/// <returns>对应的 JToken如果键不存在则为 null。</returns>
public JToken GetRawJToken(string key)
{
// 优先返回用户配置,不存在则返回默认配置,这两个都没有则返回 null
if (_configData.TryGetValue(key, out var userJToken)) return userJToken;
_programmaticDefaults.TryGetValue(key, out var defaultJToken);
return defaultJToken; // 可能会返回 null
}
}
}