提交模块化内容
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# ⚙️ ConfigLoader 配置加载模块
|
||||
|
||||
此模块负责配置文件的加载与检测,依赖于 `Common` 通用模块,请确保已安装该模块。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 📂 配置文件加载
|
||||
- 🔍 配置字段缺失检测
|
||||
- ❌ 配置字段冗余检测
|
||||
- ⚙️ 自动化插件安装
|
||||
- 📝 日志标准化输出
|
||||
|
||||
------
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
导入项目后,会弹出插件安装提示框。点击“确定”后,插件将自动安装;若点击“取消”,脚本重新编译时仍会再次弹出提示。
|
||||
|
||||
- 插件安装需要一定时间,请耐心等待。安装完成后会有弹窗提示。
|
||||
- 安装完成后,会自动生成配置文件夹 `Configs`,请将游戏配置文件复制到此文件夹中。
|
||||
- **请勿修改配置文件名**。如需替换配置文件,请先删除旧文件,确保文件夹中每个配置只有一份。
|
||||
- 游戏首次运行时,会自动将该文件夹复制到游戏的数据存储位置。
|
||||
|
||||
------
|
||||
|
||||
## 🚀 使用指南
|
||||
|
||||
在登录成功后,调用 `ConfigLoader` 的 `Init` 方法传入所需参数,具体参数请查看方法注释。
|
||||
|
||||
> 注意:
|
||||
>
|
||||
> - **List 类型**的配置需继承 `ConfigModel<T, T0>`,不再需要之前的 `IDataList` 接口。
|
||||
> - **非 List 类型**配置继承 `ConfigModel<T>`。
|
||||
|
||||
### 初始化示例
|
||||
|
||||
```csharp
|
||||
var loginModel = NetworkKit.Instance.GetLoginModel();
|
||||
ConfigLoader.Instance.Init(
|
||||
loginModel.setting,
|
||||
loginModel.cdn_url,
|
||||
new List<ConfigModel> {
|
||||
new CommonModel("Common"),
|
||||
new SignDailyRewardModel("SignDailyReward"),
|
||||
new TurntableModel("turntable"),
|
||||
// 其它配置...
|
||||
},
|
||||
state => {
|
||||
Debug.Log($"配置加载状态: {state}");
|
||||
},
|
||||
(errorName, message) => {
|
||||
Debug.LogError($"配置解析错误: {errorName},错误信息:{message}");
|
||||
});
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## 🔍 读取配置示例
|
||||
|
||||
```csharp
|
||||
// 读取基础配置
|
||||
var model = ConfigLoader.Instance.GetConfig<CommonModel>();
|
||||
|
||||
// 解析自定义配置
|
||||
ConfigLoader.Instance.ParesPersonalizedConfig(new PrizeWheelDataModel("PrizeWheelData"), "prize");
|
||||
|
||||
// 获取自定义配置
|
||||
var personalizedConfig = ConfigLoader.Instance.GetPersonalizedConfig<PrizeWheelDataModel>("prize");
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b25e03cedceda0f4db9aebc261a5bf0a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c34977ebbc9d4440826289429a2adc3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c03491d9ce1584a408c18056b93f91d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,188 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
#if USE_ADDRESSABLES
|
||||
using UnityEditor.AddressableAssets;
|
||||
#endif
|
||||
|
||||
namespace SGModule.Editor {
|
||||
[InitializeOnLoad]
|
||||
public static class AddressablesManager {
|
||||
private const string AddressablesPackageName = "com.unity.addressables";
|
||||
private const string AddressablesSymbol = "USE_ADDRESSABLES";
|
||||
private static AddRequest _addRequest;
|
||||
|
||||
// 安装标记,避免重复弹窗
|
||||
private static bool _isInstallingAddressables;
|
||||
|
||||
static AddressablesManager() {
|
||||
EditorApplication.projectChanged += OnProjectChanged; // 监听项目变更
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
|
||||
private static string DefaultConfigsPath => "Assets/Configs"; // 默认的 Addressables 配置路径
|
||||
|
||||
private static void CheckAndSetupAddressables() {
|
||||
if (IsAddressablesInstalled()) {
|
||||
Log.Info("ConfigLoader", "Addressables 已安装,正在初始化...");
|
||||
|
||||
AddScriptingDefineSymbol(AddressablesSymbol);
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
EnsureAddressablesConfigured();
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
RemoveScriptingDefineSymbol(AddressablesSymbol);
|
||||
if (!_isInstallingAddressables) {
|
||||
PromptToInstallAddressables();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAddressablesInstalled() {
|
||||
return PackageInfo.GetAllRegisteredPackages().Any(package => package.name == AddressablesPackageName);
|
||||
}
|
||||
|
||||
private static void PromptToInstallAddressables() {
|
||||
var install = EditorUtility.DisplayDialog(
|
||||
"Addressables 未安装",
|
||||
"当前插件依赖 Addressables 功能,请安装以确保正常使用。\n\n是否立即安装 Addressables?",
|
||||
"安装",
|
||||
"取消");
|
||||
|
||||
if (install) {
|
||||
InstallAddressables();
|
||||
}
|
||||
else {
|
||||
Log.Warning("ConfigLoader", "用户取消安装 Addressables,部分功能可能无法正常使用!");
|
||||
}
|
||||
}
|
||||
|
||||
private static void InstallAddressables() {
|
||||
if (_isInstallingAddressables) {
|
||||
return; // 防止重复安装
|
||||
}
|
||||
|
||||
Log.Info("ConfigLoader", "开始安装 Addressables...");
|
||||
|
||||
_isInstallingAddressables = true; // 标记正在安装,避免重复弹窗
|
||||
_addRequest = Client.Add(AddressablesPackageName);
|
||||
|
||||
EditorApplication.update += MonitorAddRequest;
|
||||
}
|
||||
|
||||
private static void MonitorAddRequest() {
|
||||
if (_addRequest == null || !_addRequest.IsCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorApplication.update -= MonitorAddRequest;
|
||||
|
||||
if (_addRequest.Status == StatusCode.Success) {
|
||||
Log.Info("ConfigLoader", "Addressables 安装成功!");
|
||||
|
||||
EditorUtility.DisplayDialog("安装完成", "Addressables 安装成功,请稍候等待 Unity 刷新。", "确定");
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
else if (_addRequest.Status >= StatusCode.Failure) {
|
||||
Log.Error("ConfigLoader", $"安装 Addressables 失败:{_addRequest.Error.message}");
|
||||
|
||||
EditorUtility.DisplayDialog("安装失败", $"安装 Addressables 失败:{_addRequest.Error.message}", "确定");
|
||||
}
|
||||
|
||||
_isInstallingAddressables = false; // 重置标记
|
||||
}
|
||||
|
||||
private static void AddScriptingDefineSymbol(string symbol) {
|
||||
var symbols = new List<string>(PlayerSettings
|
||||
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
|
||||
if (symbols.Contains(symbol)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
symbols.Add(symbol);
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
|
||||
string.Join(";", symbols));
|
||||
Log.Info("ConfigLoader", $"已添加脚本定义符号: {symbol}");
|
||||
}
|
||||
|
||||
private static void RemoveScriptingDefineSymbol(string symbol) {
|
||||
var symbols = new List<string>(PlayerSettings
|
||||
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
|
||||
if (symbols.Contains(symbol)) {
|
||||
symbols.Remove(symbol);
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
|
||||
string.Join(";", symbols));
|
||||
|
||||
Log.Info("ConfigLoader", $"已移除脚本定义符号: {symbol}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnProjectChanged() {
|
||||
Log.Info("ConfigLoader", "检测到项目变更,重新检查 Addressables 状态...");
|
||||
|
||||
CheckAndSetupAddressables();
|
||||
}
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
private static void EnsureAddressablesConfigured() {
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
|
||||
if (settings == null) {
|
||||
Log.Info("ConfigLoader", "正在初始化 Addressables 设置...");
|
||||
|
||||
settings = AddressableAssetSettingsDefaultObject.GetSettings(true); // 自动创建配置
|
||||
}
|
||||
|
||||
// 确保默认的 Configs 文件夹存在并被添加到 Addressables
|
||||
var needRefresh = false;
|
||||
if (!Directory.Exists(DefaultConfigsPath)) {
|
||||
Directory.CreateDirectory(DefaultConfigsPath);
|
||||
Log.Info("ConfigLoader", $"已创建文件夹: {DefaultConfigsPath}");
|
||||
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
if (!IsFolderInAddressables(DefaultConfigsPath)) {
|
||||
var guid = AssetDatabase.AssetPathToGUID(DefaultConfigsPath);
|
||||
var group = settings.DefaultGroup;
|
||||
settings.CreateOrMoveEntry(guid, group)?.SetLabel("config", true, true);
|
||||
needRefresh = true;
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFolderInAddressables(string folderPath) {
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
foreach (var group in settings.groups)
|
||||
{
|
||||
if (group)
|
||||
{
|
||||
foreach (var entry in group.entries)
|
||||
{
|
||||
if (AssetDatabase.GUIDToAssetPath(entry.guid) == folderPath)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 383ac67f695b4c6e94ba988c9303cd5b
|
||||
timeCreated: 1733734050
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34c8928adac69ec44ac63b385ef330d1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53a14b320d93d7c439c59c8190a874ab
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,418 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using SGModule.Common;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Extensions;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace SGModule.ConfigLoader
|
||||
{
|
||||
public enum ConfigLoaderState
|
||||
{
|
||||
None,
|
||||
Failed,
|
||||
JsonEmptyError,
|
||||
Successful
|
||||
}
|
||||
|
||||
public class ConfigLoader : SingletonMonoBehaviour<ConfigLoader>
|
||||
{
|
||||
private const int MaxErrorCount = 6;
|
||||
private const string ConfigFileNameKey = "ConfigFileName";
|
||||
private readonly Dictionary<Type, object> _configData = new();
|
||||
private readonly Dictionary<string, ConfigModel> _userDefinedConfig = new();
|
||||
|
||||
private string _cdnConfigFileName;
|
||||
private string _cdnUrl;
|
||||
|
||||
private string _configJson;
|
||||
|
||||
private int _initConfigErrorCount;
|
||||
|
||||
private bool _isConfigLoaded;
|
||||
private Dictionary<string, object> _jsonDictionary = new();
|
||||
|
||||
// 暴露只读接口
|
||||
public IReadOnlyDictionary<string, object> JsonDictionary => _jsonDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化配置模块方法
|
||||
/// </summary>
|
||||
/// <param name="cdnConfigFileName">登录获得的配置文件名</param>
|
||||
/// <param name="cdnUrl">下载配置的地址</param>
|
||||
/// <param name="configs">需要初始化解析的配置</param>
|
||||
/// <param name="configLoadCallback">配置加载回调</param>
|
||||
/// <param name="parseErrorHandler">配置解析错误回调</param>
|
||||
public void Init(string cdnConfigFileName, string cdnUrl, List<ConfigModel> configs,
|
||||
UnityAction<ConfigLoaderState> configLoadCallback = null,
|
||||
UnityAction<string, string> parseErrorHandler = null)
|
||||
{
|
||||
_cdnConfigFileName = cdnConfigFileName;
|
||||
_cdnUrl = cdnUrl;
|
||||
|
||||
IsInitComplete = true;
|
||||
|
||||
CheckConfigFile(state =>
|
||||
{
|
||||
if (state == ConfigLoaderState.Successful)
|
||||
{
|
||||
InitConfigList(configs, parseErrorHandler);
|
||||
}
|
||||
|
||||
configLoadCallback?.Invoke(state);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="configs"></param>
|
||||
/// <param name="errorCallback"></param>
|
||||
private void InitConfigList(List<ConfigModel> configs, UnityAction<string, string> errorCallback = null)
|
||||
{
|
||||
foreach (var config in configs)
|
||||
{
|
||||
Pares(config, errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并加载配置文件
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
private void CheckConfigFile(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
if (!IsInitComplete)
|
||||
{
|
||||
Log.ConfigLoader.Warning("配置加载模块模块未初始化");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_initConfigErrorCount > MaxErrorCount)
|
||||
{
|
||||
callback?.Invoke(ConfigLoaderState.Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFirstLaunch())
|
||||
{
|
||||
FirstLaunchCopyConfig(callback); // 进行首次启动的操作
|
||||
return;
|
||||
}
|
||||
|
||||
bool needDownloadConfigFile = false;
|
||||
if ((!string.IsNullOrEmpty(_cdnConfigFileName)) && PlayerPrefs.GetString(ConfigFileNameKey, "") != _cdnConfigFileName) needDownloadConfigFile = true;
|
||||
//需要下载配置的话等待下载更新
|
||||
if (needDownloadConfigFile)
|
||||
{
|
||||
DownloadConfig(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadLocalConfig(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是首次打开游戏
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsFirstLaunch()
|
||||
{
|
||||
return !PlayerPrefs.HasKey("FirstLaunch");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置首次启动游戏
|
||||
/// </summary>
|
||||
private void SetFirstLaunch()
|
||||
{
|
||||
PlayerPrefs.SetInt("FirstLaunch", 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 首次启动游戏拷贝配置文件到PersistentData目录下
|
||||
/// </summary>
|
||||
private void FirstLaunchCopyConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
SetFirstLaunch();
|
||||
FileNetworkManager.Instance.CopyStreamingAssetsToPersistentDataPath(result =>
|
||||
{
|
||||
var isSuccess = false;
|
||||
if (result)
|
||||
{
|
||||
var names =
|
||||
FileNetworkManager.Instance.GetFileNamesFromPersistentDataPath(FileNetworkManager.FolderName);
|
||||
if (names.Length > 0)
|
||||
{
|
||||
PlayerPrefs.SetString(ConfigFileNameKey, names[0]); //项目当前文件中的配置版本
|
||||
isSuccess = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ConfigLoader.Error("拷贝文件失败 请检查Configs文件夹是否有配置文件");
|
||||
}
|
||||
|
||||
if (!isSuccess)
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
}
|
||||
|
||||
CheckConfigFile(callback);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载配置文件
|
||||
/// </summary>
|
||||
private void DownloadConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
FileNetworkManager.Instance.ReadData($"{_cdnUrl}/config/{_cdnConfigFileName}", result =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
PlayerPrefs.SetString(ConfigFileNameKey, _cdnConfigFileName); //更新当前配置文件文件名
|
||||
FileNetworkManager.Instance.WriteToPersistentData(FileNetworkManager.Instance.GetConfigFOlderPath(),
|
||||
_cdnConfigFileName, result);
|
||||
ReloadConfig(result, callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
Log.ConfigLoader.Error("下载配置文件失败");
|
||||
ReadLocalConfig(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取本地配置文件
|
||||
/// </summary>
|
||||
private void ReadLocalConfig(UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
|
||||
var path = Path.Combine(FileNetworkManager.Instance.GetConfigFOlderPath(), savedCfgName);
|
||||
FileNetworkManager.Instance.ReadData(path, result =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
ReloadConfig(result, callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
_initConfigErrorCount++;
|
||||
Log.ConfigLoader.Error("读取本地数据异常");
|
||||
|
||||
|
||||
//读取本地配置文件失败,重新从默认位置拷贝原始配置文件到配置文件夹下
|
||||
FirstLaunchCopyConfig(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有新的配置
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool HasNewConfig()
|
||||
{
|
||||
if (!PlayerPrefs.HasKey(ConfigFileNameKey))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
|
||||
var needDownloadConfigFile = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_cdnConfigFileName))
|
||||
{
|
||||
//如果本地Player Prefs里没有保存配置文件名
|
||||
if (string.IsNullOrEmpty(savedCfgName))
|
||||
{
|
||||
needDownloadConfigFile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//与CDN上的对比名称
|
||||
if (!savedCfgName.Equals(_cdnConfigFileName))
|
||||
{
|
||||
needDownloadConfigFile = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return needDownloadConfigFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新加载配置
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="callback"></param>
|
||||
private void ReloadConfig(string json, [NotNull] UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
ValidateConfigFile(json, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查json
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <param name="callback"></param>
|
||||
private void ValidateConfigFile(string json, UnityAction<ConfigLoaderState> callback)
|
||||
{
|
||||
if (json.IsNullOrWhiteSpace())
|
||||
{
|
||||
callback?.Invoke(ConfigLoaderState.JsonEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.StartsWith("{"))
|
||||
{
|
||||
var gameConfig = ConfigManager.GameConfig;
|
||||
json = Cryptor.Decrypt(json, gameConfig.packageName);
|
||||
}
|
||||
|
||||
_configJson = json;
|
||||
var dictionary = SerializeHelper.ToObject<Dictionary<string, object>>(_configJson);
|
||||
_jsonDictionary = dictionary;
|
||||
|
||||
_isConfigLoaded = true;
|
||||
callback?.Invoke(ConfigLoaderState.Successful);
|
||||
|
||||
|
||||
// ParseGameConfig(dictionary, callback);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T ParseNewConfig<T>(T config, UnityAction<string, string> errorCallback = null) where T : ConfigModel
|
||||
{
|
||||
if (!_isConfigLoaded)
|
||||
{
|
||||
Log.ConfigLoader.Warning("配置文件未加载完成");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (_configData.TryGetValue(config.GetType(), out var obj))
|
||||
{
|
||||
Log.ConfigLoader.Warning("当前配置已经成功解析了");
|
||||
return obj as T;
|
||||
}
|
||||
|
||||
return Pares(config, errorCallback) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析配置文件
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <param name="addToConfigData">是否保存起来</param>
|
||||
/// <returns></returns>
|
||||
private ConfigModel Pares(ConfigModel config, UnityAction<string, string> errorCallback = null,
|
||||
bool addToConfigData = true)
|
||||
{
|
||||
var configModel = config.Parse(JsonDictionary);
|
||||
if (configModel != null)
|
||||
{
|
||||
if (addToConfigData)
|
||||
{
|
||||
var type = configModel.GetType();
|
||||
// if (!_configData.ContainsKey(type))
|
||||
// {
|
||||
// _configData.Add(type, configModel);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogWarning($"请注意,重复解析配置文件{type}");
|
||||
// _configData[type] = configModel;
|
||||
// }
|
||||
|
||||
_configData[type] = configModel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCallback?.Invoke(config.GetType().Name, "解析异常");
|
||||
Log.ConfigLoader.Warning("解析异常");
|
||||
}
|
||||
|
||||
return configModel as ConfigModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析自定义名称配置文件,将配置解析为自定义名称的配置文件并保存
|
||||
/// </summary>
|
||||
/// <param name="config">配置数据结构</param>
|
||||
/// <param name="configName">数据结构类型</param>
|
||||
/// <param name="errorCallback"></param>
|
||||
/// <returns></returns>
|
||||
public bool ParesPersonalizedConfig(ConfigModel config, string configName,
|
||||
UnityAction<string, string> errorCallback = null)
|
||||
{
|
||||
if (_userDefinedConfig.ContainsKey(configName))
|
||||
{
|
||||
Log.ConfigLoader.Warning("存在相同名称的自定义配置");
|
||||
return false;
|
||||
}
|
||||
|
||||
var configModel = Pares(config, errorCallback, false);
|
||||
if (configModel != null)
|
||||
{
|
||||
_userDefinedConfig.Add(configName, configModel);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"解析配置重新异常{config} {configName}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取自定义配置数据
|
||||
/// </summary>
|
||||
/// <param name="configName">配置名称</param>
|
||||
/// <typeparam name="T">对应数据结构</typeparam>
|
||||
/// <returns></returns>
|
||||
public T GetPersonalizedConfig<T>(string configName)
|
||||
{
|
||||
if (_userDefinedConfig.TryGetValue(configName, out var value))
|
||||
{
|
||||
if (value is T configModel)
|
||||
{
|
||||
return configModel;
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"有{configName}的自定义配置,但类型不对,期望类型是{typeof(T).Name} 现有类型{value.GetType().Name} ");
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public T GetConfig<T>()
|
||||
{
|
||||
return _configData.TryGetValue(typeof(T), out var value) ? (T)value : default;
|
||||
}
|
||||
|
||||
public void AddConfig(ConfigModel configModel)
|
||||
{
|
||||
var type = configModel.GetType();
|
||||
_configData[type] = configModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cda67e479db4e8fbd10667fba04b197
|
||||
timeCreated: 1731984367
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
#if USE_ADDRESSABLES
|
||||
#endif
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class FileNetworkManager : SingletonMonoBehaviour<FileNetworkManager> {
|
||||
public const string FolderName = "Configs";
|
||||
private const string FolderLabel = "config";
|
||||
private string _configFolderPath;
|
||||
|
||||
protected override void Awake() {
|
||||
base.Awake();
|
||||
|
||||
_configFolderPath = Path.Combine(Application.persistentDataPath, FolderName);
|
||||
}
|
||||
|
||||
public string GetConfigFOlderPath() {
|
||||
return _configFolderPath;
|
||||
}
|
||||
|
||||
// 示例解析文件列表方法(你可以根据实际需求实现)
|
||||
private List<string> ParseFileList(string data) {
|
||||
// 假设 data 是文件列表的文本,解析文件名
|
||||
// 根据实际情况解析,例如通过换行符分割
|
||||
return data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
}
|
||||
|
||||
public void CopyStreamingAssetsToPersistentDataPath(Action<bool> onComplete = null) {
|
||||
// 如果目标文件夹不存在,创建它
|
||||
if (!Directory.Exists(_configFolderPath)) {
|
||||
Directory.CreateDirectory(_configFolderPath);
|
||||
}
|
||||
|
||||
StartCoroutine(CopyFile(onComplete));
|
||||
}
|
||||
|
||||
private void HandleInitializationError() {
|
||||
// 检查文件是否存在
|
||||
var path = $"{Application.dataPath}/Library/com.unity.addressables/aa/Android/settings.json";
|
||||
if (!File.Exists(path)) {
|
||||
Log.ConfigLoader.Warning(
|
||||
$"Settings file not found at: {path}. Rebuilding Addressables may be required.");
|
||||
}
|
||||
|
||||
|
||||
// 提示用户或执行其他逻辑
|
||||
// 比如:显示弹窗或退出程序
|
||||
}
|
||||
|
||||
private IEnumerator CopyFile(Action<bool> onComplete = null) {
|
||||
#if USE_ADDRESSABLES
|
||||
var handle = Addressables.LoadResourceLocationsAsync(FolderLabel);
|
||||
|
||||
yield return handle;
|
||||
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded) {
|
||||
// 查找以 ".json" 结尾的文件
|
||||
var jsonLocation = handle.Result.FirstOrDefault(loc => loc.PrimaryKey.EndsWith(".txt"));
|
||||
if (jsonLocation != null) {
|
||||
var jsonFileName = Path.GetFileName(jsonLocation.PrimaryKey);
|
||||
// 加载 JSON 文件
|
||||
var textAssetAsync = Addressables.LoadAssetAsync<TextAsset>(jsonLocation);
|
||||
|
||||
yield return textAssetAsync;
|
||||
|
||||
if (textAssetAsync.Status == AsyncOperationStatus.Succeeded) {
|
||||
var jsonFile = textAssetAsync.Result;
|
||||
Log.ConfigLoader.Info($"Loaded JSON Name:{jsonFileName} content: " + jsonFile.text);
|
||||
|
||||
|
||||
var destFilePath = Path.Combine(_configFolderPath, jsonFileName);
|
||||
File.WriteAllBytes(destFilePath, jsonFile.bytes);
|
||||
onComplete?.Invoke(true);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to load JSON file");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("No JSON file found in folder");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to load folder resources");
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
#else
|
||||
Log.Error( "没有 Addressables 插件,请检查 ");
|
||||
yield break;
|
||||
#endif
|
||||
}
|
||||
|
||||
private IEnumerator CopyFile(string sourceFile, string destFile, Action<bool> onComplete = null) {
|
||||
var filePath = "file://" + sourceFile;
|
||||
|
||||
using (var www = UnityWebRequest.Get(filePath)) {
|
||||
yield return www.SendWebRequest();
|
||||
|
||||
if (www.result == UnityWebRequest.Result.Success) {
|
||||
File.WriteAllBytes(destFile, www.downloadHandler.data);
|
||||
Log.ConfigLoader.Info($"File copied to {destFile}");
|
||||
onComplete?.Invoke(true);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("Failed to copy file: " + www.error);
|
||||
|
||||
onComplete?.Invoke(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取或下载
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="callback"></param>
|
||||
public void ReadData(string path, Action<string> callback) {
|
||||
StartCoroutine(ReadDataEnumerator(path, callback));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取或下载
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <returns></returns>
|
||||
private IEnumerator ReadDataEnumerator(string path, Action<string> callback) {
|
||||
string fullPath;
|
||||
|
||||
// 判断是网络URL还是本地文件路径
|
||||
if (path.StartsWith("http://") || path.StartsWith("https://")) {
|
||||
fullPath = path; // 网络URL
|
||||
#if UNITY_EDITOR
|
||||
if (path.StartsWith("http://") && PlayerSettings.insecureHttpOption == InsecureHttpOption.NotAllowed) {
|
||||
Log.ConfigLoader.Error("发起了 HTTP 链接,但设置了不允许非Https连接,请检查设置!!!");
|
||||
|
||||
callback?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// 本地文件路径
|
||||
fullPath = "file://" + path;
|
||||
}
|
||||
|
||||
// 使用UnityWebRequest读取数据
|
||||
using (var webRequest = UnityWebRequest.Get(fullPath)) {
|
||||
yield return webRequest.SendWebRequest();
|
||||
|
||||
if (webRequest.result == UnityWebRequest.Result.ConnectionError ||
|
||||
webRequest.result == UnityWebRequest.Result.ProtocolError) {
|
||||
Log.ConfigLoader.Error($"Error while reading file: {webRequest.error} path: {fullPath}");
|
||||
|
||||
callback?.Invoke(null); // 返回null表示出错
|
||||
}
|
||||
else {
|
||||
var data = webRequest.downloadHandler.text;
|
||||
Log.ConfigLoader.Info($"Data read successfully:\n {data}");
|
||||
|
||||
callback?.Invoke(data); // 通过回调传出数据
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将内容写入指定文件,并删除原有文件。
|
||||
/// </summary>
|
||||
/// <param name="folderName">文件夹名称</param>
|
||||
/// <param name="fileName">文件名称</param>
|
||||
/// <param name="content">要写入的内容</param>
|
||||
public void WriteToPersistentData(string folderName, string fileName, string content) {
|
||||
// 获取持久化数据路径
|
||||
var folderPath = Path.Combine(Application.persistentDataPath, folderName);
|
||||
|
||||
// 如果文件夹不存在,则创建它
|
||||
if (!Directory.Exists(folderPath)) {
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
// 删除原有文件
|
||||
var existingFiles = Directory.GetFiles(folderPath);
|
||||
foreach (var file in existingFiles) {
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
// 完整文件路径
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
|
||||
// 写入新文件
|
||||
File.WriteAllText(filePath, content);
|
||||
|
||||
Log.ConfigLoader.Info($"File written to: {filePath}");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 提取指定目录下所有文件文件名
|
||||
/// </summary>
|
||||
/// <param name="subdirectory"></param>
|
||||
/// <returns></returns>
|
||||
public string[] GetFileNamesFromPersistentDataPath(string subdirectory) {
|
||||
var directoryPath = Path.Combine(Application.persistentDataPath, subdirectory);
|
||||
|
||||
if (Directory.Exists(directoryPath))
|
||||
// 获取目录中的所有文件名
|
||||
{
|
||||
return Directory.GetFiles(directoryPath)
|
||||
.Select(Path.GetFileName) // 只提取文件名
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Warning($"Directory does not exist: {directoryPath}");
|
||||
return Array.Empty<string>(); // 返回空数组
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35da25a8aaf547b8accde3b52c38a887
|
||||
timeCreated: 1731982304
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed771f8b80c42a24e8b44eb1aa5d12bd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb3fbe8a7d2b0ea4c822d6cefeee3708
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class ConfigModel<T, TD> : ConfigModel where T : class {
|
||||
public ConfigModel(string key) : base(key) {
|
||||
DataList = new List<TD>();
|
||||
}
|
||||
|
||||
public List<TD> DataList {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override object Parse(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
return LoadConfigList<T, TD>(dictionary, errorCallback);
|
||||
}
|
||||
|
||||
protected override T1 LoadConfigList<T1, TD1>(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
ParseConfig(dictionary, out List<TD> data, errorCallback);
|
||||
DataList = data;
|
||||
return this as T1;
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigModel<T> : ConfigModel {
|
||||
public ConfigModel(string key) : base(key) {
|
||||
}
|
||||
|
||||
public override object Parse(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
return LoadConfig<T>(dictionary, errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigModel {
|
||||
protected readonly string Key;
|
||||
public bool IsList = false;
|
||||
|
||||
protected ConfigModel(string key) {
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public virtual object Parse(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected T LoadConfig<T>(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
if (ParseConfig(dictionary, out T data, errorCallback)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected virtual T LoadConfigList<T, TD>(IReadOnlyDictionary<string, object> dictionary,
|
||||
UnityAction<string, string> errorCallback = null) where T : class {
|
||||
return default;
|
||||
}
|
||||
|
||||
protected bool ParseConfig<T>(IReadOnlyDictionary<string, object> dictionary, out T obj,
|
||||
UnityAction<string, string> errorCallback = null) {
|
||||
obj = default;
|
||||
if (!dictionary.TryGetValue(Key, out var data)) {
|
||||
errorCallback?.Invoke(Key, "Excel Not find Sheet");
|
||||
Log.ConfigLoader.Warning($"Excel Not find Sheet: {Key}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = data.ToString();
|
||||
try {
|
||||
var tempObj = SerializeHelper.ToObject<T>(config);
|
||||
|
||||
if (tempObj != null) {
|
||||
#if !GAME_RELEASE
|
||||
CheckFields(Key, tempObj, config, FieldCheckType.Missing);
|
||||
CheckFields(Key, tempObj, config, FieldCheckType.Extra);
|
||||
#endif
|
||||
obj = tempObj;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errorCallback?.Invoke(Key, ex.Message);
|
||||
// 在此处添加异常处理逻辑,例如日志记录
|
||||
Log.ConfigLoader.Error("Error deserializing config data for type " + typeof(T).Name + ": " + ex.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CheckFields<T>(string key, T tempObj, string configValue, FieldCheckType checkType) {
|
||||
List<string> fields;
|
||||
|
||||
if (tempObj is IEnumerable<object> tempArray) {
|
||||
fields = CheckFieldsInArray(tempArray.FirstOrDefault()?.GetType(), configValue, checkType);
|
||||
}
|
||||
else {
|
||||
fields = CheckFields(tempObj.GetType(), configValue, checkType);
|
||||
}
|
||||
|
||||
if (fields.Count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fieldType = checkType == FieldCheckType.Missing ? "Missing" : "Extra";
|
||||
Log.ConfigLoader.Warning($"Excel Sheet: {key} | {fieldType} fields in JSON: " + string.Join(", ", fields.Distinct()));
|
||||
}
|
||||
|
||||
private static List<string> CheckFieldsInArray(Type objType, string jsonString, FieldCheckType checkType) {
|
||||
var fields = new List<string>();
|
||||
var jsonArray = JArray.Parse(jsonString);
|
||||
|
||||
foreach (var item in jsonArray) {
|
||||
fields.AddRange(CheckFields(objType, item.ToString(), checkType));
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static List<string> CheckFields(Type objType, string jsonString, FieldCheckType checkType) {
|
||||
var resultFields = new List<string>();
|
||||
|
||||
try {
|
||||
var jsonObject = SerializeHelper.ToObject<Dictionary<string, object>>(jsonString);
|
||||
var fieldNames = objType.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(f => f.Name)
|
||||
.ToHashSet();
|
||||
|
||||
switch (checkType) {
|
||||
// 检查目标类中存在但 JSON 中缺失的字段
|
||||
case FieldCheckType.Missing:
|
||||
resultFields.AddRange(fieldNames.Where(field => !jsonObject.ContainsKey(field)));
|
||||
break;
|
||||
// 检查 JSON 中存在但目标类中不存在的字段
|
||||
case FieldCheckType.Extra:
|
||||
resultFields.AddRange(jsonObject.Keys.Where(key => !fieldNames.Contains(key)));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(checkType), checkType, null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Log.ConfigLoader.Error("Error checking fields for type " + objType.Name + ": " + ex.Message);
|
||||
}
|
||||
|
||||
return resultFields;
|
||||
}
|
||||
|
||||
private enum FieldCheckType {
|
||||
Missing, // 检查缺失字段
|
||||
Extra // 检查多余字段
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1224984e6984416abd0e625b5b842ac
|
||||
timeCreated: 1732095296
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace SGModule.ConfigLoader {
|
||||
// 列表形式的 数据Model类, 演示
|
||||
public class TestListModel : ConfigModel<TestListModel, TestListOneModel> {
|
||||
public TestListModel(string key) : base(key) {
|
||||
}
|
||||
}
|
||||
|
||||
public class TestListOneModel {
|
||||
public int Filed1;
|
||||
public float Filed2;
|
||||
public int Filed3;
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed458f2acffd4b5b813691f3bd548d50
|
||||
timeCreated: 1732007715
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
namespace SGModule.ConfigLoader {
|
||||
// KV形式的 数据Model类, 演示
|
||||
public class TestSingleModel : ConfigModel<TestSingleModel> {
|
||||
public int Field1;
|
||||
public float Field2;
|
||||
|
||||
public TestSingleModel(string key) : base(key) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 820d755e7f654e158e3d67e5965cea16
|
||||
timeCreated: 1732007669
|
||||
Reference in New Issue
Block a user