初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.App.Configuration.Storage;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.App.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 全局配置服务。
|
||||
/// </summary>
|
||||
[LifecycleService(LifecycleState.Loading, Priority = 1919810)]
|
||||
[LifecycleScope("config", "配置")]
|
||||
public sealed partial class ConfigService
|
||||
{
|
||||
private static readonly Dictionary<string, ConfigItem> _Items = [];
|
||||
|
||||
private static readonly HashSet<string> _KeySet = [];
|
||||
|
||||
/// <summary>
|
||||
/// 配置键的集合。
|
||||
/// </summary>
|
||||
public static IReadOnlySet<string> KeySet => _KeySet;
|
||||
|
||||
/// <summary>
|
||||
/// 全局配置文件的版本号。
|
||||
/// </summary>
|
||||
[ConfigItem<int>("FileVersion", 1)] public static partial int SharedVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本地配置文件的版本号。
|
||||
/// </summary>
|
||||
[ConfigItem<int>("LocalFileVersion", 1, ConfigSource.Local)] public static partial int LocalVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 全局共享配置文件路径。
|
||||
/// </summary>
|
||||
public static string SharedConfigPath { get; } = Path.Combine(Paths.SharedData, "config.v1.json");
|
||||
|
||||
/// <summary>
|
||||
/// 本地配置文件路径。
|
||||
/// </summary>
|
||||
public static string LocalConfigPath { get; } = Path.Combine(Paths.Data, "config.v1.yml");
|
||||
|
||||
#region Getters & Setters
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取无泛型的配置项。
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="item">返回可观察对象</param>
|
||||
/// <returns>若配置键存在,则为 <c>true</c>,否则为 <c>false</c></returns>
|
||||
public static bool TryGetConfigItemNoType(string key, [NotNullWhen(true)] out ConfigItem? item)
|
||||
=> _Items.TryGetValue(key, out item);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取配置项。
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="item">返回配置项,若类型不匹配则为 <c>null</c></param>
|
||||
/// <typeparam name="TValue">配置项的值类型</typeparam>
|
||||
/// <returns>若配置键存在,则为 <c>true</c>,否则为 <c>false</c></returns>
|
||||
/// <exception cref="InvalidOperationException">配置项尚未初始化完成</exception>
|
||||
public static bool TryGetConfigItem<TValue>(string key, out ConfigItem<TValue>? item)
|
||||
{
|
||||
if (!_isConfigItemsInitialized) throw new InvalidOperationException("Not initialized");
|
||||
var result = TryGetConfigItemNoType(key, out var value);
|
||||
item = result ? (value as ConfigItem<TValue>) : null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置项。
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <typeparam name="TValue">配置项的值类型</typeparam>
|
||||
/// <returns>配置项实例</returns>
|
||||
/// <exception cref="InvalidOperationException">配置项尚未初始化完成</exception>
|
||||
/// <exception cref="KeyNotFoundException">配置键不存在</exception>
|
||||
/// <exception cref="InvalidCastException">值类型参数与实际类型不匹配</exception>
|
||||
public static ConfigItem<TValue> GetConfigItem<TValue>(string key)
|
||||
{
|
||||
var result = TryGetConfigItem<TValue>(key, out var item);
|
||||
if (!result) throw new KeyNotFoundException($"Config key not found: '{key}'");
|
||||
return item ?? throw new InvalidCastException($"Type of '{key}' is incompatible with {typeof(TValue).FullName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按键设置配置值,自动处理类型匹配。若键不存在则静默失败。
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="value">配置值</param>
|
||||
/// <param name="argument">上下文参数(实例路径等)</param>
|
||||
public static void TrySetValue(string key, object value, object? argument = null)
|
||||
{
|
||||
if (!TryGetConfigItemNoType(key, out var item)) return;
|
||||
if (item.Type.IsEnum && value is not string)
|
||||
item.SetValueNoType(Enum.ToObject(item.Type, value), argument);
|
||||
else
|
||||
item.SetValueNoType(value, argument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向指定作用域批量注册事件观察器。
|
||||
/// </summary>
|
||||
/// <param name="scope"><see cref="IConfigScope"/> 实例</param>
|
||||
/// <param name="observer">观察器实例</param>
|
||||
public static void RegisterObserver(IConfigScope scope, ConfigObserver observer)
|
||||
{
|
||||
var itemKeys = scope.CheckScope(KeySet);
|
||||
foreach (var key in itemKeys)
|
||||
{
|
||||
var item = _Items[key];
|
||||
item.Observe(observer);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Providers
|
||||
|
||||
private static ConfigStorage? _sharedConfigProvider;
|
||||
private static ConfigStorage? _sharedEncryptedConfigProvider;
|
||||
private static ConfigStorage? _localConfigProvider;
|
||||
private static ConfigStorage? _instanceConfigProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置提供方。
|
||||
/// </summary>
|
||||
/// <param name="source">来源定义</param>
|
||||
/// <returns>提供方实例</returns>
|
||||
/// <exception cref="InvalidOperationException">配置提供方尚未初始化完成</exception>
|
||||
/// <exception cref="ArgumentException">来源定义无效</exception>
|
||||
public static IConfigProvider GetProvider(ConfigSource source)
|
||||
{
|
||||
if (!_isProvidersInitialized) throw new InvalidOperationException("Not initialized");
|
||||
return source switch
|
||||
{
|
||||
ConfigSource.Shared => _sharedConfigProvider!,
|
||||
ConfigSource.SharedEncrypt => _sharedEncryptedConfigProvider!,
|
||||
ConfigSource.Local => _localConfigProvider!,
|
||||
ConfigSource.GameInstance => _instanceConfigProvider!,
|
||||
_ => throw new ArgumentException($"Invalid source: {source}")
|
||||
};
|
||||
}
|
||||
|
||||
private static void _InitializeProviders()
|
||||
{
|
||||
Action[] inits = [
|
||||
() => // shared config file
|
||||
{
|
||||
// try migrate
|
||||
if (!File.Exists(SharedConfigPath))
|
||||
{
|
||||
string[] oldPaths = [
|
||||
Path.Combine(Paths.OldSharedData, "Config.json"),
|
||||
Path.Combine(Paths.SharedData, "config.json")
|
||||
];
|
||||
_TryMigrate(SharedConfigPath, oldPaths.Select(path =>
|
||||
new ConfigMigration { From = path, To = SharedConfigPath, OnMigration = SharedJsonMigration }));
|
||||
}
|
||||
// load
|
||||
var fileProvider = new JsonFileProvider(SharedConfigPath);
|
||||
var storage = new FileConfigStorage(fileProvider);
|
||||
_sharedConfigProvider = storage;
|
||||
_sharedEncryptedConfigProvider = new EncryptedFileConfigStorage(storage);
|
||||
},
|
||||
() => // local config file
|
||||
{
|
||||
// try migrate
|
||||
if (!File.Exists(LocalConfigPath)) _TryMigrate(LocalConfigPath, [
|
||||
new ConfigMigration
|
||||
{
|
||||
From = Path.Combine(Paths.Data, "setup.ini"),
|
||||
To = LocalConfigPath,
|
||||
OnMigration = CatIniMigration
|
||||
}
|
||||
]);
|
||||
// load
|
||||
var fileProvider = new YamlFileProvider(LocalConfigPath);
|
||||
_localConfigProvider = new FileConfigStorage(fileProvider);
|
||||
},
|
||||
() => // instance config file(s)
|
||||
{
|
||||
_instanceConfigProvider = new DynamicCacheConfigStorage
|
||||
{
|
||||
StorageFactory = argument =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(argument);
|
||||
var dir = Path.GetFullPath(argument.ToString()!);
|
||||
var configPath = Path.Combine(dir, "PCL", "config.v1.yml");
|
||||
if (!File.Exists(dir)) _TryMigrate(dir, [
|
||||
new ConfigMigration
|
||||
{
|
||||
From = Path.Combine(dir, "PCL", "setup.ini"),
|
||||
To = configPath,
|
||||
OnMigration = CatIniMigration
|
||||
}
|
||||
]);
|
||||
var fileProvider = new YamlFileProvider(configPath);
|
||||
var storage = new FileConfigStorage(fileProvider);
|
||||
return storage;
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
try { Task.WaitAll(inits.Select(Task.Run).ToArray()); }
|
||||
catch (AggregateException ex) { throw ex.GetBaseException(); }
|
||||
|
||||
return;
|
||||
void SharedJsonMigration(string from, string to)
|
||||
{
|
||||
File.Copy(from, to);
|
||||
}
|
||||
void CatIniMigration(string from, string to)
|
||||
{
|
||||
var lines = File.ReadAllLines(from);
|
||||
var yamlProvider = new YamlFileProvider(to);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.IsNullOrWhiteSpace()) continue;
|
||||
var kv = line.Split(':', 2);
|
||||
if (kv.Length != 2) continue;
|
||||
yamlProvider.Set(kv[0], kv[1]);
|
||||
}
|
||||
yamlProvider.Sync();
|
||||
}
|
||||
}
|
||||
|
||||
private static void _TryMigrate(string target, IEnumerable<ConfigMigration> migrations)
|
||||
{
|
||||
Context.Info($"Try migrating config: {target}");
|
||||
try
|
||||
{
|
||||
var result = ConfigMigration.Migrate(target, migrations);
|
||||
if (!result) Context.Info("No migration solution available");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Context.Warn("Migration failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle & Initialization
|
||||
|
||||
/// <summary>
|
||||
/// 配置服务是否已加载完成。未加载完成时,调用与配置项相关的方法可能会抛出 <see cref="InvalidOperationException"/>。
|
||||
/// </summary>
|
||||
public static bool IsInitialized { get; private set; } = false;
|
||||
|
||||
private static bool _isProvidersInitialized = false;
|
||||
private static bool _isConfigItemsInitialized = false;
|
||||
|
||||
[LifecycleStart]
|
||||
private static void _Start()
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
#if TRACE
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
#endif
|
||||
Context.Info("Config initialization started");
|
||||
try
|
||||
{
|
||||
Context.Trace("Initializing config items...");
|
||||
_InitializeConfigItems();
|
||||
Context.Debug($"Finished initialize {_Items.Count} item(s)");
|
||||
_isConfigItemsInitialized = true;
|
||||
Context.Trace("Initializing providers...");
|
||||
_InitializeProviders();
|
||||
_isProvidersInitialized = true;
|
||||
Context.Trace("Initializing observers...");
|
||||
_InitializeObservers();
|
||||
Context.Info("Invoking init events...");
|
||||
foreach (var (_, item) in _Items)
|
||||
{
|
||||
item.TriggerEvent(ConfigEvent.Init, null, true, true);
|
||||
}
|
||||
IsInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var currentSection = _isConfigItemsInitialized ? "OBSERVER" :
|
||||
_isProvidersInitialized ? "CONFIG_ITEM" : "PROVIDER";
|
||||
string msg;
|
||||
#if DEBUG
|
||||
msg = Lang.Text("Config.Error.LoadFailed.DebugMessage", currentSection);
|
||||
#else
|
||||
if (ex is ConfigFileInitException e)
|
||||
{
|
||||
var filePath = e.Path;
|
||||
var backupPath = e.Path + ".failbackup";
|
||||
var bakPath = e.Path + ".bak";
|
||||
File.Move(filePath, backupPath, true);
|
||||
if (File.Exists(bakPath)) File.Copy(bakPath, filePath, true);
|
||||
msg = Lang.Text(
|
||||
"Config.Error.InvalidFormat.RecoveryMessage",
|
||||
currentSection,
|
||||
filePath,
|
||||
backupPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg = Lang.Text("Config.Error.LoadFailed.Message", currentSection);
|
||||
}
|
||||
#endif
|
||||
Context.Fatal(msg, ex);
|
||||
}
|
||||
#if TRACE
|
||||
timer.Stop();
|
||||
Context.Info($"Config initialization finished in {timer.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
[LifecycleStop]
|
||||
private static void _Stop()
|
||||
{
|
||||
// 检测是否初始化出错
|
||||
if (Lifecycle.GetServiceLastException(Service.Identifier) is { } ex)
|
||||
{
|
||||
Context.Fatal(Lang.Text("Config.Error.LoadFailed.Title"), ex);
|
||||
return;
|
||||
}
|
||||
|
||||
Context.Info("Saving config...");
|
||||
// 停止物流中心并释放资源
|
||||
_sharedConfigProvider?.Stop();
|
||||
_localConfigProvider?.Stop();
|
||||
_instanceConfigProvider?.Stop();
|
||||
}
|
||||
|
||||
[RegisterConfigEvent]
|
||||
public static ConfigEventRegistry SharedVersionInit => new(
|
||||
SharedVersionConfig,
|
||||
trigger: ConfigEvent.Init,
|
||||
handler: e => _UpdateConfigVersion(SharedVersionConfig, "全局", (int)e.NewValue!)
|
||||
);
|
||||
|
||||
[RegisterConfigEvent]
|
||||
public static ConfigEventRegistry LocalVersionInit => new(
|
||||
scope: LocalVersionConfig,
|
||||
trigger: ConfigEvent.Init,
|
||||
handler: e => _UpdateConfigVersion(LocalVersionConfig, "本地", (int)e.NewValue!)
|
||||
);
|
||||
|
||||
private static void _UpdateConfigVersion(ConfigItem<int> versionConfig, string name, int fileVersion)
|
||||
{
|
||||
var targetVersion = versionConfig.DefaultValue;
|
||||
var isUnset = versionConfig.IsDefault();
|
||||
LogWrapper.Info($"{name}配置: 文件版本 {(isUnset ? "UNSET" : fileVersion)}, 目标版本 {targetVersion}");
|
||||
if (isUnset || targetVersion != fileVersion) versionConfig.SetValue(targetVersion);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user