using System.IO; using FileAccess = Godot.FileAccess; namespace GFramework.Godot.Config; /// /// 抽象 与具体宿主环境之间的 Godot 路径和文件访问边界。 /// /// /// 该抽象存在的原因,是编辑器态与导出态对 res://user:// 的访问方式不同: /// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源, /// 再把它们复制到运行时缓存目录。 在目录不存在或当前环境无法枚举时必须返回 /// ,用来表达“不可访问”而不是抛出未找到异常; 则应保留底层读取失败异常, /// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 / 语义; /// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。 /// internal sealed class GodotYamlConfigEnvironment { /// /// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。 /// /// 返回当前进程是否处于 Godot 编辑器态的委托。 /// /// 把 Godot 特殊路径转换为普通绝对路径的委托。 /// 当前加载器仅会在输入为 res://user:// 时调用它,返回值必须为非空绝对路径。 /// /// /// 枚举指定目录直接子项的委托。 /// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 。 /// /// /// 检查指定路径上的文件是否存在的委托。 /// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。 /// /// /// 读取指定文件完整字节内容的委托。 /// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。 /// /// 任一委托参数为 时抛出。 public GodotYamlConfigEnvironment( Func isEditor, Func globalizePath, Func?> enumerateDirectory, Func fileExists, Func readAllBytes) { IsEditor = isEditor ?? throw new ArgumentNullException(nameof(isEditor)); GlobalizePath = globalizePath ?? throw new ArgumentNullException(nameof(globalizePath)); EnumerateDirectory = enumerateDirectory ?? throw new ArgumentNullException(nameof(enumerateDirectory)); FileExists = fileExists ?? throw new ArgumentNullException(nameof(fileExists)); ReadAllBytes = readAllBytes ?? throw new ArgumentNullException(nameof(readAllBytes)); } /// /// 获取默认的 Godot 运行时环境实现。 /// /// /// 默认实现使用 检测编辑器态, /// 使用 处理 Godot 特殊路径, /// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。 /// public static GodotYamlConfigEnvironment Default { get; } = new( static () => OS.HasFeature("editor"), static path => ProjectSettings.GlobalizePath(path), EnumerateDirectoryCore, FileExistsCore, ReadAllBytesCore); /// /// 获取用于判断当前进程是否处于编辑器态的委托。 /// public Func IsEditor { get; } /// /// 获取把 Godot 特殊路径转换为普通绝对路径的委托。 /// /// /// 当前加载器只会对 res://user:// 路径调用该委托。 /// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。 /// public Func GlobalizePath { get; } /// /// 获取用于枚举目录直接子项的委托。 /// /// /// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 。 /// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。 /// public Func?> EnumerateDirectory { get; } /// /// 获取用于检查文件是否存在的委托。 /// public Func FileExists { get; } /// /// 获取用于读取文件完整字节内容的委托。 /// /// /// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。 /// public Func ReadAllBytes { get; } private static IReadOnlyList? EnumerateDirectoryCore(string path) { if (!path.IsGodotPath()) { try { if (!Directory.Exists(path)) { return null; } return Directory .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) .Select(static entryPath => new GodotYamlConfigDirectoryEntry( Path.GetFileName(entryPath), Directory.Exists(entryPath))) .ToArray(); } catch (IOException) { // 非 Godot 路径分支与公开契约保持一致:宿主无法访问目录时返回 null,而不是泄漏底层异常。 return null; } catch (UnauthorizedAccessException) { return null; } catch (ArgumentException) { return null; } catch (NotSupportedException) { return null; } } using var directory = DirAccess.Open(path); if (directory == null) { return null; } var entries = new List(); var listDirectoryError = directory.ListDirBegin(); if (listDirectoryError != Error.Ok) { return null; } try { while (true) { var name = directory.GetNext(); if (string.IsNullOrEmpty(name)) { break; } entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); } } finally { // 目录枚举句柄必须成对结束,避免未来循环体扩展后在异常路径上遗留引擎状态。 directory.ListDirEnd(); } return entries; } private static bool FileExistsCore(string path) { return path.IsGodotPath() ? FileAccess.FileExists(path) : File.Exists(path); } private static byte[] ReadAllBytesCore(string path) { if (!path.IsGodotPath()) { return File.ReadAllBytes(path); } var bytes = FileAccess.GetFileAsBytes(path); var error = FileAccess.GetOpenError(); if (error == Error.Ok) { return bytes; } throw CreateReadException(path, error); } private static Exception CreateReadException(string path, Error error) { return error switch { Error.FileNotFound => new FileNotFoundException($"Godot file not found: {path}", path), Error.FileCantOpen => new IOException($"Godot could not open file '{path}'. Error: {error}"), _ => new IOException($"Godot failed to read file '{path}'. Error: {error}") }; } }