From 6a12bb5e2e0e46a186adaba867f91848c8a91710 Mon Sep 17 00:00:00 2001 From: ChestnutYueyue <952134128@qq.com> Date: Fri, 20 Feb 2026 22:15:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=B3=BB=E7=BB=9F=E5=8F=8A=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增资源服务系统核心功能,包括资源加载、缓存管理和异步处理机制。添加测试框架支持单元测试,包含以下主要变更: - 实现 AssetService 核心功能,支持资源同步/异步加载 - 添加 AssetCache 实现 LRU 缓存策略 - 引入测试框架,包含测试用例注册和断言机制 - 实现资源打包工具 asset_packer - 添加压缩库支持 (Zstd/LZ4/Zlib) - 完善文档说明资源服务系统设计 测试用例覆盖资源缓存、数据处理器、资源包管理等核心功能。文档详细说明系统架构和使用方法。 --- Docs/asset_service.md | 528 ++++++++++++++++++ .../include/extra2d/services/asset_service.h | 4 +- Extra2D/src/services/asset_service.cpp | 17 +- Tests/test_asset_cache.cpp | 307 ++++++++++ Tests/test_asset_pack.cpp | 427 ++++++++++++++ Tests/test_asset_service.cpp | 500 +++++++++++++++++ Tests/test_data_processor.cpp | 260 +++++++++ Tests/test_framework.cpp | 65 +++ Tests/test_framework.h | 140 +++++ Tests/test_main.cpp | 15 + Tools/asset_packer.cpp | 460 +++++++++++++++ xmake.lua | 97 ++++ xmake/engine.lua | 9 +- 13 files changed, 2823 insertions(+), 6 deletions(-) create mode 100644 Docs/asset_service.md create mode 100644 Tests/test_asset_cache.cpp create mode 100644 Tests/test_asset_pack.cpp create mode 100644 Tests/test_asset_service.cpp create mode 100644 Tests/test_data_processor.cpp create mode 100644 Tests/test_framework.cpp create mode 100644 Tests/test_framework.h create mode 100644 Tests/test_main.cpp create mode 100644 Tools/asset_packer.cpp diff --git a/Docs/asset_service.md b/Docs/asset_service.md new file mode 100644 index 0000000..92ece0f --- /dev/null +++ b/Docs/asset_service.md @@ -0,0 +1,528 @@ +# Extra2D 资源服务系统 + +## 概述 + +Extra2D 资源服务系统提供了一套完整的资源管理解决方案,包括资源加载、缓存、打包和加密压缩功能。 + +### 主要特性 + +- **类型安全**:使用模板和强类型 ID 避免类型错误 +- **自动缓存**:LRU 缓存策略,自动管理内存 +- **异步加载**:后台线程加载,不阻塞主线程 +- **资源打包**:支持压缩和加密的资源包格式 +- **多格式支持**:纹理、字体、着色器、音频等 + +## 模块结构 + +``` +extra2d/asset/ +├── asset_types.h # 资源类型定义 +├── asset.h # 资源基类 +├── asset_handle.h # 资源句柄 +├── asset_cache.h # 资源缓存 +├── asset_loader.h # 资源加载器 +├── asset_pack.h # 资源包 +├── data_processor.h # 数据处理器 +└── asset_service.h # 资源服务 +``` + +## 快速开始 + +### 1. 初始化资源服务 + +```cpp +#include + +using namespace extra2d; + +// 创建资源服务 +AssetService service; +service.init(); +service.setRoot("assets"); + +// 注册加载器 +service.registerLoader(AssetLoaderFactory::createTextureLoader()); +service.registerLoader(AssetLoaderFactory::createFontLoader()); +service.registerLoader(AssetLoaderFactory::createShaderLoader()); +``` + +### 2. 同步加载资源 + +```cpp +// 加载纹理 +AssetHandle texture = service.load("sprites/player.png"); +if (texture.valid()) { + int width = texture->width(); + int height = texture->height(); + const u8* data = texture->data(); +} +``` + +### 3. 异步加载资源 + +```cpp +// 异步加载 +service.loadAsync("sprites/background.png", + [](AssetHandle handle) { + if (handle.valid()) { + // 资源加载完成,在主线程处理 + } + }); + +// 在主线程处理回调 +service.process(); +``` + +### 4. 使用资源包 + +```cpp +// 挂载资源包 +service.mount("data.pak"); + +// 从资源包加载 +auto texture = service.load("textures/hero.png"); + +// 卸载资源包 +service.unmount("data.pak"); +``` + +### 5. 缓存管理 + +```cpp +// 设置缓存上限 (100MB) +service.setLimit(100 * 1024 * 1024); + +// 获取缓存统计 +CacheStats stats = service.stats(); +std::cout << "缓存使用: " << stats.bytes << " 字节\n"; +std::cout << "命中率: " << (stats.hitRate() * 100) << "%\n"; + +// 清理无引用资源 +service.purge(); + +// 清空缓存 +service.clear(); +``` + +## API 参考 + +### AssetID + +资源标识符,使用哈希值进行快速比较。 + +```cpp +struct AssetID { + u64 hash; // 哈希值 + std::string path; // 原始路径 + + explicit AssetID(const std::string& path); + bool valid() const; + bool operator==(const AssetID& other) const; +}; +``` + +### AssetHandle + +类型安全的资源句柄,使用弱引用避免阻止资源回收。 + +```cpp +template +class AssetHandle { +public: + bool valid() const; // 检查是否有效 + Ref get() const; // 获取强引用 + T* operator->() const; // 解引用 + bool loaded() const; // 检查是否已加载 + AssetState state() const; // 获取状态 +}; +``` + +### AssetCache + +LRU 缓存管理器。 + +```cpp +class AssetCache { +public: + explicit AssetCache(size_t limit = 0); + + template + AssetHandle add(Ref asset); + + template + AssetHandle get(const AssetID& id); + + bool has(const AssetID& id) const; + bool remove(const AssetID& id); + + void setLimit(size_t limit); + size_t bytes() const; + size_t count() const; + + size_t purge(); // 清理无引用资源 + void clear(); // 清空缓存 + CacheStats stats() const; +}; +``` + +### IAssetService + +资源服务接口。 + +```cpp +class IAssetService : public IService { +public: + // 同步加载 + template + AssetHandle load(const std::string& path); + + // 异步加载 + template + void loadAsync(const std::string& path, AssetLoadCallback callback); + + // 获取已缓存资源 + template + AssetHandle get(const std::string& path); + + // 预加载 + template + void preload(const std::string& path); + + // 状态查询 + bool isLoaded(const std::string& path) const; + bool isLoading(const std::string& path) const; + + // 资源管理 + void unload(const std::string& path); + void setLimit(size_t maxBytes); + size_t size() const; + void purge(); + void clear(); + CacheStats stats() const; + + // 加载器注册 + template + void registerLoader(Unique> loader); + + // 资源包管理 + bool mount(const std::string& path); + void unmount(const std::string& path); + + // 数据处理管道 + void setPipe(DataPipe pipe); + + // 根目录 + void setRoot(const std::string& path); + std::string root() const; + + // 处理异步回调 + void process(); +}; +``` + +### DataPipe + +数据处理管道,支持链式调用。 + +```cpp +class DataPipe { +public: + DataPipe& decrypt(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR); + DataPipe& decompress(Compression algo); + DataPipe& encrypt(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR); + DataPipe& compress(Compression algo, int level = 3); + DataPipe& add(Unique processor); + + std::vector process(const std::vector& input); + void clear(); + bool empty() const; + size_t size() const; +}; +``` + +### AssetPackBuilder + +资源包构建器。 + +```cpp +class AssetPackBuilder { +public: + explicit AssetPackBuilder(Compression compression = Compression::None, int level = 3); + + void add(const std::string& path, const std::vector& data); + void add(const std::string& path, std::vector&& data); + bool addFile(const std::string& filePath, const std::string& packPath = ""); + size_t addDirectory(const std::string& dirPath, const std::string& prefix = ""); + + void setEncryption(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR); + bool build(const std::string& outputPath); + + void clear(); + size_t count() const; + size_t totalOriginalSize() const; + size_t totalCompressedSize() const; +}; +``` + +## 资源类型 + +### TextureAsset + +纹理资源,使用 stb_image 加载。 + +```cpp +class TextureAsset : public Asset { +public: + AssetType type() const override { return AssetType::Texture; } + + int width() const; + int height() const; + int channels() const; + const u8* data() const; + size_t dataSize() const; +}; +``` + +### FontAsset + +字体资源,支持 TrueType 字体。 + +```cpp +class FontAsset : public Asset { +public: + AssetType type() const override { return AssetType::Font; } + + float scaleForPixelHeight(float pixels) const; + const u8* data() const; + size_t dataSize() const; +}; +``` + +### ShaderAsset + +着色器资源。 + +```cpp +class ShaderAsset : public Asset { +public: + AssetType type() const override { return AssetType::Shader; } + + const std::string& vertexSource() const; + const std::string& fragmentSource() const; +}; +``` + +### AudioAsset + +音频资源。 + +```cpp +class AudioAsset : public Asset { +public: + AssetType type() const override { return AssetType::Audio; } + + AudioFormat format() const; + int channels() const; + int sampleRate() const; + int bitsPerSample() const; + float duration() const; + const u8* data() const; + size_t dataSize() const; + bool streaming() const; +}; +``` + +### DataAsset + +通用二进制数据。 + +```cpp +class DataAsset : public Asset { +public: + AssetType type() const override { return AssetType::Data; } + + const u8* data() const; + size_t size() const; +}; +``` + +## 资源打包工具 + +### 安装 + +```bash +xmake build asset_packer +``` + +### 用法 + +``` +Extra2D 资源打包工具 v1.0.0 + +用法: + asset_packer create [options] + asset_packer list + asset_packer extract + +命令: + create 创建资源包 + list 列出资源包内容 + extract 提取资源包内容 + +选项: + -c, --compression 压缩算法 (none, zstd, lz4, zlib),默认 zstd + -l, --level 压缩级别 (1-22),默认 3 + -e, --encrypt 加密密钥 + -t, --encrypt-type 加密类型 (xor, aes),默认 xor + -v, --verbose 详细输出 + -h, --help 显示帮助 +``` + +### 示例 + +```bash +# 创建资源包(使用 Zstd 压缩) +asset_packer create game.pak -c zstd -v assets/ + +# 创建加密资源包 +asset_packer create game.pak -c zstd -e "secret_key" -t xor -v assets/ + +# 列出资源包内容 +asset_packer list game.pak + +# 提取资源包 +asset_packer extract game.pak extracted/ + +# 使用不同压缩算法 +asset_packer create game.pak -c lz4 -v assets/ +asset_packer create game.pak -c zlib -l 9 -v assets/ +``` + +## 压缩算法对比 + +| 算法 | 压缩率 | 压缩速度 | 解压速度 | 适用场景 | +|------|--------|----------|----------|----------| +| Zstd | 高 | 快 | 很快 | 通用推荐 | +| LZ4 | 中 | 很快 | 很快 | 实时解压 | +| Zlib | 高 | 中 | 中 | 兼容性好 | +| None | - | - | - | 已压缩资源 | + +## 设计模式 + +### 策略模式 (AssetLoader) + +不同资源类型使用不同的加载策略。 + +```cpp +class AssetLoader { +public: + virtual Ref load(const std::string& path) = 0; + virtual Ref loadFromMemory(const u8* data, size_t size) = 0; + virtual bool canLoad(const std::string& path) const = 0; + virtual AssetType type() const = 0; + virtual std::vector extensions() const = 0; +}; +``` + +### 享元模式 (AssetCache) + +共享资源实例,减少内存占用。 + +### 装饰器模式 (DataProcessor) + +链式处理数据流。 + +```cpp +DataPipe pipe; +pipe.encrypt("key") + .compress(Compression::Zstd); +``` + +### 服务定位器模式 + +全局访问资源服务。 + +```cpp +auto* service = ServiceLocator::get(); +``` + +## 线程安全 + +- `AssetCache` 使用读写锁 (`std::shared_mutex`) +- `AssetService` 使用读写锁保护资源映射 +- 异步加载使用独立的工作线程 +- 回调在主线程执行(需要调用 `process()`) + +## 最佳实践 + +### 1. 资源路径约定 + +``` +assets/ +├── textures/ +│ ├── sprites/ +│ └── backgrounds/ +├── fonts/ +├── shaders/ +└── audio/ +``` + +### 2. 预加载关键资源 + +```cpp +// 游戏启动时预加载 +service.preload("textures/loading.png"); +service.preload("fonts/main.ttf"); +``` + +### 3. 合理设置缓存上限 + +```cpp +// 根据目标平台设置 +#ifdef MOBILE + service.setLimit(50 * 1024 * 1024); // 50MB +#else + service.setLimit(200 * 1024 * 1024); // 200MB +#endif +``` + +### 4. 定期清理缓存 + +```cpp +// 场景切换时清理 +void onSceneChange() { + service.purge(); +} +``` + +### 5. 使用资源包减少文件数量 + +```cpp +// 将小文件打包成资源包 +// 减少文件 I/O 操作,提高加载速度 +service.mount("textures.pak"); +service.mount("audio.pak"); +``` + +## 错误处理 + +```cpp +auto handle = service.load("missing.png"); +if (!handle.valid()) { + // 资源加载失败 + std::cerr << "Failed to load texture\n"; +} + +// 检查加载状态 +if (handle.state() == AssetState::Failed) { + // 处理失败情况 +} +``` + +## 版本历史 + +- **v1.0.0** - 初始版本 + - 资源加载和缓存 + - 资源打包和加密 + - 异步加载支持 + - 多种压缩算法 diff --git a/Extra2D/include/extra2d/services/asset_service.h b/Extra2D/include/extra2d/services/asset_service.h index 6436477..e4e4982 100644 --- a/Extra2D/include/extra2d/services/asset_service.h +++ b/Extra2D/include/extra2d/services/asset_service.h @@ -47,7 +47,7 @@ public: */ template AssetHandle load(const std::string &path) { static_assert(std::is_base_of_v, "T must derive from Asset"); - return loadImpl(AssetID(path), typeid(T)); + return AssetHandle(loadImpl(AssetID(path), typeid(T))); } /** @@ -73,7 +73,7 @@ public: */ template AssetHandle get(const std::string &path) { static_assert(std::is_base_of_v, "T must derive from Asset"); - return getImpl(AssetID(path), typeid(T)); + return AssetHandle(getImpl(AssetID(path), typeid(T))); } /** diff --git a/Extra2D/src/services/asset_service.cpp b/Extra2D/src/services/asset_service.cpp index 8f726b1..884e899 100644 --- a/Extra2D/src/services/asset_service.cpp +++ b/Extra2D/src/services/asset_service.cpp @@ -77,7 +77,22 @@ void AssetService::setLimit(size_t maxBytes) { cache_->setLimit(maxBytes); } size_t AssetService::size() const { return cache_->size(); } -void AssetService::purge() { cache_->purge(); } +void AssetService::purge() { + std::unique_lock lock(mutex_); + + std::vector toRemove; + for (const auto& [id, loaded] : assets_) { + if (loaded.asset.use_count() <= 2) { + toRemove.push_back(id); + } + } + for (const auto& id : toRemove) { + assets_.erase(id); + states_.erase(id); + } + + cache_->purge(); +} void AssetService::clear() { std::unique_lock lock(mutex_); diff --git a/Tests/test_asset_cache.cpp b/Tests/test_asset_cache.cpp new file mode 100644 index 0000000..2e6eb77 --- /dev/null +++ b/Tests/test_asset_cache.cpp @@ -0,0 +1,307 @@ +/** + * @file test_asset_cache.cpp + * @brief AssetCache 单元测试 + * + * 测试资源缓存的添加、获取、淘汰等功能。 + */ + +#include "test_framework.h" +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +// --------------------------------------------------------------------------- +// 测试辅助类 +// --------------------------------------------------------------------------- + +/** + * @brief 测试用资源类 + */ +class TestAsset : public Asset { +public: + AssetType type() const override { return AssetType::Data; } + + bool loaded() const override { + return state_.load(std::memory_order_acquire) == AssetState::Loaded; + } + + size_t memSize() const override { return dataSize_; } + + void setData(size_t size) { + dataSize_ = size; + setState(AssetState::Loaded); + } + + static Ref create(const std::string& path, size_t size = 100) { + auto asset = std::make_shared(); + asset->setId(AssetID(path)); + asset->setPath(path); + asset->setData(size); + return asset; + } + +private: + size_t dataSize_ = 0; +}; + +// --------------------------------------------------------------------------- +// 基本功能测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, Create) { + AssetCache cache; + + TEST_ASSERT_EQ(0u, cache.count()); + TEST_ASSERT_EQ(0u, cache.bytes()); + TEST_ASSERT_EQ(0u, cache.limit()); +} + +TEST(AssetCache, CreateWithLimit) { + AssetCache cache(1024 * 1024); + + TEST_ASSERT_EQ(1024 * 1024, cache.limit()); +} + +TEST(AssetCache, AddAsset) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + auto handle = cache.add(asset); + + TEST_ASSERT_TRUE(handle.valid()); + TEST_ASSERT_EQ(1u, cache.count()); + TEST_ASSERT_EQ(100u, cache.bytes()); +} + +TEST(AssetCache, AddNullAsset) { + AssetCache cache; + + Ref nullAsset; + auto handle = cache.add(nullAsset); + + TEST_ASSERT_FALSE(handle.valid()); + TEST_ASSERT_EQ(0u, cache.count()); +} + +TEST(AssetCache, GetAsset) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 200); + cache.add(asset); + + auto handle = cache.get(AssetID("test.asset")); + + TEST_ASSERT_TRUE(handle.valid()); + TEST_ASSERT_TRUE(handle.get() != nullptr); + TEST_ASSERT_EQ(asset.get(), handle.get().get()); +} + +TEST(AssetCache, GetNonExistent) { + AssetCache cache; + + auto handle = cache.get(AssetID("nonexistent.asset")); + + TEST_ASSERT_FALSE(handle.valid()); +} + +TEST(AssetCache, HasAsset) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + cache.add(asset); + + TEST_ASSERT_TRUE(cache.has(AssetID("test.asset"))); + TEST_ASSERT_FALSE(cache.has(AssetID("other.asset"))); +} + +TEST(AssetCache, RemoveAsset) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + cache.add(asset); + + TEST_ASSERT_TRUE(cache.remove(AssetID("test.asset"))); + TEST_ASSERT_EQ(0u, cache.count()); + TEST_ASSERT_FALSE(cache.has(AssetID("test.asset"))); +} + +TEST(AssetCache, RemoveNonExistent) { + AssetCache cache; + + TEST_ASSERT_FALSE(cache.remove(AssetID("nonexistent.asset"))); +} + +// --------------------------------------------------------------------------- +// 缓存统计测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, Stats_Initial) { + AssetCache cache; + + auto stats = cache.stats(); + + TEST_ASSERT_EQ(0u, stats.hits); + TEST_ASSERT_EQ(0u, stats.misses); + TEST_ASSERT_EQ(0u, stats.count); +} + +TEST(AssetCache, Stats_HitMiss) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + cache.add(asset); + + cache.get(AssetID("test.asset")); + cache.get(AssetID("nonexistent.asset")); + + auto stats = cache.stats(); + + TEST_ASSERT_EQ(1u, stats.hits); + TEST_ASSERT_EQ(1u, stats.misses); +} + +TEST(AssetCache, Stats_Reset) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + cache.add(asset); + + cache.get(AssetID("test.asset")); + cache.get(AssetID("nonexistent.asset")); + + cache.resetStats(); + + auto stats = cache.stats(); + + TEST_ASSERT_EQ(0u, stats.hits); + TEST_ASSERT_EQ(0u, stats.misses); +} + +// --------------------------------------------------------------------------- +// 内存限制测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, SetLimit) { + AssetCache cache; + + cache.setLimit(500); + + TEST_ASSERT_EQ(500u, cache.limit()); +} + +TEST(AssetCache, MemoryTracking) { + AssetCache cache; + + auto asset1 = TestAsset::create("asset1", 100); + auto asset2 = TestAsset::create("asset2", 200); + + cache.add(asset1); + cache.add(asset2); + + TEST_ASSERT_EQ(300u, cache.bytes()); +} + +TEST(AssetCache, Clear) { + AssetCache cache; + + auto asset1 = TestAsset::create("asset1", 100); + auto asset2 = TestAsset::create("asset2", 200); + + cache.add(asset1); + cache.add(asset2); + + cache.clear(); + + TEST_ASSERT_EQ(0u, cache.count()); + TEST_ASSERT_EQ(0u, cache.bytes()); +} + +// --------------------------------------------------------------------------- +// LRU 测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, LRU_AccessOrder) { + AssetCache cache; + + auto asset1 = TestAsset::create("asset1", 100); + auto asset2 = TestAsset::create("asset2", 100); + auto asset3 = TestAsset::create("asset3", 100); + + cache.add(asset1); + cache.add(asset2); + cache.add(asset3); + + cache.get(AssetID("asset1")); + + TEST_ASSERT_TRUE(cache.has(AssetID("asset1"))); + TEST_ASSERT_TRUE(cache.has(AssetID("asset2"))); + TEST_ASSERT_TRUE(cache.has(AssetID("asset3"))); +} + +// --------------------------------------------------------------------------- +// Purge 测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, Purge_NoReferences) { + AssetCache cache; + + { + auto asset = TestAsset::create("test.asset", 100); + cache.add(asset); + } + + size_t purged = cache.purge(); + + TEST_ASSERT_EQ(1u, purged); + TEST_ASSERT_EQ(0u, cache.count()); +} + +TEST(AssetCache, Purge_WithReferences) { + AssetCache cache; + + auto asset = TestAsset::create("test.asset", 100); + auto handle = cache.add(asset); + + size_t purged = cache.purge(); + + TEST_ASSERT_EQ(0u, purged); + TEST_ASSERT_EQ(1u, cache.count()); +} + +// --------------------------------------------------------------------------- +// 多资源测试 +// --------------------------------------------------------------------------- + +TEST(AssetCache, MultipleAssets) { + AssetCache cache; + + for (int i = 0; i < 10; ++i) { + auto asset = TestAsset::create("asset" + std::to_string(i), 100); + cache.add(asset); + } + + TEST_ASSERT_EQ(10u, cache.count()); + TEST_ASSERT_EQ(1000u, cache.bytes()); + + for (int i = 0; i < 10; ++i) { + TEST_ASSERT_TRUE(cache.has(AssetID("asset" + std::to_string(i)))); + } +} + +TEST(AssetCache, RemoveMultiple) { + AssetCache cache; + + for (int i = 0; i < 5; ++i) { + auto asset = TestAsset::create("asset" + std::to_string(i), 100); + cache.add(asset); + } + + for (int i = 0; i < 3; ++i) { + cache.remove(AssetID("asset" + std::to_string(i))); + } + + TEST_ASSERT_EQ(2u, cache.count()); + TEST_ASSERT_EQ(200u, cache.bytes()); +} diff --git a/Tests/test_asset_pack.cpp b/Tests/test_asset_pack.cpp new file mode 100644 index 0000000..81c057c --- /dev/null +++ b/Tests/test_asset_pack.cpp @@ -0,0 +1,427 @@ +/** + * @file test_asset_pack.cpp + * @brief AssetPack 单元测试 + * + * 测试资源包的创建、读取和管理功能。 + */ + +#include "test_framework.h" +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +// --------------------------------------------------------------------------- +// 测试辅助函数 +// --------------------------------------------------------------------------- + +namespace { +std::string testDir = "test_temp"; + +void setupTestDir() { std::filesystem::create_directories(testDir); } + +void cleanupTestDir() { std::filesystem::remove_all(testDir); } + +std::string getTestPath(const std::string &name) { + return testDir + "/" + name; +} + +void writeTestFile(const std::string &path, const std::vector &data) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(data.data()), data.size()); +} +} // namespace + +// --------------------------------------------------------------------------- +// AssetPackBuilder 测试 +// --------------------------------------------------------------------------- + +TEST(AssetPackBuilder, Create) { + AssetPackBuilder builder; + + TEST_ASSERT_EQ(0u, builder.count()); + TEST_ASSERT_EQ(0u, builder.totalOriginalSize()); + TEST_ASSERT_EQ(0u, builder.totalCompressedSize()); +} + +TEST(AssetPackBuilder, AddData) { + AssetPackBuilder builder; + + std::vector data = createTestData(1024); + builder.add("test.bin", data); + + TEST_ASSERT_EQ(1u, builder.count()); + TEST_ASSERT_EQ(1024u, builder.totalOriginalSize()); +} + +TEST(AssetPackBuilder, AddMultipleData) { + AssetPackBuilder builder; + + builder.add("file1.bin", createTestData(100)); + builder.add("file2.bin", createTestData(200)); + builder.add("file3.bin", createTestData(300)); + + TEST_ASSERT_EQ(3u, builder.count()); + TEST_ASSERT_EQ(600u, builder.totalOriginalSize()); +} + +TEST(AssetPackBuilder, AddMovedData) { + AssetPackBuilder builder; + + std::vector data = createTestData(512); + builder.add("moved.bin", std::move(data)); + + TEST_ASSERT_EQ(1u, builder.count()); +} + +TEST(AssetPackBuilder, Clear) { + AssetPackBuilder builder; + + builder.add("test.bin", createTestData(100)); + builder.clear(); + + TEST_ASSERT_EQ(0u, builder.count()); + TEST_ASSERT_EQ(0u, builder.totalOriginalSize()); +} + +TEST(AssetPackBuilder, BuildSimple) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(256)); + + std::string packPath = getTestPath("simple.pack"); + bool result = builder.build(packPath); + + TEST_ASSERT_TRUE(result); + TEST_ASSERT_TRUE(std::filesystem::exists(packPath)); + + cleanupTestDir(); +} + +TEST(AssetPackBuilder, BuildMultipleFiles) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("file1.bin", createTestData(100, 1)); + builder.add("file2.bin", createTestData(200, 2)); + builder.add("file3.bin", createTestData(300, 3)); + + std::string packPath = getTestPath("multi.pack"); + bool result = builder.build(packPath); + + TEST_ASSERT_TRUE(result); + TEST_ASSERT_TRUE(std::filesystem::exists(packPath)); + + cleanupTestDir(); +} + +TEST(AssetPackBuilder, SetEncryption) { + AssetPackBuilder builder; + builder.setEncryption("secret-key", Decryptor::Type::XOR); + + builder.add("encrypted.bin", createTestData(128)); + + TEST_ASSERT_EQ(1u, builder.count()); +} + +// --------------------------------------------------------------------------- +// AssetPack 测试 +// --------------------------------------------------------------------------- + +TEST(AssetPack, Create) { + AssetPack pack; + + TEST_ASSERT_FALSE(pack.isOpen()); + TEST_ASSERT_EQ(0u, pack.count()); +} + +TEST(AssetPack, OpenNonExistent) { + AssetPack pack; + + bool result = pack.open("nonexistent.pack"); + + TEST_ASSERT_FALSE(result); + TEST_ASSERT_FALSE(pack.isOpen()); +} + +TEST(AssetPack, OpenAndClose) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(100)); + builder.build(getTestPath("test.pack")); + + AssetPack pack; + bool opened = pack.open(getTestPath("test.pack")); + + TEST_ASSERT_TRUE(opened); + TEST_ASSERT_TRUE(pack.isOpen()); + + pack.close(); + + TEST_ASSERT_FALSE(pack.isOpen()); + + cleanupTestDir(); +} + +TEST(AssetPack, ReadAsset) { + setupTestDir(); + + std::vector originalData = createTestData(512); + + AssetPackBuilder builder; + builder.add("data.bin", originalData); + builder.build(getTestPath("read.pack")); + + AssetPack pack; + pack.open(getTestPath("read.pack")); + + std::vector readData = pack.read("data.bin"); + + TEST_ASSERT_TRUE(dataEquals(originalData, readData)); + + pack.close(); + cleanupTestDir(); +} + +TEST(AssetPack, HasAsset) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("exists.bin", createTestData(100)); + builder.build(getTestPath("has.pack")); + + AssetPack pack; + pack.open(getTestPath("has.pack")); + + TEST_ASSERT_TRUE(pack.has("exists.bin")); + TEST_ASSERT_FALSE(pack.has("not_exists.bin")); + + pack.close(); + cleanupTestDir(); +} + +TEST(AssetPack, GetAssets) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("file1.bin", createTestData(100)); + builder.add("file2.bin", createTestData(100)); + builder.add("file3.bin", createTestData(100)); + builder.build(getTestPath("assets.pack")); + + AssetPack pack; + pack.open(getTestPath("assets.pack")); + + std::vector assets = pack.assets(); + + TEST_ASSERT_EQ(3u, assets.size()); + + pack.close(); + cleanupTestDir(); +} + +TEST(AssetPack, MoveSemantics) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(100)); + builder.build(getTestPath("move.pack")); + + AssetPack pack1; + pack1.open(getTestPath("move.pack")); + + AssetPack pack2 = std::move(pack1); + + TEST_ASSERT_TRUE(pack2.isOpen()); + TEST_ASSERT_FALSE(pack1.isOpen()); + + pack2.close(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// PackManager 测试 +// --------------------------------------------------------------------------- + +TEST(PackManager, Create) { + PackManager manager; + + TEST_ASSERT_EQ(0u, manager.count()); +} + +TEST(PackManager, MountUnmount) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(100)); + builder.build(getTestPath("manager.pack")); + + PackManager manager; + bool mounted = manager.mount(getTestPath("manager.pack")); + + TEST_ASSERT_TRUE(mounted); + TEST_ASSERT_EQ(1u, manager.count()); + + manager.unmount(getTestPath("manager.pack")); + + TEST_ASSERT_EQ(0u, manager.count()); + + cleanupTestDir(); +} + +TEST(PackManager, MountNonExistent) { + PackManager manager; + + bool mounted = manager.mount("nonexistent.pack"); + + TEST_ASSERT_FALSE(mounted); + TEST_ASSERT_EQ(0u, manager.count()); +} + +TEST(PackManager, HasAsset) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("resource.bin", createTestData(100)); + builder.build(getTestPath("has.pack")); + + PackManager manager; + manager.mount(getTestPath("has.pack")); + + TEST_ASSERT_TRUE(manager.has("resource.bin")); + TEST_ASSERT_FALSE(manager.has("missing.bin")); + + manager.unmountAll(); + cleanupTestDir(); +} + +TEST(PackManager, ReadAsset) { + setupTestDir(); + + std::vector originalData = createTestData(256); + + AssetPackBuilder builder; + builder.add("data.bin", originalData); + builder.build(getTestPath("read.pack")); + + PackManager manager; + manager.mount(getTestPath("read.pack")); + + std::vector readData = manager.read("data.bin"); + + TEST_ASSERT_TRUE(dataEquals(originalData, readData)); + + manager.unmountAll(); + cleanupTestDir(); +} + +TEST(PackManager, MultiplePacks) { + setupTestDir(); + + AssetPackBuilder builder1; + builder1.add("pack1/file1.bin", createTestData(100)); + builder1.build(getTestPath("pack1.pack")); + + AssetPackBuilder builder2; + builder2.add("pack2/file2.bin", createTestData(200)); + builder2.build(getTestPath("pack2.pack")); + + PackManager manager; + manager.mount(getTestPath("pack1.pack")); + manager.mount(getTestPath("pack2.pack")); + + TEST_ASSERT_EQ(2u, manager.count()); + TEST_ASSERT_TRUE(manager.has("pack1/file1.bin")); + TEST_ASSERT_TRUE(manager.has("pack2/file2.bin")); + + manager.unmountAll(); + cleanupTestDir(); +} + +TEST(PackManager, UnmountAll) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(100)); + builder.build(getTestPath("unmount.pack")); + + PackManager manager; + manager.mount(getTestPath("unmount.pack")); + + manager.unmountAll(); + + TEST_ASSERT_EQ(0u, manager.count()); + + cleanupTestDir(); +} + +TEST(PackManager, AllAssets) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("file1.bin", createTestData(100)); + builder.add("file2.bin", createTestData(100)); + builder.build(getTestPath("all.pack")); + + PackManager manager; + manager.mount(getTestPath("all.pack")); + + std::vector assets = manager.allAssets(); + + TEST_ASSERT_EQ(2u, assets.size()); + + manager.unmountAll(); + cleanupTestDir(); +} + +TEST(PackManager, MountedPacks) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("test.bin", createTestData(100)); + builder.build(getTestPath("mounted.pack")); + + PackManager manager; + manager.mount(getTestPath("mounted.pack")); + + std::vector mounted = manager.mountedPacks(); + + TEST_ASSERT_EQ(1u, mounted.size()); + + manager.unmountAll(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 加密资源包测试 +// --------------------------------------------------------------------------- + +TEST(AssetPack, EncryptedPack) { + setupTestDir(); + + std::vector originalData = createTestData(256); + std::string key = "encryption-key"; + + AssetPackBuilder builder(Compression::None); + builder.setEncryption(key, Decryptor::Type::XOR); + builder.add("encrypted.bin", originalData); + builder.build(getTestPath("encrypted.pack")); + + AssetPack pack; + pack.open(getTestPath("encrypted.pack")); + + DataPipe pipe; + pipe.decrypt(key, Decryptor::Type::XOR); + pack.setPipe(std::move(pipe)); + + std::vector readData = pack.read("encrypted.bin"); + + TEST_ASSERT_TRUE(dataEquals(originalData, readData)); + + pack.close(); + cleanupTestDir(); +} diff --git a/Tests/test_asset_service.cpp b/Tests/test_asset_service.cpp new file mode 100644 index 0000000..4b7b75c --- /dev/null +++ b/Tests/test_asset_service.cpp @@ -0,0 +1,500 @@ +/** + * @file test_asset_service.cpp + * @brief AssetService 单元测试 + * + * 测试资源服务的加载、缓存、异步加载等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +// --------------------------------------------------------------------------- +// 测试辅助函数 +// --------------------------------------------------------------------------- + +namespace { + std::string testDir = "test_temp_service"; + + void setupTestDir() { + std::filesystem::create_directories(testDir); + } + + void cleanupTestDir() { + std::filesystem::remove_all(testDir); + } + + std::string getTestPath(const std::string& name) { + return testDir + "/" + name; + } + + void writeTestFile(const std::string& path, const std::vector& data) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(data.data()), data.size()); + } +} + +// --------------------------------------------------------------------------- +// 测试用资源类和加载器 +// --------------------------------------------------------------------------- + +/** + * @brief 测试用资源类 + */ +class TestResource : public Asset { +public: + AssetType type() const override { return AssetType::Data; } + + bool loaded() const override { + return state_.load(std::memory_order_acquire) == AssetState::Loaded; + } + + size_t memSize() const override { return data_.size(); } + + const std::vector& data() const { return data_; } + + void setData(const std::vector& data) { + data_ = data; + setState(AssetState::Loaded); + } + + void initId(const std::string& path) { + setId(AssetID(path)); + setPath(path); + } + +private: + std::vector data_; +}; + +/** + * @brief 测试用资源加载器 + */ +class TestResourceLoader : public AssetLoader { +public: + Ref load(const std::string& path) override { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return nullptr; + } + + size_t size = static_cast(file.tellg()); + file.seekg(0); + + std::vector data(size); + file.read(reinterpret_cast(data.data()), size); + + auto resource = std::make_shared(); + resource->initId(path); + resource->setData(data); + + return resource; + } + + Ref loadFromMemory(const u8* data, size_t size) override { + auto resource = std::make_shared(); + resource->setData(std::vector(data, data + size)); + return resource; + } + + bool canLoad(const std::string& path) const override { + return path.find(".test") != std::string::npos; + } + + AssetType type() const override { return AssetType::Data; } + + std::vector extensions() const override { + return {".test"}; + } +}; + +// --------------------------------------------------------------------------- +// AssetService 基本测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, Create) { + AssetService service; + + TEST_ASSERT_FALSE(service.isLoaded("nonexistent")); + TEST_ASSERT_FALSE(service.isLoading("nonexistent")); + TEST_ASSERT_EQ(0u, service.size()); +} + +TEST(AssetService, InitShutdown) { + AssetService service; + + bool initResult = service.init(); + + TEST_ASSERT_TRUE(initResult); + + service.shutdown(); +} + +TEST(AssetService, SetRoot) { + AssetService service; + service.init(); + + service.setRoot("assets"); + + TEST_ASSERT_EQ("assets", service.root()); + + service.shutdown(); +} + +TEST(AssetService, SetLimit) { + AssetService service; + service.init(); + + service.setLimit(1024 * 1024); + + service.shutdown(); +} + +// --------------------------------------------------------------------------- +// 加载器注册测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, RegisterLoader) { + AssetService service; + service.init(); + + service.registerLoader(std::make_unique()); + + service.shutdown(); +} + +// --------------------------------------------------------------------------- +// 资源加载测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, LoadNonExistent) { + setupTestDir(); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + auto handle = service.load("nonexistent.test"); + + TEST_ASSERT_FALSE(handle.valid()); + + service.shutdown(); + cleanupTestDir(); +} + +TEST(AssetService, LoadFromMemory) { + AssetService service; + service.init(); + service.registerLoader(std::make_unique()); + + std::vector testData = createTestData(256); + + service.shutdown(); +} + +TEST(AssetService, IsLoaded) { + setupTestDir(); + + std::string filePath = getTestPath("resource.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + TEST_ASSERT_FALSE(service.isLoaded("resource.test")); + + auto handle = service.load("resource.test"); + + TEST_ASSERT_TRUE(service.isLoaded("resource.test")); + + service.shutdown(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 缓存测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, CacheHit) { + setupTestDir(); + + std::string filePath = getTestPath("cached.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + auto handle1 = service.load("cached.test"); + auto handle2 = service.get("cached.test"); + + TEST_ASSERT_TRUE(handle1.valid()); + TEST_ASSERT_TRUE(handle2.valid()); + TEST_ASSERT_EQ(handle1.get().get(), handle2.get().get()); + + auto stats = service.stats(); + TEST_ASSERT_GE(stats.hits, 1u); + + service.shutdown(); + cleanupTestDir(); +} + +TEST(AssetService, CacheMiss) { + AssetService service; + service.init(); + + auto handle = service.get("not_in_cache.test"); + + TEST_ASSERT_FALSE(handle.valid()); + + auto stats = service.stats(); + TEST_ASSERT_GE(stats.misses, 1u); + + service.shutdown(); +} + +TEST(AssetService, Purge) { + setupTestDir(); + + std::string filePath = getTestPath("purge.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + { + auto handle = service.load("purge.test"); + } + + service.purge(); + + TEST_ASSERT_EQ(0u, service.size()); + + service.shutdown(); + cleanupTestDir(); +} + +TEST(AssetService, Clear) { + setupTestDir(); + + std::string filePath = getTestPath("clear.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + auto handle = service.load("clear.test"); + + TEST_ASSERT_GT(service.size(), 0u); + + service.clear(); + + TEST_ASSERT_EQ(0u, service.size()); + + service.shutdown(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 卸载测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, Unload) { + setupTestDir(); + + std::string filePath = getTestPath("unload.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + auto handle = service.load("unload.test"); + + TEST_ASSERT_TRUE(service.isLoaded("unload.test")); + + service.unload("unload.test"); + + TEST_ASSERT_FALSE(service.isLoaded("unload.test")); + + service.shutdown(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 资源包挂载测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, MountPack) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("packed.test", createTestData(256)); + builder.build(getTestPath("service.pack")); + + AssetService service; + service.init(); + service.registerLoader(std::make_unique()); + + bool mounted = service.mount(getTestPath("service.pack")); + + TEST_ASSERT_TRUE(mounted); + + service.shutdown(); + cleanupTestDir(); +} + +TEST(AssetService, UnmountPack) { + setupTestDir(); + + AssetPackBuilder builder; + builder.add("packed.test", createTestData(256)); + builder.build(getTestPath("unmount.pack")); + + AssetService service; + service.init(); + service.registerLoader(std::make_unique()); + + service.mount(getTestPath("unmount.pack")); + service.unmount(getTestPath("unmount.pack")); + + service.shutdown(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 统计测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, Stats) { + AssetService service; + service.init(); + + auto stats = service.stats(); + + TEST_ASSERT_EQ(0u, stats.hits); + TEST_ASSERT_EQ(0u, stats.misses); + TEST_ASSERT_EQ(0u, stats.count); + + service.shutdown(); +} + +// --------------------------------------------------------------------------- +// 数据处理管道测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, SetPipe) { + AssetService service; + service.init(); + + DataPipe pipe; + pipe.encrypt("key", Decryptor::Type::XOR); + + service.setPipe(std::move(pipe)); + + service.shutdown(); +} + +// --------------------------------------------------------------------------- +// 异步加载测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, AsyncLoad) { + setupTestDir(); + + std::string filePath = getTestPath("async.test"); + writeTestFile(filePath, createTestData(512)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + std::atomic callbackCalled{false}; + AssetHandle resultHandle; + + service.loadAsync("async.test", + [&](AssetHandle handle) { + resultHandle = handle; + callbackCalled = true; + }); + + for (int i = 0; i < 100 && !callbackCalled; ++i) { + service.process(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + service.process(); + + TEST_ASSERT_TRUE(callbackCalled); + TEST_ASSERT_TRUE(resultHandle.valid()); + + service.shutdown(); + cleanupTestDir(); +} + +TEST(AssetService, Preload) { + setupTestDir(); + + std::string filePath = getTestPath("preload.test"); + writeTestFile(filePath, createTestData(256)); + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + service.preload("preload.test"); + + for (int i = 0; i < 50; ++i) { + service.process(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + service.shutdown(); + cleanupTestDir(); +} + +// --------------------------------------------------------------------------- +// 多资源测试 +// --------------------------------------------------------------------------- + +TEST(AssetService, LoadMultiple) { + setupTestDir(); + + for (int i = 0; i < 5; ++i) { + std::string filePath = getTestPath("multi" + std::to_string(i) + ".test"); + writeTestFile(filePath, createTestData(100 * (i + 1))); + } + + AssetService service; + service.init(); + service.setRoot(testDir); + service.registerLoader(std::make_unique()); + + for (int i = 0; i < 5; ++i) { + auto handle = service.load("multi" + std::to_string(i) + ".test"); + TEST_ASSERT_TRUE(handle.valid()); + } + + TEST_ASSERT_GT(service.size(), 0u); + + service.shutdown(); + cleanupTestDir(); +} diff --git a/Tests/test_data_processor.cpp b/Tests/test_data_processor.cpp new file mode 100644 index 0000000..89522bb --- /dev/null +++ b/Tests/test_data_processor.cpp @@ -0,0 +1,260 @@ +/** + * @file test_data_processor.cpp + * @brief DataProcessor 单元测试 + * + * 测试数据处理管道、加密/解密、压缩/解压功能。 + */ + +#include "test_framework.h" +#include + +using namespace extra2d; +using namespace extra2d::test; + +// --------------------------------------------------------------------------- +// XOR 加密/解密测试 +// --------------------------------------------------------------------------- + +TEST(DataProcessor, XOR_EncryptDecrypt) { + std::vector original = createTestData(1024); + std::string key = "test-key-12345"; + + Encryptor encryptor(key, Decryptor::Type::XOR); + Decryptor decryptor(key, Decryptor::Type::XOR); + + std::vector encrypted = encryptor.process(original); + std::vector decrypted = decryptor.process(encrypted); + + TEST_ASSERT_TRUE(dataEquals(original, decrypted)); + TEST_ASSERT_FALSE(dataEquals(original, encrypted)); +} + +TEST(DataProcessor, XOR_SameKeyProducesSameResult) { + std::vector data = createTestData(512); + std::string key = "secret"; + + Encryptor enc1(key, Decryptor::Type::XOR); + Encryptor enc2(key, Decryptor::Type::XOR); + + std::vector result1 = enc1.process(data); + std::vector result2 = enc2.process(data); + + TEST_ASSERT_TRUE(dataEquals(result1, result2)); +} + +TEST(DataProcessor, XOR_DifferentKeysProduceDifferentResults) { + std::vector data = createTestData(512); + + Encryptor enc1("key1", Decryptor::Type::XOR); + Encryptor enc2("key2", Decryptor::Type::XOR); + + std::vector result1 = enc1.process(data); + std::vector result2 = enc2.process(data); + + TEST_ASSERT_FALSE(dataEquals(result1, result2)); +} + +TEST(DataProcessor, XOR_EmptyKey) { + std::vector data = createTestData(256); + + Encryptor encryptor("", Decryptor::Type::XOR); + Decryptor decryptor("", Decryptor::Type::XOR); + + std::vector encrypted = encryptor.process(data); + std::vector decrypted = decryptor.process(encrypted); + + TEST_ASSERT_TRUE(dataEquals(data, decrypted)); +} + +TEST(DataProcessor, XOR_EmptyData) { + std::vector empty; + std::string key = "test"; + + Encryptor encryptor(key, Decryptor::Type::XOR); + Decryptor decryptor(key, Decryptor::Type::XOR); + + std::vector encrypted = encryptor.process(empty); + std::vector decrypted = decryptor.process(encrypted); + + TEST_ASSERT_TRUE(decrypted.empty()); +} + +// --------------------------------------------------------------------------- +// DataPipe 测试 +// --------------------------------------------------------------------------- + +TEST(DataProcessor, DataPipe_Empty) { + DataPipe pipe; + + TEST_ASSERT_TRUE(pipe.empty()); + TEST_ASSERT_EQ(0u, pipe.size()); + + std::vector data = createTestData(100); + std::vector result = pipe.process(data); + + TEST_ASSERT_TRUE(dataEquals(data, result)); +} + +TEST(DataProcessor, DataPipe_SingleProcessor) { + DataPipe pipe; + pipe.encrypt("key", Decryptor::Type::XOR); + + TEST_ASSERT_FALSE(pipe.empty()); + TEST_ASSERT_EQ(1u, pipe.size()); + + std::vector original = createTestData(256); + std::vector processed = pipe.process(original); + + TEST_ASSERT_FALSE(dataEquals(original, processed)); +} + +TEST(DataProcessor, DataPipe_MultipleProcessors) { + DataPipe pipe; + pipe.encrypt("key", Decryptor::Type::XOR); + pipe.encrypt("key2", Decryptor::Type::XOR); + pipe.encrypt("key", Decryptor::Type::XOR); + pipe.encrypt("key2", Decryptor::Type::XOR); + + TEST_ASSERT_EQ(4u, pipe.size()); + + std::vector original = createTestData(512); + std::vector processed = pipe.process(original); + + TEST_ASSERT_TRUE(dataEquals(original, processed)); +} + +TEST(DataProcessor, DataPipe_EncryptDecrypt) { + DataPipe encryptPipe; + encryptPipe.encrypt("secret-key", Decryptor::Type::XOR); + + DataPipe decryptPipe; + decryptPipe.decrypt("secret-key", Decryptor::Type::XOR); + + std::vector original = createTestData(1024); + std::vector encrypted = encryptPipe.process(original); + std::vector decrypted = decryptPipe.process(encrypted); + + TEST_ASSERT_TRUE(dataEquals(original, decrypted)); +} + +TEST(DataProcessor, DataPipe_Clear) { + DataPipe pipe; + pipe.encrypt("key", Decryptor::Type::XOR); + pipe.encrypt("key2", Decryptor::Type::XOR); + + TEST_ASSERT_EQ(2u, pipe.size()); + + pipe.clear(); + + TEST_ASSERT_TRUE(pipe.empty()); + TEST_ASSERT_EQ(0u, pipe.size()); +} + +TEST(DataProcessor, DataPipe_MoveSemantics) { + DataPipe pipe1; + pipe1.encrypt("key", Decryptor::Type::XOR); + + DataPipe pipe2 = std::move(pipe1); + + TEST_ASSERT_EQ(1u, pipe2.size()); + + std::vector data = createTestData(128); + std::vector result = pipe2.process(data); + + TEST_ASSERT_FALSE(dataEquals(data, result)); +} + +// --------------------------------------------------------------------------- +// 校验和测试 +// --------------------------------------------------------------------------- + +TEST(DataProcessor, Checksum_Consistency) { + std::vector data = createTestData(1024); + + std::vector checksum1 = computeChecksum(data); + std::vector checksum2 = computeChecksum(data); + + TEST_ASSERT_TRUE(dataEquals(checksum1, checksum2)); +} + +TEST(DataProcessor, Checksum_DifferentData) { + std::vector data1 = createTestData(1024, 42); + std::vector data2 = createTestData(1024, 43); + + std::vector checksum1 = computeChecksum(data1); + std::vector checksum2 = computeChecksum(data2); + + TEST_ASSERT_FALSE(dataEquals(checksum1, checksum2)); +} + +TEST(DataProcessor, Checksum_Verify) { + std::vector data = createTestData(2048); + std::vector checksum = computeChecksum(data); + + TEST_ASSERT_TRUE(verifyChecksum(data, checksum)); +} + +TEST(DataProcessor, Checksum_VerifyTampered) { + std::vector data = createTestData(2048); + std::vector checksum = computeChecksum(data); + + data[100] ^= 0xFF; + + TEST_ASSERT_FALSE(verifyChecksum(data, checksum)); +} + +TEST(DataProcessor, Checksum_Size) { + std::vector data = createTestData(100); + std::vector checksum = computeChecksum(data); + + TEST_ASSERT_EQ(32u, checksum.size()); +} + +// --------------------------------------------------------------------------- +// 处理器链测试 +// --------------------------------------------------------------------------- + +TEST(DataProcessor, Chain_EncryptDecrypt) { + std::vector original = createTestData(512); + std::string key = "chain-test-key"; + + Encryptor encryptor(key, Decryptor::Type::XOR); + Decryptor decryptor(key, Decryptor::Type::XOR); + + encryptor.setNext(std::make_unique(key, Decryptor::Type::XOR)); + + std::vector result = encryptor.process(original); + + TEST_ASSERT_TRUE(dataEquals(original, result)); +} + +// --------------------------------------------------------------------------- +// 边界条件测试 +// --------------------------------------------------------------------------- + +TEST(DataProcessor, Boundary_SingleByte) { + std::vector data = {0x42}; + std::string key = "key"; + + Encryptor encryptor(key, Decryptor::Type::XOR); + Decryptor decryptor(key, Decryptor::Type::XOR); + + std::vector encrypted = encryptor.process(data); + std::vector decrypted = decryptor.process(encrypted); + + TEST_ASSERT_TRUE(dataEquals(data, decrypted)); + TEST_ASSERT_EQ(1u, encrypted.size()); +} + +TEST(DataProcessor, Boundary_LargeData) { + std::vector data = createTestData(1024 * 1024); + std::string key = "large-data-key"; + + Encryptor encryptor(key, Decryptor::Type::XOR); + Decryptor decryptor(key, Decryptor::Type::XOR); + + std::vector encrypted = encryptor.process(data); + std::vector decrypted = decryptor.process(encrypted); + + TEST_ASSERT_TRUE(dataEquals(data, decrypted)); +} diff --git a/Tests/test_framework.cpp b/Tests/test_framework.cpp new file mode 100644 index 0000000..4d1a81e --- /dev/null +++ b/Tests/test_framework.cpp @@ -0,0 +1,65 @@ +/** + * @file test_framework.cpp + * @brief 测试框架实现 + */ + +#include "test_framework.h" + +namespace extra2d { +namespace test { + +static TestStats g_stats; + +std::vector& getTestCases() { + static std::vector cases; + return cases; +} + +TestRegistrar::TestRegistrar(const std::string& suite, const std::string& name, TestFunc func) { + getTestCases().push_back({name, suite, func}); +} + +int runAllTests() { + auto& cases = getTestCases(); + std::cout << "========================================\n"; + std::cout << "Running " << cases.size() << " tests...\n"; + std::cout << "========================================\n\n"; + + std::string currentSuite; + + for (const auto& tc : cases) { + if (tc.suite != currentSuite) { + currentSuite = tc.suite; + std::cout << "\n[" << currentSuite << "]\n"; + } + + auto start = std::chrono::high_resolution_clock::now(); + + try { + tc.func(); + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + std::cout << " [PASS] " << tc.name << " (" << duration.count() << "ms)\n"; + g_stats.passed++; + } catch (const std::exception& e) { + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + std::cout << " [FAIL] " << tc.name << " (" << duration.count() << "ms)\n"; + std::cout << " Error: " << e.what() << "\n"; + g_stats.failed++; + } + } + + std::cout << "\n========================================\n"; + std::cout << "Results: " << g_stats.passed << " passed, " + << g_stats.failed << " failed, " + << g_stats.skipped << " skipped\n"; + std::cout << "========================================\n"; + + return g_stats.failed > 0 ? 1 : 0; +} + +} // namespace test +} // namespace extra2d diff --git a/Tests/test_framework.h b/Tests/test_framework.h new file mode 100644 index 0000000..88ad597 --- /dev/null +++ b/Tests/test_framework.h @@ -0,0 +1,140 @@ +#pragma once + +/** + * @file test_framework.h + * @brief 轻量级测试框架 + * + * 提供简单的测试断言和测试注册机制。 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace extra2d { +namespace test { + +// --------------------------------------------------------------------------- +// 测试结果统计 +// --------------------------------------------------------------------------- + +struct TestStats { + int passed = 0; + int failed = 0; + int skipped = 0; + + int total() const { return passed + failed + skipped; } +}; + +// --------------------------------------------------------------------------- +// 测试断言 +// --------------------------------------------------------------------------- + +#define TEST_ASSERT(condition) \ + do { \ + if (!(condition)) { \ + std::ostringstream _ss; \ + _ss << "Assertion failed: " << #condition \ + << " at " << __FILE__ << ":" << __LINE__; \ + throw std::runtime_error(_ss.str()); \ + } \ + } while (0) + +#define TEST_ASSERT_EQ(expected, actual) \ + do { \ + if (!((expected) == (actual))) { \ + std::ostringstream _ss; \ + _ss << "Assertion failed: expected " << (expected) \ + << " but got " << (actual) \ + << " at " << __FILE__ << ":" << __LINE__; \ + throw std::runtime_error(_ss.str()); \ + } \ + } while (0) + +#define TEST_ASSERT_NE(val1, val2) \ + do { \ + if ((val1) == (val2)) { \ + std::ostringstream _ss; \ + _ss << "Assertion failed: " << (val1) \ + << " should not equal " << (val2) \ + << " at " << __FILE__ << ":" << __LINE__; \ + throw std::runtime_error(_ss.str()); \ + } \ + } while (0) + +#define TEST_ASSERT_TRUE(condition) TEST_ASSERT(condition) +#define TEST_ASSERT_FALSE(condition) TEST_ASSERT(!(condition)) +#define TEST_ASSERT_NULL(ptr) TEST_ASSERT((ptr) == nullptr) +#define TEST_ASSERT_NOT_NULL(ptr) TEST_ASSERT((ptr) != nullptr) +#define TEST_ASSERT_GT(val1, val2) TEST_ASSERT((val1) > (val2)) +#define TEST_ASSERT_GE(val1, val2) TEST_ASSERT((val1) >= (val2)) +#define TEST_ASSERT_LT(val1, val2) TEST_ASSERT((val1) < (val2)) +#define TEST_ASSERT_LE(val1, val2) TEST_ASSERT((val1) <= (val2)) + +// --------------------------------------------------------------------------- +// 测试用例 +// --------------------------------------------------------------------------- + +using TestFunc = std::function; + +struct TestCase { + std::string name; + std::string suite; + TestFunc func; +}; + +std::vector& getTestCases(); + +// --------------------------------------------------------------------------- +// 测试注册器 +// --------------------------------------------------------------------------- + +class TestRegistrar { +public: + TestRegistrar(const std::string& suite, const std::string& name, TestFunc func); +}; + +#define TEST(suite, name) \ + static void test_##suite##_##name(); \ + static extra2d::test::TestRegistrar registrar_##suite##_##name(#suite, #name, test_##suite##_##name); \ + static void test_##suite##_##name() + +// --------------------------------------------------------------------------- +// 测试运行器 +// --------------------------------------------------------------------------- + +int runAllTests(); + +// --------------------------------------------------------------------------- +// 测试工具函数 +// --------------------------------------------------------------------------- + +/** + * @brief 创建测试数据 + * @param size 数据大小 + * @param seed 随机种子 + * @return 测试数据 + */ +inline std::vector createTestData(size_t size, u32 seed = 42) { + std::vector data(size); + for (size_t i = 0; i < size; ++i) { + data[i] = static_cast((seed + i) % 256); + } + return data; +} + +/** + * @brief 比较两个数据是否相等 + */ +inline bool dataEquals(const std::vector& a, const std::vector& b) { + if (a.size() != b.size()) return false; + return std::equal(a.begin(), a.end(), b.begin()); +} + +} // namespace test +} // namespace extra2d diff --git a/Tests/test_main.cpp b/Tests/test_main.cpp new file mode 100644 index 0000000..aaa5d8d --- /dev/null +++ b/Tests/test_main.cpp @@ -0,0 +1,15 @@ +/** + * @file test_main.cpp + * @brief 测试主入口 + * + * 运行所有资源服务系统的单元测试。 + */ + +#include "test_framework.h" + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + return extra2d::test::runAllTests(); +} diff --git a/Tools/asset_packer.cpp b/Tools/asset_packer.cpp new file mode 100644 index 0000000..0961c1d --- /dev/null +++ b/Tools/asset_packer.cpp @@ -0,0 +1,460 @@ +/** + * @file asset_packer.cpp + * @brief 资源打包工具 + * + * 命令行工具,用于创建、查看和提取资源包。 + * + * 用法: + * asset_packer create [options] + * asset_packer list + * asset_packer extract + * + * 选项: + * -c, --compression 压缩算法 (none, zstd, lz4, zlib),默认 zstd + * -l, --level 压缩级别 (1-22),默认 3 + * -e, --encrypt 加密密钥 + * -t, --encrypt-type 加密类型 (xor, aes),默认 xor + * -v, --verbose 详细输出 + * -h, --help 显示帮助 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// --------------------------------------------------------------------------- +// 命令行参数解析 +// --------------------------------------------------------------------------- + +struct Options { + std::string command; + std::string outputFile; + std::string packFile; + std::string outputDir; + std::vector inputs; + + extra2d::Compression compression = extra2d::Compression::Zstd; + int compressionLevel = 3; + std::string encryptKey; + extra2d::Decryptor::Type encryptType = extra2d::Decryptor::Type::XOR; + + bool verbose = false; + bool showHelp = false; +}; + +/** + * @brief 打印帮助信息 + */ +void printHelp(const char* programName) { + std::cout << "Extra2D 资源打包工具 v1.0.0\n\n"; + std::cout << "用法:\n"; + std::cout << " " << programName << " create [options] \n"; + std::cout << " " << programName << " list \n"; + std::cout << " " << programName << " extract \n\n"; + std::cout << "命令:\n"; + std::cout << " create 创建资源包\n"; + std::cout << " list 列出资源包内容\n"; + std::cout << " extract 提取资源包内容\n\n"; + std::cout << "选项:\n"; + std::cout << " -c, --compression 压缩算法 (none, zstd, lz4, zlib),默认 zstd\n"; + std::cout << " -l, --level 压缩级别 (1-22),默认 3\n"; + std::cout << " -e, --encrypt 加密密钥\n"; + std::cout << " -t, --encrypt-type 加密类型 (xor, aes),默认 xor\n"; + std::cout << " -v, --verbose 详细输出\n"; + std::cout << " -h, --help 显示帮助\n"; +} + +/** + * @brief 解析压缩算法名称 + */ +extra2d::Compression parseCompression(const std::string& name) { + if (name == "none" || name == "None" || name == "NONE") { + return extra2d::Compression::None; + } else if (name == "zstd" || name == "Zstd" || name == "ZSTD") { + return extra2d::Compression::Zstd; + } else if (name == "lz4" || name == "LZ4") { + return extra2d::Compression::LZ4; + } else if (name == "zlib" || name == "Zlib" || name == "ZLIB") { + return extra2d::Compression::Zlib; + } + return extra2d::Compression::Zstd; +} + +/** + * @brief 解析加密类型 + */ +extra2d::Decryptor::Type parseEncryptType(const std::string& name) { + if (name == "xor" || name == "XOR") { + return extra2d::Decryptor::Type::XOR; + } else if (name == "aes" || name == "AES" || name == "aes256") { + return extra2d::Decryptor::Type::AES256; + } + return extra2d::Decryptor::Type::XOR; +} + +/** + * @brief 解析命令行参数 + */ +bool parseArgs(int argc, char* argv[], Options& opts) { + if (argc < 2) { + opts.showHelp = true; + return false; + } + + opts.command = argv[1]; + + if (opts.command == "-h" || opts.command == "--help") { + opts.showHelp = true; + return true; + } + + for (int i = 2; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "-h" || arg == "--help") { + opts.showHelp = true; + return true; + } else if (arg == "-v" || arg == "--verbose") { + opts.verbose = true; + } else if (arg == "-c" || arg == "--compression") { + if (i + 1 < argc) { + opts.compression = parseCompression(argv[++i]); + } + } else if (arg == "-l" || arg == "--level") { + if (i + 1 < argc) { + opts.compressionLevel = std::stoi(argv[++i]); + } + } else if (arg == "-e" || arg == "--encrypt") { + if (i + 1 < argc) { + opts.encryptKey = argv[++i]; + } + } else if (arg == "-t" || arg == "--encrypt-type") { + if (i + 1 < argc) { + opts.encryptType = parseEncryptType(argv[++i]); + } + } else if (arg[0] != '-') { + if (opts.command == "create") { + if (opts.outputFile.empty()) { + opts.outputFile = arg; + } else { + opts.inputs.push_back(arg); + } + } else if (opts.command == "list") { + if (opts.packFile.empty()) { + opts.packFile = arg; + } + } else if (opts.command == "extract") { + if (opts.packFile.empty()) { + opts.packFile = arg; + } else if (opts.outputDir.empty()) { + opts.outputDir = arg; + } + } + } + } + + return true; +} + +// --------------------------------------------------------------------------- +// 创建资源包 +// --------------------------------------------------------------------------- + +/** + * @brief 格式化文件大小 + */ +std::string formatSize(size_t bytes) { + const char* units[] = {"B", "KB", "MB", "GB"}; + int unitIndex = 0; + double size = static_cast(bytes); + + while (size >= 1024.0 && unitIndex < 3) { + size /= 1024.0; + ++unitIndex; + } + + std::ostringstream ss; + ss.precision(2); + ss << size << " " << units[unitIndex]; + return ss.str(); +} + +/** + * @brief 创建资源包 + */ +int createPack(const Options& opts) { + if (opts.outputFile.empty()) { + std::cerr << "错误: 未指定输出文件\n"; + return 1; + } + + if (opts.inputs.empty()) { + std::cerr << "错误: 未指定输入文件或目录\n"; + return 1; + } + + auto startTime = std::chrono::high_resolution_clock::now(); + + extra2d::AssetPackBuilder builder(opts.compression, opts.compressionLevel); + + if (!opts.encryptKey.empty()) { + builder.setEncryption(opts.encryptKey, opts.encryptType); + } + + size_t totalFiles = 0; + size_t totalSize = 0; + + for (const auto& input : opts.inputs) { + fs::path inputPath(input); + + if (!fs::exists(inputPath)) { + std::cerr << "警告: 跳过不存在的路径: " << input << "\n"; + continue; + } + + if (fs::is_directory(inputPath)) { + if (opts.verbose) { + std::cout << "扫描目录: " << input << "\n"; + } + + for (const auto& entry : fs::recursive_directory_iterator(inputPath)) { + if (entry.is_regular_file()) { + fs::path relativePath = fs::relative(entry.path(), inputPath); + std::string packPath = relativePath.generic_string(); + + if (opts.verbose) { + std::cout << " 添加: " << packPath << "\n"; + } + + if (builder.addFile(entry.path().string(), packPath)) { + ++totalFiles; + totalSize += fs::file_size(entry.path()); + } + } + } + } else if (fs::is_regular_file(inputPath)) { + std::string packPath = inputPath.filename().string(); + + if (opts.verbose) { + std::cout << "添加文件: " << packPath << "\n"; + } + + if (builder.addFile(inputPath.string(), packPath)) { + ++totalFiles; + totalSize += fs::file_size(inputPath); + } + } + } + + if (totalFiles == 0) { + std::cerr << "错误: 没有文件被添加到资源包\n"; + return 1; + } + + if (opts.verbose) { + std::cout << "\n构建资源包: " << opts.outputFile << "\n"; + } + + if (!builder.build(opts.outputFile)) { + std::cerr << "错误: 构建资源包失败\n"; + return 1; + } + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + + size_t packSize = fs::file_size(opts.outputFile); + double ratio = totalSize > 0 ? (100.0 * packSize / totalSize) : 0.0; + + std::cout << "\n完成!\n"; + std::cout << " 文件数量: " << totalFiles << "\n"; + std::cout << " 原始大小: " << formatSize(totalSize) << "\n"; + std::cout << " 压缩大小: " << formatSize(packSize) << "\n"; + std::cout << " 压缩比率: " << std::fixed << std::setprecision(1) << ratio << "%\n"; + std::cout << " 耗时: " << duration.count() << "ms\n"; + + return 0; +} + +// --------------------------------------------------------------------------- +// 列出资源包内容 +// --------------------------------------------------------------------------- + +/** + * @brief 列出资源包内容 + */ +int listPack(const Options& opts) { + if (opts.packFile.empty()) { + std::cerr << "错误: 未指定资源包文件\n"; + return 1; + } + + if (!fs::exists(opts.packFile)) { + std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n"; + return 1; + } + + extra2d::AssetPack pack; + if (!pack.open(opts.packFile)) { + std::cerr << "错误: 无法打开资源包\n"; + return 1; + } + + const auto& header = pack.header(); + + std::cout << "资源包: " << opts.packFile << "\n"; + std::cout << "----------------------------------------\n"; + std::cout << "版本: " << header.version << "\n"; + std::cout << "条目数: " << pack.count() << "\n"; + std::cout << "----------------------------------------\n\n"; + + auto assets = pack.assets(); + + std::cout << std::left << std::setw(40) << "路径" + << std::right << std::setw(12) << "大小" << "\n"; + std::cout << std::string(52, '-') << "\n"; + + size_t totalSize = 0; + for (const auto& id : assets) { + auto* entry = pack.getEntry(id); + if (entry) { + std::cout << std::left << std::setw(40) << id.path + << std::right << std::setw(12) << formatSize(entry->size) << "\n"; + totalSize += entry->size; + } + } + + std::cout << std::string(52, '-') << "\n"; + std::cout << std::left << std::setw(40) << "总计" + << std::right << std::setw(12) << formatSize(totalSize) << "\n"; + + pack.close(); + return 0; +} + +// --------------------------------------------------------------------------- +// 提取资源包 +// --------------------------------------------------------------------------- + +/** + * @brief 提取资源包内容 + */ +int extractPack(const Options& opts) { + if (opts.packFile.empty()) { + std::cerr << "错误: 未指定资源包文件\n"; + return 1; + } + + if (opts.outputDir.empty()) { + std::cerr << "错误: 未指定输出目录\n"; + return 1; + } + + if (!fs::exists(opts.packFile)) { + std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n"; + return 1; + } + + extra2d::AssetPack pack; + if (!pack.open(opts.packFile)) { + std::cerr << "错误: 无法打开资源包\n"; + return 1; + } + + fs::path outputDir(opts.outputDir); + + if (!fs::exists(outputDir)) { + fs::create_directories(outputDir); + } + + auto assets = pack.assets(); + size_t extracted = 0; + size_t failed = 0; + + std::cout << "提取资源包到: " << opts.outputDir << "\n\n"; + + for (const auto& id : assets) { + fs::path outputPath = outputDir / id.path; + fs::path parentDir = outputPath.parent_path(); + + if (!parentDir.empty() && !fs::exists(parentDir)) { + fs::create_directories(parentDir); + } + + auto data = pack.read(id); + + if (data.empty()) { + std::cerr << "警告: 无法读取: " << id.path << "\n"; + ++failed; + continue; + } + + std::ofstream outFile(outputPath, std::ios::binary); + if (!outFile) { + std::cerr << "警告: 无法创建文件: " << outputPath.string() << "\n"; + ++failed; + continue; + } + + outFile.write(reinterpret_cast(data.data()), data.size()); + + if (opts.verbose) { + std::cout << " 提取: " << id.path << " (" << formatSize(data.size()) << ")\n"; + } + + ++extracted; + } + + pack.close(); + + std::cout << "\n完成! 提取 " << extracted << " 个文件"; + if (failed > 0) { + std::cout << ", " << failed << " 个失败"; + } + std::cout << "\n"; + + return failed > 0 ? 1 : 0; +} + +// --------------------------------------------------------------------------- +// 主函数 +// --------------------------------------------------------------------------- + +#include + +int main(int argc, char* argv[]) { + Options opts; + + if (!parseArgs(argc, argv, opts)) { + printHelp(argv[0]); + return 1; + } + + if (opts.showHelp) { + printHelp(argv[0]); + return 0; + } + + if (opts.command == "create") { + return createPack(opts); + } else if (opts.command == "list") { + return listPack(opts); + } else if (opts.command == "extract") { + return extractPack(opts); + } else { + std::cerr << "错误: 未知命令: " << opts.command << "\n"; + printHelp(argv[0]); + return 1; + } +} diff --git a/xmake.lua b/xmake.lua index a28985b..cacb449 100644 --- a/xmake.lua +++ b/xmake.lua @@ -87,6 +87,9 @@ if target_plat ~= "switch" then add_requires("glm") add_requires("nlohmann_json") add_requires("glfw") + add_requires("zstd") + add_requires("lz4") + add_requires("zlib") end @@ -99,3 +102,97 @@ includes("xmake/engine.lua") -- 定义引擎库 define_extra2d_engine() + +-- ============================================== +-- 测试目标 +-- ============================================== + +target("extra2d_tests") + set_kind("binary") + set_default(false) + + -- 测试源文件 + add_files("Tests/test_framework.cpp") + add_files("Tests/test_main.cpp") + add_files("Tests/test_data_processor.cpp") + add_files("Tests/test_asset_cache.cpp") + add_files("Tests/test_asset_pack.cpp") + add_files("Tests/test_asset_service.cpp") + + -- 头文件路径 + add_includedirs("Extra2D/include", {public = true}) + add_includedirs("Tests") + + -- 依赖引擎库 + add_deps("extra2d") + + -- 平台配置 + local plat = get_config("plat") or os.host() + + if plat == "mingw" or plat == "windows" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_syslinks("winmm", "imm32", "version", "setupapi") + elseif plat == "linux" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_syslinks("dl", "pthread") + elseif plat == "macosx" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_frameworks("Cocoa", "IOKit", "CoreVideo") + end + + -- 编译器标志 + add_cxflags("-Wall", "-Wextra", {force = true}) + add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true}) + + if is_mode("debug") then + add_defines("_DEBUG") + add_cxxflags("-O0", "-g", {force = true}) + else + add_defines("NDEBUG") + add_cxxflags("-O2", {force = true}) + end +target_end() + +-- ============================================== +-- 资源打包工具 +-- ============================================== + +target("asset_packer") + set_kind("binary") + set_default(false) + + -- 源文件 + add_files("Tools/asset_packer.cpp") + + -- 头文件路径 + add_includedirs("Extra2D/include") + + -- 依赖引擎库 + add_deps("extra2d") + + -- 平台配置 + local plat = get_config("plat") or os.host() + + if plat == "mingw" or plat == "windows" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_syslinks("winmm", "imm32", "version", "setupapi") + elseif plat == "linux" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_syslinks("dl", "pthread") + elseif plat == "macosx" then + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") + add_frameworks("Cocoa", "IOKit", "CoreVideo") + end + + -- 编译器标志 + add_cxflags("-Wall", "-Wextra", {force = true}) + add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true}) + + if is_mode("debug") then + add_defines("_DEBUG") + add_cxxflags("-O0", "-g", {force = true}) + else + add_defines("NDEBUG") + add_cxxflags("-O2", {force = true}) + end +target_end() diff --git a/xmake/engine.lua b/xmake/engine.lua index 0252544..e55683d 100644 --- a/xmake/engine.lua +++ b/xmake/engine.lua @@ -39,18 +39,21 @@ function define_extra2d_engine() add_syslinks("glfw", "nx", "m", {public = true}) elseif plat == "mingw" or plat == "windows" then -- Windows (MinGW) 平台配置 - add_packages("glm", "nlohmann_json", "glfw", {public = true}) + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_syslinks("winmm", "imm32", "version", "setupapi", {public = true}) elseif plat == "linux" then -- Linux 平台配置 - add_packages("glm", "nlohmann_json", "glfw", {public = true}) + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_syslinks("dl", "pthread", {public = true}) elseif plat == "macosx" then -- macOS 平台配置 - add_packages("glm", "nlohmann_json", "glfw", {public = true}) + add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_frameworks("Cocoa", "IOKit", "CoreVideo", {public = true}) end + -- 启用压缩库 + add_defines("E2D_USE_ZSTD", "E2D_USE_LZ4", "E2D_USE_ZLIB", {public = true}) + -- 编译器标志 (C 和 C++ 共用) add_cxflags("-Wall", "-Wextra", {force = true}) add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})