diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs index b38bdcec..6aa7a8e2 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Godot.Config; using NUnit.Framework; @@ -176,6 +177,31 @@ public sealed class GodotYamlConfigLoaderTests }); } + /// + /// 验证运行时缓存目录无法重置时,Godot 适配层仍会返回结构化的配置加载诊断。 + /// + [Test] + public void LoadAsync_Should_Wrap_Runtime_Cache_Directory_Reset_Failure_As_ConfigLoadException() + { + CreateMonsterFiles(_resourceRoot); + WriteFile(_userRoot, "config_cache", "occupied"); + + var loader = CreateLoader(isEditor: false); + + var exception = Assert.ThrowsAsync(async () => + await loader.LoadAsync(new ConfigRegistry())); + + Assert.Multiple(() => + { + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConfigFileReadFailed)); + Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster")); + Assert.That(exception.Diagnostic.ConfigDirectoryPath, Is.EqualTo(Path.Combine(_resourceRoot, "monster"))); + Assert.That(exception.Diagnostic.Detail, Does.Contain(Path.Combine(_userRoot, "config_cache", "monster"))); + Assert.That(exception.InnerException, Is.InstanceOf()); + }); + } + /// /// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。 /// diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs index 36f56e52..5e4998ce 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigTableSourceTests.cs @@ -15,11 +15,16 @@ public sealed class GodotYamlConfigTableSourceTests /// /// 待验证的配置目录路径。 [TestCase("../outside")] + [TestCase(@"..\outside")] [TestCase("./monster")] + [TestCase(@".\monster")] [TestCase("monster/../outside")] + [TestCase(@"monster\..\outside")] [TestCase("monster/./child")] + [TestCase(@"monster\.\child")] [TestCase("/monster")] [TestCase("C:/monster")] + [TestCase(@"C:\monster")] [TestCase("res://monster")] [TestCase("user://monster")] public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath) @@ -35,11 +40,16 @@ public sealed class GodotYamlConfigTableSourceTests /// /// 待验证的 schema 路径。 [TestCase("../schemas/monster.schema.json")] + [TestCase(@"..\schemas\monster.schema.json")] [TestCase("./schemas/monster.schema.json")] + [TestCase(@".\schemas\monster.schema.json")] [TestCase("schemas/../monster.schema.json")] + [TestCase(@"schemas\..\monster.schema.json")] [TestCase("schemas/./monster.schema.json")] + [TestCase(@"schemas\.\monster.schema.json")] [TestCase("/schemas/monster.schema.json")] [TestCase("C:/schemas/monster.schema.json")] + [TestCase(@"C:\schemas\monster.schema.json")] [TestCase("res://schemas/monster.schema.json")] [TestCase("user://schemas/monster.schema.json")] public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath) diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index 7ba1e9e5..68943834 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -22,6 +22,20 @@ public sealed class GodotYamlConfigLoader : IConfigLoader /// 使用指定选项创建一个 Godot YAML 配置加载器。 /// /// 加载器初始化选项。 + /// 时抛出。 + /// + /// 当 或 + /// 为空白字符串时抛出。 + /// + /// + /// 当 Godot 特殊路径无法被全局化为非空绝对路径时抛出。 + /// + /// + /// 构造完成后,加载器会根据当前环境决定直接读取 ,还是先同步到 + /// 再交给底层 。 + /// 只有源根目录可直接作为普通文件系统目录访问时, 才会返回 + /// 。 + /// public GodotYamlConfigLoader(GodotYamlConfigLoaderOptions options) : this(options, GodotYamlConfigEnvironment.Default) { @@ -115,6 +129,19 @@ public sealed class GodotYamlConfigLoader : IConfigLoader /// 要被热重载更新的配置注册表。 /// 热重载选项;为空时使用默认值。 /// 用于停止监听的注销句柄。 + /// 时抛出。 + /// + /// 当当前实例必须通过运行时缓存访问配置源,无法直接监听真实源目录时抛出。 + /// + /// + /// 当 的防抖延迟小于 时, + /// 底层 会拒绝启用热重载。 + /// + /// + /// 调用前应先检查 。 + /// 当 只能通过缓存同步访问时,拒绝启用热重载是为了避免监听缓存副本后误导调用方, + /// 让其误以为源目录改动会被自动反映到运行时。 + /// public IUnRegister EnableHotReload( IConfigRegistry registry, YamlConfigHotReloadOptions? options = null) @@ -171,7 +198,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader var sourceDirectoryPath = CombinePath(SourceRootPath, representative.ConfigRelativePath); var targetDirectoryPath = CombineAbsolutePath(LoaderRootPath, representative.ConfigRelativePath); - ResetDirectory(targetDirectoryPath); + ResetDirectory(representative.TableName, sourceDirectoryPath, targetDirectoryPath); CopyYamlFilesInDirectory( representative.TableName, sourceDirectoryPath, @@ -215,8 +242,6 @@ public sealed class GodotYamlConfigLoader : IConfigLoader configDirectoryPath: DescribePath(sourceDirectoryPath)); } - Directory.CreateDirectory(targetDirectoryPath); - foreach (var entry in entries) { cancellationToken.ThrowIfCancellationRequested(); @@ -303,14 +328,28 @@ public sealed class GodotYamlConfigLoader : IConfigLoader } } - private void ResetDirectory(string directoryPath) + private void ResetDirectory(string tableName, string sourceDirectoryPath, string targetDirectoryPath) { - if (Directory.Exists(directoryPath)) + try { - Directory.Delete(directoryPath, recursive: true); - } + if (Directory.Exists(targetDirectoryPath)) + { + Directory.Delete(targetDirectoryPath, recursive: true); + } - Directory.CreateDirectory(directoryPath); + Directory.CreateDirectory(targetDirectoryPath); + } + catch (Exception exception) + { + var describedSourceDirectoryPath = DescribePath(sourceDirectoryPath); + throw CreateConfigLoadException( + ConfigLoadFailureKind.ConfigFileReadFailed, + tableName, + $"Failed to reset runtime cache directory '{targetDirectoryPath}' while preparing config directory '{describedSourceDirectoryPath}'.", + configDirectoryPath: describedSourceDirectoryPath, + detail: $"Runtime cache directory: {targetDirectoryPath}.", + innerException: exception); + } } private string EnsureAbsolutePath(string path, string optionName) @@ -417,6 +456,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader string? configDirectoryPath = null, string? yamlPath = null, string? schemaPath = null, + string? detail = null, Exception? innerException = null) { return new ConfigLoadException( @@ -425,7 +465,8 @@ public sealed class GodotYamlConfigLoader : IConfigLoader tableName, configDirectoryPath: configDirectoryPath, yamlPath: yamlPath, - schemaPath: schemaPath), + schemaPath: schemaPath, + detail: detail), message, innerException); }