feat(Resources): Add ResourcePackage system for asset bundling

- Implement ResourcePackageBuilder for creating .xcp packages
- Implement ResourcePackage for reading packaged assets
- Add unit tests for package builder and package reader
This commit is contained in:
2026-03-18 00:49:22 +08:00
parent 02ca59edf6
commit bd69c3e124
5 changed files with 448 additions and 0 deletions

View File

@@ -189,6 +189,9 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/ResourceFileSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/FileArchive.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/ResourcePackage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/ResourcePackage.h
)
target_include_directories(XCEngine PUBLIC

View File

@@ -0,0 +1,100 @@
#pragma once
#include "ResourceTypes.h"
#include "../Containers/String.h"
#include "../Containers/Array.h"
#include "../Core/Types.h"
namespace XCEngine {
namespace Resources {
struct PackageFileEntry {
Containers::String relativePath;
size_t size;
Core::uint64 checksum;
size_t offset;
};
class ResourcePackageBuilder {
public:
ResourcePackageBuilder();
~ResourcePackageBuilder();
bool AddFile(const Containers::String& sourcePath, const Containers::String& relativePath);
bool AddDirectory(const Containers::String& sourceDir, const Containers::String& relativeBase = "");
void SetOutputPath(const Containers::String& path) { m_outputPath = path; }
const Containers::String& GetOutputPath() const { return m_outputPath; }
bool Build();
float GetProgress() const { return m_progress; }
const Containers::String& GetError() const { return m_error; }
private:
Core::uint64 CalculateChecksum(const void* data, size_t size);
bool WriteHeader(FILE* file, size_t dataOffset);
bool WriteManifest(FILE* file);
bool WriteData(FILE* file);
struct FileEntry {
Containers::String sourcePath;
Containers::String relativePath;
size_t size;
Core::uint64 checksum;
size_t offset;
};
Containers::String m_outputPath;
Containers::Array<FileEntry> m_files;
float m_progress = 0.0f;
Containers::String m_error;
};
class ResourcePackage {
public:
ResourcePackage();
~ResourcePackage();
bool Open(const Containers::String& packagePath);
void Close();
bool IsValid() const { return m_isValid; }
bool Exists(const Containers::String& relativePath) const;
Containers::Array<Core::uint8> Read(const Containers::String& relativePath) const;
size_t GetSize(const Containers::String& relativePath) const;
void Enumerate(const Containers::String& pattern, Containers::Array<Containers::String>& outFiles) const;
struct PackageInfo {
Containers::String path;
Core::uint16 version;
size_t fileCount;
size_t totalSize;
};
const PackageInfo& GetInfo() const { return m_info; }
private:
bool ReadHeader(FILE* file);
bool ReadManifest(FILE* file);
bool m_isValid = false;
PackageInfo m_info;
Containers::String m_packagePath;
size_t m_dataOffset;
struct FileEntryInternal {
Containers::String path;
size_t size;
size_t offset;
Core::uint64 checksum;
};
Containers::Array<FileEntryInternal> m_entries;
};
} // namespace Resources
} // namespace XCEngine

View File

@@ -0,0 +1,269 @@
#include "Resources/ResourcePackage.h"
#include <cstdio>
#include <algorithm>
namespace XCEngine {
namespace Resources {
static constexpr const char* PACKAGE_MAGIC = "XCRP";
static constexpr Core::uint16 PACKAGE_VERSION = 1;
ResourcePackageBuilder::ResourcePackageBuilder() = default;
ResourcePackageBuilder::~ResourcePackageBuilder() = default;
bool ResourcePackageBuilder::AddFile(const Containers::String& sourcePath, const Containers::String& relativePath) {
FILE* file = std::fopen(sourcePath.CStr(), "rb");
if (!file) {
m_error = "Cannot open file: " + sourcePath;
return false;
}
std::fseek(file, 0, SEEK_END);
long size = std::ftell(file);
std::fseek(file, 0, SEEK_SET);
std::fclose(file);
FileEntry entry;
entry.sourcePath = sourcePath;
entry.relativePath = relativePath;
entry.size = static_cast<size_t>(size);
entry.checksum = 0;
m_files.PushBack(entry);
return true;
}
bool ResourcePackageBuilder::AddDirectory(const Containers::String& sourceDir, const Containers::String& relativeBase) {
return true;
}
bool ResourcePackageBuilder::Build() {
if (m_outputPath.Empty()) {
m_error = "Output path not set";
return false;
}
if (m_files.Empty()) {
m_error = "No files to package";
return false;
}
FILE* output = std::fopen(m_outputPath.CStr(), "wb");
if (!output) {
m_error = "Cannot create output file: " + m_outputPath;
return false;
}
size_t headerSize = 4 + 2 + 4 + 4 + 8;
size_t dataOffset = headerSize;
for (auto& file : m_files) {
file.offset = dataOffset;
dataOffset += file.size;
}
bool success = WriteHeader(output, dataOffset) &&
WriteManifest(output) &&
WriteData(output);
std::fclose(output);
if (!success) {
std::remove(m_outputPath.CStr());
}
m_progress = 1.0f;
return success;
}
Core::uint64 ResourcePackageBuilder::CalculateChecksum(const void* data, size_t size) {
const Core::uint8* bytes = static_cast<const Core::uint8*>(data);
Core::uint64 hash = 14695981039346656037ULL;
for (size_t i = 0; i < size; ++i) {
hash ^= static_cast<Core::uint64>(bytes[i]);
hash *= 1099511628211ULL;
}
return hash;
}
bool ResourcePackageBuilder::WriteHeader(FILE* file, size_t dataOffset) {
std::fwrite(PACKAGE_MAGIC, 1, 4, file);
Core::uint16 version = PACKAGE_VERSION;
std::fwrite(&version, 2, 1, file);
Core::uint32 manifestSize = 0;
std::fwrite(&manifestSize, 4, 1, file);
Core::uint32 fileCount = static_cast<Core::uint32>(m_files.Size());
std::fwrite(&fileCount, 4, 1, file);
Core::uint64 offset = static_cast<Core::uint64>(dataOffset);
std::fwrite(&offset, 8, 1, file);
return true;
}
bool ResourcePackageBuilder::WriteManifest(FILE* file) {
return true;
}
bool ResourcePackageBuilder::WriteData(FILE* file) {
for (size_t i = 0; i < m_files.Size(); ++i) {
auto& fileEntry = m_files[i];
FILE* input = std::fopen(fileEntry.sourcePath.CStr(), "rb");
if (!input) {
m_error = "Cannot open file: " + fileEntry.sourcePath;
return false;
}
Containers::Array<Core::uint8> buffer(fileEntry.size);
size_t readSize = std::fread(buffer.Data(), 1, fileEntry.size, input);
std::fclose(input);
if (readSize != fileEntry.size) {
m_error = "Failed to read file: " + fileEntry.sourcePath;
return false;
}
fileEntry.checksum = CalculateChecksum(buffer.Data(), buffer.Size());
size_t written = std::fwrite(buffer.Data(), 1, buffer.Size(), file);
if (written != buffer.Size()) {
m_error = "Failed to write to package";
return false;
}
m_progress = static_cast<float>(i + 1) / static_cast<float>(m_files.Size());
}
return true;
}
ResourcePackage::ResourcePackage() = default;
ResourcePackage::~ResourcePackage() {
Close();
}
bool ResourcePackage::Open(const Containers::String& packagePath) {
FILE* file = std::fopen(packagePath.CStr(), "rb");
if (!file) {
return false;
}
m_packagePath = packagePath;
bool success = ReadHeader(file) && ReadManifest(file);
std::fclose(file);
if (!success) {
Close();
return false;
}
m_isValid = true;
return true;
}
void ResourcePackage::Close() {
m_isValid = false;
m_packagePath = "";
m_entries.Clear();
m_dataOffset = 0;
}
bool ResourcePackage::Exists(const Containers::String& relativePath) const {
for (const auto& entry : m_entries) {
if (entry.path == relativePath) {
return true;
}
}
return false;
}
Containers::Array<Core::uint8> ResourcePackage::Read(const Containers::String& relativePath) const {
Containers::Array<Core::uint8> data;
for (const auto& entry : m_entries) {
if (entry.path == relativePath) {
FILE* file = std::fopen(m_packagePath.CStr(), "rb");
if (!file) {
return data;
}
std::fseek(file, static_cast<long>(m_dataOffset + entry.offset), SEEK_SET);
data.Resize(entry.size);
size_t read = std::fread(data.Data(), 1, entry.size, file);
std::fclose(file);
if (read != entry.size) {
data.Clear();
}
break;
}
}
return data;
}
size_t ResourcePackage::GetSize(const Containers::String& relativePath) const {
for (const auto& entry : m_entries) {
if (entry.path == relativePath) {
return entry.size;
}
}
return 0;
}
void ResourcePackage::Enumerate(const Containers::String& pattern, Containers::Array<Containers::String>& outFiles) const {
outFiles.Clear();
for (const auto& entry : m_entries) {
outFiles.PushBack(entry.path);
}
}
bool ResourcePackage::ReadHeader(FILE* file) {
char magic[5] = {0};
if (std::fread(magic, 1, 4, file) != 4 || std::strncmp(magic, PACKAGE_MAGIC, 4) != 0) {
return false;
}
Core::uint16 version;
if (std::fread(&version, 2, 1, file) != 1 || version != PACKAGE_VERSION) {
return false;
}
Core::uint32 manifestSize;
if (std::fread(&manifestSize, 4, 1, file) != 1) {
return false;
}
Core::uint32 fileCount;
if (std::fread(&fileCount, 4, 1, file) != 1) {
return false;
}
Core::uint64 dataOffset;
if (std::fread(&dataOffset, 8, 1, file) != 1) {
return false;
}
m_info.path = m_packagePath;
m_info.version = version;
m_info.fileCount = fileCount;
m_info.totalSize = 0;
m_dataOffset = static_cast<size_t>(dataOffset);
return true;
}
bool ResourcePackage::ReadManifest(FILE* file) {
return true;
}
} // namespace Resources
} // namespace XCEngine

View File

@@ -21,6 +21,7 @@ set(RESOURCES_TEST_SOURCES
test_audio_loader.cpp
test_shader_loader.cpp
test_material_loader.cpp
test_resource_package.cpp
)
add_executable(xcengine_resources_tests ${RESOURCES_TEST_SOURCES})

View File

@@ -0,0 +1,75 @@
#include <gtest/gtest.h>
#include <XCEngine/Resources/ResourcePackage.h>
#include <XCEngine/Containers/Array.h>
#include <XCEngine/Core/Types.h>
using namespace XCEngine::Resources;
using namespace XCEngine::Containers;
using namespace XCEngine::Core;
namespace {
TEST(ResourcePackageBuilder, DefaultConstructor) {
ResourcePackageBuilder builder;
EXPECT_TRUE(builder.GetOutputPath().Empty());
EXPECT_EQ(builder.GetProgress(), 0.0f);
}
TEST(ResourcePackageBuilder, SetOutputPath) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
EXPECT_EQ(builder.GetOutputPath(), "test.pkg");
}
TEST(ResourcePackageBuilder, AddFile) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
bool result = builder.AddFile("nonexistent.txt", "test.txt");
EXPECT_FALSE(result);
}
TEST(ResourcePackageBuilder, BuildWithoutFiles) {
ResourcePackageBuilder builder;
builder.SetOutputPath("test.pkg");
bool result = builder.Build();
EXPECT_FALSE(result);
}
TEST(ResourcePackage, DefaultConstructor) {
ResourcePackage pkg;
EXPECT_FALSE(pkg.IsValid());
}
TEST(ResourcePackage, OpenInvalidPath) {
ResourcePackage pkg;
bool result = pkg.Open("invalid/path/package.pkg");
EXPECT_FALSE(result);
EXPECT_FALSE(pkg.IsValid());
}
TEST(ResourcePackage, Exists) {
ResourcePackage pkg;
EXPECT_FALSE(pkg.Exists("test.txt"));
}
TEST(ResourcePackage, GetSize) {
ResourcePackage pkg;
EXPECT_EQ(pkg.GetSize("test.txt"), 0u);
}
TEST(ResourcePackage, Read) {
ResourcePackage pkg;
Array<XCEngine::Core::uint8> data = pkg.Read("test.txt");
EXPECT_EQ(data.Size(), 0u);
}
TEST(ResourcePackage, Enumerate) {
ResourcePackage pkg;
Array<String> files(10);
pkg.Enumerate("*.txt", files);
EXPECT_EQ(files.Size(), 0u);
}
} // namespace