From 2d4527d06647b459ce33017055fc5617d9a75dd2 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Tue, 24 Feb 2026 13:19:55 +0800
Subject: [PATCH] =?UTF-8?q?feat(storage):=20=E6=B7=BB=E5=8A=A0=E7=9B=AE?=
=?UTF-8?q?=E5=BD=95=E6=93=8D=E4=BD=9C=E5=8A=9F=E8=83=BD=E5=92=8C=E5=AD=98?=
=?UTF-8?q?=E6=A1=A3=E4=BB=93=E5=BA=93=E7=B3=BB=E7=BB=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在IStorage接口中添加目录操作相关方法:ListDirectoriesAsync、
ListFilesAsync、DirectoryExistsAsync、CreateDirectoryAsync
- 为FileStorage和GodotFileStorage实现目录操作功能
- 添加ScopedStorage的目录操作委托实现
- 新增ISaveRepository接口定义基于槽位的存档系统
- 实现SaveRepository类提供完整的存档管理功能
- 添加SaveConfiguration类用于存档系统配置
---
.../storage/IStorage.cs | 28 ++
.../data/ISaveRepository.cs | 57 ++++
GFramework.Game/data/SaveConfiguration.cs | 35 +++
GFramework.Game/data/SaveRepository.cs | 145 +++++++++
.../data/SaveRepository使用指南.md | 292 ++++++++++++++++++
GFramework.Game/storage/FileStorage.cs | 65 ++++
GFramework.Game/storage/ScopedStorage.cs | 40 +++
GFramework.Godot/storage/GodotFileStorage.cs | 88 ++++++
8 files changed, 750 insertions(+)
create mode 100644 GFramework.Game.Abstractions/data/ISaveRepository.cs
create mode 100644 GFramework.Game/data/SaveConfiguration.cs
create mode 100644 GFramework.Game/data/SaveRepository.cs
create mode 100644 GFramework.Game/data/SaveRepository使用指南.md
diff --git a/GFramework.Core.Abstractions/storage/IStorage.cs b/GFramework.Core.Abstractions/storage/IStorage.cs
index 1d833e6..cc8ad40 100644
--- a/GFramework.Core.Abstractions/storage/IStorage.cs
+++ b/GFramework.Core.Abstractions/storage/IStorage.cs
@@ -75,4 +75,32 @@ public interface IStorage : IUtility
/// 要删除的键
/// 表示异步操作的Task
Task DeleteAsync(string key);
+
+ ///
+ /// 列举指定路径下的所有子目录名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 子目录名称列表
+ Task> ListDirectoriesAsync(string path = "");
+
+ ///
+ /// 列举指定路径下的所有文件名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 文件名称列表
+ Task> ListFilesAsync(string path = "");
+
+ ///
+ /// 检查指定路径的目录是否存在
+ ///
+ /// 要检查的目录路径
+ /// 如果目录存在则返回true,否则返回false
+ Task DirectoryExistsAsync(string path);
+
+ ///
+ /// 创建目录(递归创建父目录)
+ ///
+ /// 要创建的目录路径
+ /// 表示异步操作的Task
+ Task CreateDirectoryAsync(string path);
}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/data/ISaveRepository.cs b/GFramework.Game.Abstractions/data/ISaveRepository.cs
new file mode 100644
index 0000000..bcca839
--- /dev/null
+++ b/GFramework.Game.Abstractions/data/ISaveRepository.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using GFramework.Core.Abstractions.utility;
+
+namespace GFramework.Game.Abstractions.data;
+
+///
+/// 存档仓库接口,管理基于槽位的存档系统
+///
+/// 存档数据类型,必须实现IData接口并具有无参构造函数
+public interface ISaveRepository : IUtility
+ where TSaveData : class, IData, new()
+{
+ ///
+ /// 检查指定槽位是否存在存档
+ ///
+ /// 存档槽位编号
+ /// 如果存档存在返回true,否则返回false
+ Task ExistsAsync(int slot);
+
+ ///
+ /// 加载指定槽位的存档
+ ///
+ /// 存档槽位编号
+ /// 存档数据对象,如果不存在则返回新实例
+ Task LoadAsync(int slot);
+
+ ///
+ /// 保存存档到指定槽位
+ ///
+ /// 存档槽位编号
+ /// 要保存的存档数据
+ Task SaveAsync(int slot, TSaveData data);
+
+ ///
+ /// 删除指定槽位的存档
+ ///
+ /// 存档槽位编号
+ Task DeleteAsync(int slot);
+
+ ///
+ /// 列出所有有效的存档槽位
+ ///
+ /// 包含所有有效存档槽位编号的只读列表,按升序排列
+ Task> ListSlotsAsync();
+}
diff --git a/GFramework.Game/data/SaveConfiguration.cs b/GFramework.Game/data/SaveConfiguration.cs
new file mode 100644
index 0000000..b7bdd3b
--- /dev/null
+++ b/GFramework.Game/data/SaveConfiguration.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace GFramework.Game.data;
+
+///
+/// 存档系统配置
+///
+public sealed class SaveConfiguration
+{
+ ///
+ /// 存档根目录 (如 "user://saves")
+ ///
+ public string SaveRoot { get; init; } = "user://saves";
+
+ ///
+ /// 存档槽位前缀 (如 "slot_")
+ ///
+ public string SaveSlotPrefix { get; init; } = "slot_";
+
+ ///
+ /// 存档文件名 (如 "save.json")
+ ///
+ public string SaveFileName { get; init; } = "save.json";
+}
diff --git a/GFramework.Game/data/SaveRepository.cs b/GFramework.Game/data/SaveRepository.cs
new file mode 100644
index 0000000..cc30082
--- /dev/null
+++ b/GFramework.Game/data/SaveRepository.cs
@@ -0,0 +1,145 @@
+// Copyright (c) 2026 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Globalization;
+using GFramework.Core.Abstractions.storage;
+using GFramework.Core.utility;
+using GFramework.Game.Abstractions.data;
+using GFramework.Game.storage;
+
+namespace GFramework.Game.data;
+
+///
+/// 基于槽位的存档仓库实现
+///
+/// 存档数据类型
+public class SaveRepository : AbstractContextUtility, ISaveRepository
+ where TSaveData : class, IData, new()
+{
+ private readonly SaveConfiguration _config;
+ private readonly IStorage _rootStorage;
+
+ ///
+ /// 初始化存档仓库
+ ///
+ /// 存储实例
+ /// 存档配置
+ public SaveRepository(IStorage storage, SaveConfiguration config)
+ {
+ ArgumentNullException.ThrowIfNull(storage);
+ ArgumentNullException.ThrowIfNull(config);
+
+ _config = config;
+ _rootStorage = new ScopedStorage(storage, config.SaveRoot);
+ }
+
+ ///
+ /// 检查指定槽位是否存在存档
+ ///
+ /// 存档槽位编号
+ /// 如果存档存在返回true,否则返回false
+ public async Task ExistsAsync(int slot)
+ {
+ var storage = GetSlotStorage(slot);
+ return await storage.ExistsAsync(_config.SaveFileName);
+ }
+
+ ///
+ /// 加载指定槽位的存档
+ ///
+ /// 存档槽位编号
+ /// 存档数据对象,如果不存在则返回新实例
+ public async Task LoadAsync(int slot)
+ {
+ var storage = GetSlotStorage(slot);
+
+ if (await storage.ExistsAsync(_config.SaveFileName))
+ return await storage.ReadAsync(_config.SaveFileName);
+
+ return new TSaveData();
+ }
+
+ ///
+ /// 保存存档到指定槽位
+ ///
+ /// 存档槽位编号
+ /// 要保存的存档数据
+ public async Task SaveAsync(int slot, TSaveData data)
+ {
+ var slotPath = $"{_config.SaveSlotPrefix}{slot}";
+
+ // 确保槽位目录存在
+ if (!await _rootStorage.DirectoryExistsAsync(slotPath))
+ await _rootStorage.CreateDirectoryAsync(slotPath);
+
+ var storage = GetSlotStorage(slot);
+ await storage.WriteAsync(_config.SaveFileName, data);
+ }
+
+ ///
+ /// 删除指定槽位的存档
+ ///
+ /// 存档槽位编号
+ public async Task DeleteAsync(int slot)
+ {
+ var storage = GetSlotStorage(slot);
+ await storage.DeleteAsync(_config.SaveFileName);
+ }
+
+ ///
+ /// 列出所有有效的存档槽位
+ ///
+ /// 包含所有有效存档槽位编号的只读列表,按升序排列
+ public async Task> ListSlotsAsync()
+ {
+ // 列举所有槽位目录
+ var directories = await _rootStorage.ListDirectoriesAsync();
+
+ var slots = new List();
+
+ foreach (var dirName in directories)
+ {
+ // 检查目录名是否符合槽位前缀
+ if (!dirName.StartsWith(_config.SaveSlotPrefix, StringComparison.Ordinal))
+ continue;
+
+ // 提取槽位编号
+ var slotStr = dirName[_config.SaveSlotPrefix.Length..];
+ if (!int.TryParse(slotStr, CultureInfo.InvariantCulture, out var slot))
+ continue;
+
+ // 检查槽位中是否存在存档文件
+ if (await ExistsAsync(slot))
+ slots.Add(slot);
+ }
+
+ return slots.OrderBy(x => x).ToList();
+ }
+
+ ///
+ /// 获取指定槽位的存储对象
+ ///
+ /// 存档槽位编号
+ /// 对应槽位的存储对象
+ private IStorage GetSlotStorage(int slot)
+ {
+ return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}");
+ }
+
+ ///
+ /// 初始化逻辑
+ ///
+ protected override void OnInit()
+ {
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Game/data/SaveRepository使用指南.md b/GFramework.Game/data/SaveRepository使用指南.md
new file mode 100644
index 0000000..ac75fa6
--- /dev/null
+++ b/GFramework.Game/data/SaveRepository使用指南.md
@@ -0,0 +1,292 @@
+# SaveRepository 使用指南
+
+## 概述
+
+`SaveRepository` 是 GFramework 提供的通用存档仓库实现,基于 `IStorage` 抽象层,支持基于槽位的存档管理。
+
+## 核心特性
+
+- ✅ **完全解耦引擎依赖** - 框架层无 Godot 代码
+- ✅ **配置对象带默认值** - 无需 ProjectSettings 也能工作
+- ✅ **泛型设计** - 支持任意实现 `IData` 的存档类型
+- ✅ **自动槽位管理** - 利用 `ScopedStorage` 实现路径隔离
+- ✅ **异步操作** - 所有方法均为异步,避免阻塞主线程
+
+## 快速开始
+
+### 1. 定义存档数据类型
+
+```csharp
+using GFramework.Game.Abstractions.data;
+
+public class GameSaveData : IData
+{
+ public string PlayerName { get; set; } = "";
+ public int Level { get; set; } = 1;
+ public float PlayTime { get; set; } = 0f;
+ public DateTime SaveTime { get; set; } = DateTime.Now;
+}
+```
+
+### 2. 在启动类中注册
+
+```csharp
+// 在 GameApp.cs 或类似的启动类中
+protected override void OnRegisterUtility()
+{
+ // 1. 注册序列化器
+ var serializer = new JsonSerializer();
+ this.RegisterUtility(serializer);
+
+ // 2. 注册存储
+ this.RegisterUtility(new GodotFileStorage(serializer));
+
+ // 3. 创建存档配置
+ var saveConfig = new SaveConfiguration
+ {
+ SaveRoot = "user://saves", // 存档根目录
+ SaveSlotPrefix = "slot_", // 槽位前缀
+ SaveFileName = "save.json" // 存档文件名
+ };
+
+ // 4. 注册存档仓库
+ this.RegisterUtility>(
+ new SaveRepository(
+ this.GetUtility()!,
+ saveConfig
+ )
+ );
+}
+```
+
+### 3. 使用存档仓库
+
+```csharp
+// 获取存档仓库
+var saveRepo = this.GetUtility>();
+
+// 保存存档到槽位 1
+var saveData = new GameSaveData
+{
+ PlayerName = "玩家1",
+ Level = 10,
+ PlayTime = 3600f
+};
+await saveRepo.SaveAsync(slot: 1, saveData);
+
+// 加载槽位 1 的存档
+var loadedData = await saveRepo.LoadAsync(slot: 1);
+GD.Print($"玩家: {loadedData.PlayerName}, 等级: {loadedData.Level}");
+
+// 检查槽位是否存在
+if (await saveRepo.ExistsAsync(slot: 1))
+{
+ GD.Print("槽位 1 存在存档");
+}
+
+// 列出所有有效槽位
+var slots = await saveRepo.ListSlotsAsync();
+GD.Print($"找到 {slots.Count} 个存档槽位");
+
+// 删除槽位 1 的存档
+await saveRepo.DeleteAsync(slot: 1);
+```
+
+## 高级用法
+
+### 从 ProjectSettings 读取配置
+
+```csharp
+var saveConfig = new SaveConfiguration
+{
+ SaveRoot = ProjectSettings.HasSetting("application/config/save/save_path")
+ ? ProjectSettings.GetSetting("application/config/save/save_path").AsString()
+ : "user://saves",
+ SaveSlotPrefix = ProjectSettings.HasSetting("application/config/save/save_slot_prefix")
+ ? ProjectSettings.GetSetting("application/config/save/save_slot_prefix").AsString()
+ : "slot_",
+ SaveFileName = ProjectSettings.HasSetting("application/config/save/save_file_name")
+ ? ProjectSettings.GetSetting("application/config/save/save_file_name").AsString()
+ : "save.json"
+};
+```
+
+### 支持版本迁移
+
+```csharp
+using GFramework.Game.Abstractions.data;
+
+public class GameSaveData : IData, IVersionedData
+{
+ public int Version { get; set; } = 1;
+ public string PlayerName { get; set; } = "";
+ public int Level { get; set; } = 1;
+
+ // 版本 2 新增字段
+ public int Experience { get; set; } = 0;
+}
+
+// 实现迁移逻辑(未来可扩展)
+public class GameSaveDataMigration_1_to_2 : IDataMigration
+{
+ public int FromVersion => 1;
+ public int ToVersion => 2;
+
+ public GameSaveData Migrate(GameSaveData oldData)
+ {
+ // 迁移逻辑:为旧存档添加默认经验值
+ oldData.Experience = oldData.Level * 100;
+ oldData.Version = 2;
+ return oldData;
+ }
+}
+```
+
+### 多种存档类型
+
+```csharp
+// 游戏存档
+this.RegisterUtility>(
+ new SaveRepository(storage, new SaveConfiguration
+ {
+ SaveRoot = "user://saves/game",
+ SaveSlotPrefix = "slot_",
+ SaveFileName = "save.json"
+ })
+);
+
+// 用户配置存档
+this.RegisterUtility>(
+ new SaveRepository(storage, new SaveConfiguration
+ {
+ SaveRoot = "user://saves/profile",
+ SaveSlotPrefix = "profile_",
+ SaveFileName = "profile.json"
+ })
+);
+
+// 成就数据存档
+this.RegisterUtility>(
+ new SaveRepository(storage, new SaveConfiguration
+ {
+ SaveRoot = "user://saves/achievements",
+ SaveSlotPrefix = "achievement_",
+ SaveFileName = "data.json"
+ })
+);
+```
+
+## 文件结构
+
+使用默认配置时,存档文件结构如下:
+
+```
+user://saves/
+├── slot_1/
+│ └── save.json
+├── slot_2/
+│ └── save.json
+└── slot_3/
+ └── save.json
+```
+
+## API 参考
+
+### ISaveRepository
+
+| 方法 | 说明 | 返回值 |
+|------|------|--------|
+| `ExistsAsync(int slot)` | 检查指定槽位是否存在存档 | `Task` |
+| `LoadAsync(int slot)` | 加载指定槽位的存档 | `Task` |
+| `SaveAsync(int slot, TSaveData data)` | 保存存档到指定槽位 | `Task` |
+| `DeleteAsync(int slot)` | 删除指定槽位的存档 | `Task` |
+| `ListSlotsAsync()` | 列出所有有效的存档槽位 | `Task>` |
+
+### SaveConfiguration
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `SaveRoot` | `string` | `"user://saves"` | 存档根目录 |
+| `SaveSlotPrefix` | `string` | `"slot_"` | 槽位前缀 |
+| `SaveFileName` | `string` | `"save.json"` | 存档文件名 |
+
+## 从旧版 SaveStorageUtility 迁移
+
+### 主要变化
+
+1. **所有方法改为异步** - 使用 `async/await`
+2. **接口名称变更** - `ISaveStorageUtility` → `ISaveRepository`
+3. **方法名称添加 Async 后缀**
+4. **配置通过对象传递** - 支持默认值
+
+### 迁移对照表
+
+| 旧方法 | 新方法 |
+|--------|--------|
+| `Exists(int slot)` | `await ExistsAsync(int slot)` |
+| `Load(int slot)` | `await LoadAsync(int slot)` |
+| `Save(int slot, data)` | `await SaveAsync(int slot, data)` |
+| `Delete(int slot)` | `await DeleteAsync(int slot)` |
+| `ListSlots()` | `await ListSlotsAsync()` |
+
+### 迁移示例
+
+**旧代码:**
+```csharp
+var saveUtil = this.GetUtility();
+saveUtil.Save(1, data);
+var loadedData = saveUtil.Load(1);
+```
+
+**新代码:**
+```csharp
+var saveRepo = this.GetUtility>();
+await saveRepo.SaveAsync(1, data);
+var loadedData = await saveRepo.LoadAsync(1);
+```
+
+## 常见问题
+
+### Q: 如何切换到云存档?
+
+A: 只需替换 `IStorage` 实现即可:
+
+```csharp
+// 本地存档
+this.RegisterUtility(new GodotFileStorage(serializer));
+
+// 云存档(需要自行实现 CloudStorage)
+this.RegisterUtility(new CloudStorage(apiKey, serializer));
+```
+
+### Q: 如何加密存档?
+
+A: 使用装饰器模式包装 `IStorage`:
+
+```csharp
+var baseStorage = new GodotFileStorage(serializer);
+var encryptedStorage = new EncryptedStorage(baseStorage, encryptionKey);
+this.RegisterUtility(encryptedStorage);
+```
+
+### Q: 如何实现自动备份?
+
+A: 使用 `DataRepository` 的 `AutoBackup` 选项(需要配合 `DataRepository` 使用)。
+
+### Q: LoadAsync 返回的是新实例还是缓存实例?
+
+A: 每次调用都会从存储读取并返回新实例,不会缓存。如果需要缓存,请在业务层实现。
+
+## 最佳实践
+
+1. **使用异步方法** - 避免阻塞主线程
+2. **错误处理** - 使用 try-catch 捕获 IO 异常
+3. **定期保存** - 不要等到游戏退出才保存
+4. **槽位验证** - 加载前先检查槽位是否存在
+5. **版本管理** - 为存档数据实现 `IVersionedData` 接口
+
+## 相关文档
+
+- [IStorage 接口文档](./IStorage.md)
+- [ScopedStorage 使用指南](./ScopedStorage.md)
+- [数据仓库模式](./DataRepository.md)
diff --git a/GFramework.Game/storage/FileStorage.cs b/GFramework.Game/storage/FileStorage.cs
index 5b983fb..45dcc46 100644
--- a/GFramework.Game/storage/FileStorage.cs
+++ b/GFramework.Game/storage/FileStorage.cs
@@ -228,6 +228,71 @@ public sealed class FileStorage : IFileStorage
#endregion
+ #region Directory Operations
+
+ ///
+ /// 列举指定路径下的所有子目录名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 子目录名称列表
+ public Task> ListDirectoriesAsync(string path = "")
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
+ if (!Directory.Exists(fullPath))
+ return Task.FromResult>(Array.Empty());
+
+ var dirs = Directory.GetDirectories(fullPath)
+ .Select(Path.GetFileName)
+ .Where(name => !string.IsNullOrEmpty(name))
+ .ToList();
+
+ return Task.FromResult>(dirs);
+ }
+
+ ///
+ /// 列举指定路径下的所有文件名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 文件名称列表
+ public Task> ListFilesAsync(string path = "")
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
+ if (!Directory.Exists(fullPath))
+ return Task.FromResult>(Array.Empty());
+
+ var files = Directory.GetFiles(fullPath)
+ .Select(Path.GetFileName)
+ .Where(name => !string.IsNullOrEmpty(name))
+ .ToList();
+
+ return Task.FromResult>(files);
+ }
+
+ ///
+ /// 检查指定路径的目录是否存在
+ ///
+ /// 要检查的目录路径
+ /// 如果目录存在则返回true,否则返回false
+ public Task DirectoryExistsAsync(string path)
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
+ return Task.FromResult(Directory.Exists(fullPath));
+ }
+
+ ///
+ /// 创建目录(递归创建父目录)
+ ///
+ /// 要创建的目录路径
+ /// 表示异步操作的Task
+ public Task CreateDirectoryAsync(string path)
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
+ Directory.CreateDirectory(fullPath);
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
#region Write
///
diff --git a/GFramework.Game/storage/ScopedStorage.cs b/GFramework.Game/storage/ScopedStorage.cs
index 0a82ca7..0ce4785 100644
--- a/GFramework.Game/storage/ScopedStorage.cs
+++ b/GFramework.Game/storage/ScopedStorage.cs
@@ -105,6 +105,46 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
await inner.DeleteAsync(Key(key));
}
+ ///
+ /// 列举指定路径下的所有子目录名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 子目录名称列表
+ public Task> ListDirectoriesAsync(string path = "")
+ {
+ return inner.ListDirectoriesAsync(Key(path));
+ }
+
+ ///
+ /// 列举指定路径下的所有文件名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 文件名称列表
+ public Task> ListFilesAsync(string path = "")
+ {
+ return inner.ListFilesAsync(Key(path));
+ }
+
+ ///
+ /// 检查指定路径的目录是否存在
+ ///
+ /// 要检查的目录路径
+ /// 如果目录存在则返回true,否则返回false
+ public Task DirectoryExistsAsync(string path)
+ {
+ return inner.DirectoryExistsAsync(Key(path));
+ }
+
+ ///
+ /// 创建目录(递归创建父目录)
+ ///
+ /// 要创建的目录路径
+ /// 表示异步操作的Task
+ public Task CreateDirectoryAsync(string path)
+ {
+ return inner.CreateDirectoryAsync(Key(path));
+ }
+
///
/// 为给定的键添加前缀
///
diff --git a/GFramework.Godot/storage/GodotFileStorage.cs b/GFramework.Godot/storage/GodotFileStorage.cs
index 5a27bf5..eebf571 100644
--- a/GFramework.Godot/storage/GodotFileStorage.cs
+++ b/GFramework.Godot/storage/GodotFileStorage.cs
@@ -263,6 +263,94 @@ public sealed class GodotFileStorage : IStorage
#endregion
+ #region Directory Operations
+
+ ///
+ /// 列举指定路径下的所有子目录名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 子目录名称列表
+ public async Task> ListDirectoriesAsync(string path = "")
+ {
+ return await Task.Run(() =>
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
+ var dir = DirAccess.Open(fullPath);
+ if (dir == null) return Array.Empty();
+
+ dir.ListDirBegin();
+ var result = new List();
+
+ while (true)
+ {
+ var name = dir.GetNext();
+ if (string.IsNullOrEmpty(name)) break;
+ if (dir.CurrentIsDir() && !name.StartsWith(".", StringComparison.Ordinal))
+ result.Add(name);
+ }
+
+ dir.ListDirEnd();
+ return (IReadOnlyList)result;
+ });
+ }
+
+ ///
+ /// 列举指定路径下的所有文件名称
+ ///
+ /// 要列举的路径,空字符串表示根目录
+ /// 文件名称列表
+ public async Task> ListFilesAsync(string path = "")
+ {
+ return await Task.Run(() =>
+ {
+ var fullPath = string.IsNullOrEmpty(path) ? "user://" : ToAbsolutePath(path);
+ var dir = DirAccess.Open(fullPath);
+ if (dir == null) return Array.Empty();
+
+ dir.ListDirBegin();
+ var result = new List();
+
+ while (true)
+ {
+ var name = dir.GetNext();
+ if (string.IsNullOrEmpty(name)) break;
+ if (!dir.CurrentIsDir())
+ result.Add(name);
+ }
+
+ dir.ListDirEnd();
+ return (IReadOnlyList)result;
+ });
+ }
+
+ ///
+ /// 检查指定路径的目录是否存在
+ ///
+ /// 要检查的目录路径
+ /// 如果目录存在则返回true,否则返回false
+ public Task DirectoryExistsAsync(string path)
+ {
+ var fullPath = ToAbsolutePath(path);
+ return Task.FromResult(DirAccess.DirExistsAbsolute(fullPath));
+ }
+
+ ///
+ /// 创建目录(递归创建父目录)
+ ///
+ /// 要创建的目录路径
+ /// 表示异步操作的Task
+ public async Task CreateDirectoryAsync(string path)
+ {
+ await Task.Run(() =>
+ {
+ var fullPath = ToAbsolutePath(path);
+ if (!DirAccess.DirExistsAbsolute(fullPath))
+ DirAccess.MakeDirRecursiveAbsolute(fullPath);
+ });
+ }
+
+ #endregion
+
#region Write
///