From 1bf5d287e9ae1b35075a866373ea9116cc57d174 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sat, 11 Apr 2026 07:28:49 +0800
Subject: [PATCH] =?UTF-8?q?fix(config):=20=E4=BF=AE=E5=A4=8DGodot=20YAML?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD=E5=99=A8=E7=9A=84=E7=9B=AE?=
=?UTF-8?q?=E5=BD=95=E9=87=8D=E7=BD=AE=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为构造函数添加ArgumentNullException和ArgumentException异常说明
- 为EnableHotReload方法添加InvalidOperationException异常说明
- 重构ResetDirectory方法以捕获目录操作异常并包装为ConfigLoadException
- 添加detail参数到CreateConfigLoadException方法用于提供更详细的错误信息
- 新增单元测试验证运行时缓存目录重置失败时的异常处理
- 添加GodotYamlConfigTableSourceTests测试类验证安全相对路径约束
---
.../Config/GodotYamlConfigLoaderTests.cs | 26 ++++++++
.../Config/GodotYamlConfigTableSourceTests.cs | 10 ++++
.../Config/GodotYamlConfigLoader.cs | 59 ++++++++++++++++---
3 files changed, 86 insertions(+), 9 deletions(-)
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);
}