Files

369 lines
17 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}
}