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; /// /// 全局配置服务。 /// [LifecycleService(LifecycleState.Loading, Priority = 1919810)] [LifecycleScope("config", "配置")] public sealed partial class ConfigService { private static readonly Dictionary _Items = []; private static readonly HashSet _KeySet = []; /// /// 配置键的集合。 /// public static IReadOnlySet KeySet => _KeySet; /// /// 全局配置文件的版本号。 /// [ConfigItem("FileVersion", 1)] public static partial int SharedVersion { get; set; } /// /// 本地配置文件的版本号。 /// [ConfigItem("LocalFileVersion", 1, ConfigSource.Local)] public static partial int LocalVersion { get; set; } /// /// 全局共享配置文件路径。 /// public static string SharedConfigPath { get; } = Path.Combine(Paths.SharedData, "config.v1.json"); /// /// 本地配置文件路径。 /// public static string LocalConfigPath { get; } = Path.Combine(Paths.Data, "config.v1.yml"); #region Getters & Setters /// /// 尝试获取无泛型的配置项。 /// /// 配置键 /// 返回可观察对象 /// 若配置键存在,则为 true,否则为 false public static bool TryGetConfigItemNoType(string key, [NotNullWhen(true)] out ConfigItem? item) => _Items.TryGetValue(key, out item); /// /// 尝试获取配置项。 /// /// 配置键 /// 返回配置项,若类型不匹配则为 null /// 配置项的值类型 /// 若配置键存在,则为 true,否则为 false /// 配置项尚未初始化完成 public static bool TryGetConfigItem(string key, out ConfigItem? item) { if (!_isConfigItemsInitialized) throw new InvalidOperationException("Not initialized"); var result = TryGetConfigItemNoType(key, out var value); item = result ? (value as ConfigItem) : null; return result; } /// /// 获取配置项。 /// /// 配置键 /// 配置项的值类型 /// 配置项实例 /// 配置项尚未初始化完成 /// 配置键不存在 /// 值类型参数与实际类型不匹配 public static ConfigItem GetConfigItem(string key) { var result = TryGetConfigItem(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}"); } /// /// 按键设置配置值,自动处理类型匹配。若键不存在则静默失败。 /// /// 配置键 /// 配置值 /// 上下文参数(实例路径等) 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); } /// /// 向指定作用域批量注册事件观察器。 /// /// 实例 /// 观察器实例 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; /// /// 获取配置提供方。 /// /// 来源定义 /// 提供方实例 /// 配置提供方尚未初始化完成 /// 来源定义无效 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 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 /// /// 配置服务是否已加载完成。未加载完成时,调用与配置项相关的方法可能会抛出 。 /// 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 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 }