CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
365 lines
14 KiB
C#
365 lines
14 KiB
C#
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
|
|
}
|