using GFramework.Game.Abstractions.Config; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace GFramework.Game.Config; /// /// 基于文件目录的 YAML 配置加载器。 /// 该实现用于 Runtime MVP 的文本配置接入阶段,通过显式注册表定义描述要加载的配置域, /// 再在一次加载流程中统一解析并写入配置注册表。 /// public sealed class YamlConfigLoader : IConfigLoader { private const string RootPathCannotBeNullOrWhiteSpaceMessage = "Root path cannot be null or whitespace."; private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace."; private const string RelativePathCannotBeNullOrWhiteSpaceMessage = "Relative path cannot be null or whitespace."; private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage = "Schema relative path cannot be null or whitespace."; private readonly IDeserializer _deserializer; private readonly List _registrations = new(); private readonly string _rootPath; /// /// 使用指定配置根目录创建 YAML 配置加载器。 /// /// 配置根目录。 /// 为空时抛出。 public YamlConfigLoader(string rootPath) { if (string.IsNullOrWhiteSpace(rootPath)) { throw new ArgumentException(RootPathCannotBeNullOrWhiteSpaceMessage, nameof(rootPath)); } _rootPath = rootPath; _deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); } /// /// 获取配置根目录。 /// public string RootPath => _rootPath; /// /// 获取当前已注册的配置表定义数量。 /// public int RegistrationCount => _registrations.Count; /// public async Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(registry); var loadedTables = new List<(string name, IConfigTable table)>(_registrations.Count); foreach (var registration in _registrations) { cancellationToken.ThrowIfCancellationRequested(); loadedTables.Add(await registration.LoadAsync(_rootPath, _deserializer, cancellationToken)); } // 仅当本轮所有配置表都成功加载后才写入注册表,避免暴露部分成功的中间状态。 foreach (var (name, table) in loadedTables) { RegistrationDispatcher.Register(registry, name, table); } } /// /// 注册一个 YAML 配置表定义。 /// 主键提取逻辑由调用方显式提供,以避免在 Runtime MVP 阶段引入额外特性或约定推断。 /// /// 配置主键类型。 /// 配置值类型。 /// 配置表名称。 /// 相对配置根目录的子目录。 /// 配置项主键提取器。 /// 可选主键比较器。 /// 当前加载器实例,以便链式注册。 public YamlConfigLoader RegisterTable( string tableName, string relativePath, Func keySelector, IEqualityComparer? comparer = null) where TKey : notnull { return RegisterTableCore(tableName, relativePath, null, keySelector, comparer); } /// /// 注册一个带 schema 校验的 YAML 配置表定义。 /// 该重载会在 YAML 反序列化之前使用指定 schema 拒绝未知字段、缺失必填字段和基础类型错误, /// 以避免错误配置以默认值形式悄悄进入运行时。 /// /// 配置主键类型。 /// 配置值类型。 /// 配置表名称。 /// 相对配置根目录的子目录。 /// 相对配置根目录的 schema 文件路径。 /// 配置项主键提取器。 /// 可选主键比较器。 /// 当前加载器实例,以便链式注册。 public YamlConfigLoader RegisterTable( string tableName, string relativePath, string schemaRelativePath, Func keySelector, IEqualityComparer? comparer = null) where TKey : notnull { return RegisterTableCore(tableName, relativePath, schemaRelativePath, keySelector, comparer); } private YamlConfigLoader RegisterTableCore( string tableName, string relativePath, string? schemaRelativePath, Func keySelector, IEqualityComparer? comparer) where TKey : notnull { if (string.IsNullOrWhiteSpace(tableName)) { throw new ArgumentException(TableNameCannotBeNullOrWhiteSpaceMessage, nameof(tableName)); } if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException(RelativePathCannotBeNullOrWhiteSpaceMessage, nameof(relativePath)); } ArgumentNullException.ThrowIfNull(keySelector); if (schemaRelativePath != null && string.IsNullOrWhiteSpace(schemaRelativePath)) { throw new ArgumentException( SchemaRelativePathCannotBeNullOrWhiteSpaceMessage, nameof(schemaRelativePath)); } _registrations.Add( new YamlTableRegistration( tableName, relativePath, schemaRelativePath, keySelector, comparer)); return this; } /// /// 负责在非泛型配置表与泛型注册表方法之间做分派。 /// 该静态助手将运行时反射局部封装在加载器内部,避免向外暴露弱类型注册 API。 /// private static class RegistrationDispatcher { /// /// 将强类型配置表写入注册表。 /// /// 目标配置注册表。 /// 配置表名称。 /// 已加载的配置表实例。 /// 当传入表未实现强类型配置表契约时抛出。 public static void Register(IConfigRegistry registry, string name, IConfigTable table) { var tableInterface = table.GetType() .GetInterfaces() .FirstOrDefault(static type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConfigTable<,>)); if (tableInterface == null) { throw new InvalidOperationException( $"Loaded config table '{name}' does not implement '{typeof(IConfigTable<,>).Name}'."); } var genericArguments = tableInterface.GetGenericArguments(); var method = typeof(IConfigRegistry) .GetMethod(nameof(IConfigRegistry.RegisterTable))! .MakeGenericMethod(genericArguments[0], genericArguments[1]); method.Invoke(registry, new object[] { name, table }); } } /// /// 定义 YAML 配置表注册项的统一内部契约。 /// private interface IYamlTableRegistration { /// /// 从指定根目录加载配置表。 /// /// 配置根目录。 /// YAML 反序列化器。 /// 取消令牌。 /// 已加载的配置表名称与配置表实例。 Task<(string name, IConfigTable table)> LoadAsync( string rootPath, IDeserializer deserializer, CancellationToken cancellationToken); } /// /// YAML 配置表注册项。 /// /// 配置主键类型。 /// 配置项值类型。 private sealed class YamlTableRegistration : IYamlTableRegistration where TKey : notnull { private readonly IEqualityComparer? _comparer; private readonly Func _keySelector; /// /// 初始化 YAML 配置表注册项。 /// /// 配置表名称。 /// 相对配置根目录的子目录。 /// 相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。 /// 配置项主键提取器。 /// 可选主键比较器。 public YamlTableRegistration( string name, string relativePath, string? schemaRelativePath, Func keySelector, IEqualityComparer? comparer) { Name = name; RelativePath = relativePath; SchemaRelativePath = schemaRelativePath; _keySelector = keySelector; _comparer = comparer; } /// /// 获取配置表名称。 /// public string Name { get; } /// /// 获取相对配置根目录的子目录。 /// public string RelativePath { get; } /// /// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时返回空。 /// public string? SchemaRelativePath { get; } /// public async Task<(string name, IConfigTable table)> LoadAsync( string rootPath, IDeserializer deserializer, CancellationToken cancellationToken) { var directoryPath = Path.Combine(rootPath, RelativePath); if (!Directory.Exists(directoryPath)) { throw new DirectoryNotFoundException( $"Config directory '{directoryPath}' was not found for table '{Name}'."); } YamlConfigSchema? schema = null; if (!string.IsNullOrEmpty(SchemaRelativePath)) { var schemaPath = Path.Combine(rootPath, SchemaRelativePath); schema = await YamlConfigSchemaValidator.LoadAsync(schemaPath, cancellationToken); } var values = new List(); var files = Directory .EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly) .Where(static path => path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)) .OrderBy(static path => path, StringComparer.Ordinal) .ToArray(); foreach (var file in files) { cancellationToken.ThrowIfCancellationRequested(); string yaml; try { yaml = await File.ReadAllTextAsync(file, cancellationToken); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to read config file '{file}' for table '{Name}'.", exception); } if (schema != null) { // 先按 schema 拒绝结构问题,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。 YamlConfigSchemaValidator.Validate(schema, file, yaml); } try { var value = deserializer.Deserialize(yaml); if (value == null) { throw new InvalidOperationException("YAML content was deserialized to null."); } values.Add(value); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.", exception); } } try { var table = new InMemoryConfigTable(values, _keySelector, _comparer); return (Name, table); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to build config table '{Name}' from directory '{directoryPath}'.", exception); } } } }