fix:1、更换项目,使用winter来创建
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,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class ConfigDataManager {
|
||||
private readonly Dictionary<Type, object> _configData = new();
|
||||
private readonly HashSet<string> _usedKeys = new();
|
||||
private Dictionary<string, object> _jsonDictionary = new();
|
||||
|
||||
|
||||
public void SetJsonDictionary(string json) {
|
||||
_jsonDictionary = SerializeHelper.ToObject<Dictionary<string, object>>(json);
|
||||
_usedKeys.Clear();
|
||||
}
|
||||
|
||||
public void AddConfig(Type type, object instance) {
|
||||
_configData[type] = instance;
|
||||
}
|
||||
|
||||
public void AddConfig(object configModel) {
|
||||
var type = configModel.GetType();
|
||||
_configData[type] = configModel;
|
||||
}
|
||||
|
||||
public T GetConfig<T>() where T : class {
|
||||
if (_configData.TryGetValue(typeof(T), out var obj)) {
|
||||
return obj as T;
|
||||
}
|
||||
|
||||
Log.ConfigLoader.Error($"未找到类型 {typeof(T).FullName} 的配置");
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryGetJsonValue(string key, out object value) {
|
||||
if (!_jsonDictionary.TryGetValue(key, out value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_usedKeys.Add(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetUnusedKeys() {
|
||||
return _jsonDictionary.Keys.Except(_usedKeys);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetJsonKeys() {
|
||||
return _jsonDictionary.Keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 223b606b799d46c2a7e684cba7c5df6a
|
||||
timeCreated: 1753950649
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SGModule.Common.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class ConfigFileManager {
|
||||
private const int MaxErrorCount = 6;
|
||||
private const string ConfigFileNameKey = "ConfigFileName";
|
||||
private const string FirstLaunchKey = "FirstLaunch";
|
||||
|
||||
private int _initConfigErrorCount;
|
||||
|
||||
private bool HasExceededMaxErrors => _initConfigErrorCount > MaxErrorCount;
|
||||
private static bool IsFirstLaunch => !PlayerPrefs.HasKey(FirstLaunchKey);
|
||||
private static string SavedConfigFileName => PlayerPrefs.GetString(ConfigFileNameKey);
|
||||
|
||||
private static void SetFirstLaunch() {
|
||||
PlayerPrefs.SetInt(FirstLaunchKey, 1);
|
||||
}
|
||||
|
||||
private static void SetSavedConfigFileName(string name) {
|
||||
PlayerPrefs.SetString(ConfigFileNameKey, name);
|
||||
}
|
||||
|
||||
private void IncrementErrorCount() {
|
||||
_initConfigErrorCount++;
|
||||
}
|
||||
|
||||
// 检查是否需要下载新配置,基于传入的期望配置文件名setting
|
||||
private static bool HasNewConfig(string expectedSetting) {
|
||||
if (string.IsNullOrEmpty(SavedConfigFileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !SavedConfigFileName.Equals(expectedSetting);
|
||||
}
|
||||
|
||||
private void FirstLaunchCopyConfig(Action<bool> callback) {
|
||||
SetFirstLaunch();
|
||||
|
||||
FileNetworkManager.Instance.CopyStreamingAssetsToPersistentDataPath(result => {
|
||||
var isSuccess = false;
|
||||
if (result) {
|
||||
var names = FileNetworkManager.Instance.GetFileNamesFromPersistentDataPath(FileNetworkManager.FolderName);
|
||||
if (names.Length > 0) {
|
||||
SetSavedConfigFileName(names[0]);
|
||||
isSuccess = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("拷贝文件失败 请检查Configs文件夹是否有配置文件");
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
IncrementErrorCount();
|
||||
}
|
||||
|
||||
callback?.Invoke(isSuccess);
|
||||
});
|
||||
}
|
||||
|
||||
private void DownloadConfig(string cdnUrl, string setting, Action<bool, string> callback) {
|
||||
var url = $"{cdnUrl}/config/{setting}";
|
||||
Log.ConfigLoader.Info($"开始下载配置文件 {url}", false);
|
||||
FileNetworkManager.Instance.ReadData(url, result => {
|
||||
if (!string.IsNullOrEmpty(result)) {
|
||||
SetSavedConfigFileName(setting);
|
||||
FileNetworkManager.Instance.WriteToPersistentData(FileNetworkManager.Instance.GetConfigFOlderPath(), setting, result);
|
||||
callback?.Invoke(true, result);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("下载配置文件失败");
|
||||
IncrementErrorCount();
|
||||
callback?.Invoke(false, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ReadLocalConfig(Action<bool, string> callback) {
|
||||
var savedCfgName = SavedConfigFileName;
|
||||
Log.ConfigLoader.Info($"开始读取本地配置文件 {savedCfgName}", false);
|
||||
if (string.IsNullOrEmpty(savedCfgName)) {
|
||||
IncrementErrorCount();
|
||||
callback?.Invoke(false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path.Combine(FileNetworkManager.Instance.GetConfigFOlderPath(), savedCfgName);
|
||||
FileNetworkManager.Instance.ReadData(path, result => {
|
||||
if (!string.IsNullOrEmpty(result)) {
|
||||
callback?.Invoke(true, result);
|
||||
}
|
||||
else {
|
||||
Log.ConfigLoader.Error("读取本地数据异常");
|
||||
IncrementErrorCount();
|
||||
callback?.Invoke(false, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件检查与加载流程
|
||||
/// </summary>
|
||||
public void CheckAndLoadConfig(ConfigInitOptions initOptions, Action<string> onConfigLoaded, Action<ConfigLoaderState> callback) {
|
||||
if (HasExceededMaxErrors) {
|
||||
callback?.Invoke(ConfigLoaderState.Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFirstLaunch) {
|
||||
FirstLaunchCopyConfig(success => {
|
||||
if (!success) {
|
||||
IncrementErrorCount();
|
||||
}
|
||||
|
||||
CheckAndLoadConfig(initOptions, onConfigLoaded, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasNewConfig(initOptions.Setting)) {
|
||||
DownloadConfig(initOptions.CdnUrl, initOptions.Setting, (success, json) => {
|
||||
if (success) {
|
||||
ReloadConfig(json, onConfigLoaded, callback);
|
||||
}
|
||||
else {
|
||||
ReadLocalConfig((readSuccess, localJson) => {
|
||||
if (readSuccess) {
|
||||
ReloadConfig(localJson, onConfigLoaded, callback);
|
||||
}
|
||||
else {
|
||||
callback?.Invoke(ConfigLoaderState.Failed);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ReadLocalConfig((success, json) => {
|
||||
if (success) {
|
||||
ReloadConfig(json, onConfigLoaded, callback);
|
||||
}
|
||||
else {
|
||||
FirstLaunchCopyConfig(result => {
|
||||
if (!result) {
|
||||
IncrementErrorCount();
|
||||
}
|
||||
|
||||
CheckAndLoadConfig(initOptions, onConfigLoaded, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ReloadConfig(string json, Action<string> onConfigLoaded, Action<ConfigLoaderState> callback) {
|
||||
if (string.IsNullOrWhiteSpace(json)) {
|
||||
callback?.Invoke(ConfigLoaderState.JsonEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
onConfigLoaded?.Invoke(json);
|
||||
callback?.Invoke(ConfigLoaderState.Successful);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecda816ab1fc4edaa3948be9e79fbdd2
|
||||
timeCreated: 1753950339
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public enum ConfigLoaderState {
|
||||
None,
|
||||
Failed,
|
||||
JsonEmptyError,
|
||||
Successful
|
||||
}
|
||||
|
||||
public class ConfigInitOptions {
|
||||
// 配置文件名
|
||||
public string Setting {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// 下载配置的地址
|
||||
public string CdnUrl {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// 配置加载回调
|
||||
public Action<ConfigLoaderState> OnComplete {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// 配置解析错误回调
|
||||
public Action<string, string> OnError {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// 处理未标记的配置
|
||||
public Action OnHandleUnmarkedConfig {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f04e729bc4b34a4c8a44785e9aef732b
|
||||
timeCreated: 1753946665
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SGModule.Common;
|
||||
using SGModule.Common.Base;
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class ConfigLoader : SingletonMonoBehaviour<ConfigLoader> {
|
||||
private readonly ConfigDataManager _dataManager = new();
|
||||
private readonly ConfigFileManager _fileManager = new();
|
||||
private ConfigInitOptions _initOptions;
|
||||
private bool _isConfigLoaded;
|
||||
|
||||
|
||||
public void Init(ConfigInitOptions options) {
|
||||
_initOptions = options ?? throw new ArgumentNullException(nameof(options));
|
||||
IsInitComplete = true;
|
||||
|
||||
Log.ConfigLoader.Info("Setting: " + _initOptions.Setting);
|
||||
|
||||
_fileManager.CheckAndLoadConfig(_initOptions, ReloadConfig, state => {
|
||||
if (state == ConfigLoaderState.Successful) {
|
||||
Log.ConfigLoader.Info($"Data read successfully: {SerializeHelper.ToJsonIndented(GetJsonKeys())}", false);
|
||||
ConfigParser.ParseAllConfigs(new ParseConfigOptions {
|
||||
OnParsed = (type, instance) => _dataManager.AddConfig(type, instance),
|
||||
OnError = _initOptions.OnError,
|
||||
OnHandleUnmarkedConfig = _initOptions.OnHandleUnmarkedConfig
|
||||
});
|
||||
}
|
||||
|
||||
_initOptions.OnComplete?.Invoke(state);
|
||||
});
|
||||
}
|
||||
|
||||
private void ReloadConfig(string json) {
|
||||
if (string.IsNullOrWhiteSpace(json)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.StartsWith("{")) {
|
||||
json = Cryptor.Decrypt(json, ConfigManager.GameConfig.packageName);
|
||||
}
|
||||
|
||||
_dataManager.SetJsonDictionary(json);
|
||||
_isConfigLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
public T GetConfig<T>() where T : class {
|
||||
return _dataManager.GetConfig<T>();
|
||||
}
|
||||
|
||||
public void AddConfig(object configModel) {
|
||||
_dataManager.AddConfig(configModel);
|
||||
}
|
||||
|
||||
public bool TryGetJsonValue(string key, out object value) {
|
||||
return _dataManager.TryGetJsonValue(key, out value);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetUnusedKeys() {
|
||||
return _dataManager.GetUnusedKeys();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetJsonKeys() {
|
||||
return _dataManager.GetJsonKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9336c81edc514b209f5d055056dbee52
|
||||
timeCreated: 1753950686
|
||||
@@ -0,0 +1,235 @@
|
||||
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(".json"));
|
||||
|
||||
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;
|
||||
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,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f15ea40a5b634081a8b21a4af531f8b5
|
||||
timeCreated: 1753869849
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class ConfigKeyAttribute : Attribute {
|
||||
public ConfigKeyAttribute(string key, bool isList = true) {
|
||||
Key = key;
|
||||
IsList = isList;
|
||||
}
|
||||
|
||||
public string Key {
|
||||
get;
|
||||
}
|
||||
|
||||
public bool IsList {
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93c57a91da9d46baba3feb3a46719bb2
|
||||
timeCreated: 1753869869
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SGModule.Common.Helper;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public static class ConfigParser {
|
||||
public static void ParseAllConfigs(ParseConfigOptions options) {
|
||||
var onParsed = options.OnParsed;
|
||||
var errorCallback = options.OnError;
|
||||
var onHandleUnmarkedConfig = options.OnHandleUnmarkedConfig;
|
||||
|
||||
var configTypes = Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(t => t.GetCustomAttribute<ConfigKeyAttribute>() != null);
|
||||
|
||||
|
||||
foreach (var type in configTypes) {
|
||||
var attr = type.GetCustomAttribute<ConfigKeyAttribute>();
|
||||
var key = attr.Key;
|
||||
|
||||
|
||||
if (!ConfigLoader.Instance.TryGetJsonValue(key, out var jsonData)) {
|
||||
errorCallback?.Invoke(key, "未找到对应配置数据");
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var json = jsonData.ToString();
|
||||
var targetType = attr.IsList ? typeof(List<>).MakeGenericType(type) : type;
|
||||
var parsedInstance = JsonConvert.DeserializeObject(json, targetType);
|
||||
|
||||
if (parsedInstance == null) {
|
||||
errorCallback?.Invoke(key, "解析返回空");
|
||||
continue;
|
||||
}
|
||||
|
||||
#if !GAME_RELEASE
|
||||
RunFieldCheck(json, type, key, attr.IsList);
|
||||
#endif
|
||||
|
||||
// 把结果交还给调用方
|
||||
onParsed?.Invoke(targetType, parsedInstance);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
errorCallback?.Invoke(key, $"反序列化异常: {ex.Message}");
|
||||
Log.ConfigLoader.Error($"配置 {key} 反序列化异常: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理未标记的配置
|
||||
onHandleUnmarkedConfig?.Invoke();
|
||||
|
||||
// 打印未使用的 jsonDict 中的 key
|
||||
var unusedKeys = ConfigLoader.Instance.GetUnusedKeys().ToList();
|
||||
if (unusedKeys.Count > 0) {
|
||||
Log.ConfigLoader.Warning($"未使用的配置表: {string.Join(", ", unusedKeys)};", false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if !GAME_RELEASE
|
||||
private static void RunFieldCheck(string json, Type type, string key, bool isList) {
|
||||
var objectsToCheck = new List<JObject>();
|
||||
|
||||
try {
|
||||
if (isList) {
|
||||
var jsonArray = JArray.Parse(json);
|
||||
objectsToCheck.AddRange(jsonArray.OfType<JObject>());
|
||||
}
|
||||
else {
|
||||
var singleObj = JObject.Parse(json);
|
||||
objectsToCheck.Add(singleObj);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Log.ConfigLoader.Warning($"[{key}] JSON 解析失败: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var classFields = type.GetFields(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(f => f.Name)
|
||||
.ToHashSet();
|
||||
|
||||
var allMissingFields = new HashSet<string>();
|
||||
var allExtraFields = new HashSet<string>();
|
||||
|
||||
foreach (var obj in objectsToCheck) {
|
||||
var jsonKeys = obj.Properties().Select(p => p.Name).ToHashSet();
|
||||
|
||||
allMissingFields.UnionWith(classFields.Except(jsonKeys));
|
||||
allExtraFields.UnionWith(jsonKeys.Except(classFields));
|
||||
}
|
||||
|
||||
if (allMissingFields.Count > 0 || allExtraFields.Count > 0) {
|
||||
var msg = $"[{key}]";
|
||||
if (allMissingFields.Count > 0) {
|
||||
msg += $" 缺少字段: {string.Join(", ", allMissingFields.OrderBy(f => f))};";
|
||||
}
|
||||
|
||||
if (allExtraFields.Count > 0) {
|
||||
msg += $" 多余字段: {string.Join(", ", allExtraFields.OrderBy(f => f))};";
|
||||
}
|
||||
|
||||
var shortJson = json.Length > 1000 ? json.Substring(0, 1000) + "..." : json;
|
||||
Log.ConfigLoader.Warning($"[{key}] Json: {shortJson}", false);
|
||||
Log.ConfigLoader.Warning(msg, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddad7a16d91d4f48901b7217aa6af0e9
|
||||
timeCreated: 1753870514
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace SGModule.ConfigLoader {
|
||||
public class ParseConfigOptions {
|
||||
public Action<Type, object> OnParsed {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Action<string, string> OnError {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Action OnHandleUnmarkedConfig {
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c27fb301f6224b778cdfd006ca12d7e5
|
||||
timeCreated: 1753947404
|
||||
Reference in New Issue
Block a user