Files
MRCC/launcher/PCL-CE/PCL.Core/App/Configuration/ConfigService.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

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
}