mirror of
http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite.git
synced 2025-11-20 06:47:14 +08:00
Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com> Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/60
369 lines
17 KiB
C#
369 lines
17 KiB
C#
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<ConfigManager> 和 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
|
||
}
|
||
}
|
||
} |