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:
@@ -189,6 +189,9 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioLoader.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/ResourceFileSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/ResourceFileSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/FileArchive.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
|
target_include_directories(XCEngine PUBLIC
|
||||||
|
|||||||
100
engine/include/XCEngine/Resources/ResourcePackage.h
Normal file
100
engine/include/XCEngine/Resources/ResourcePackage.h
Normal 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
|
||||||
269
engine/src/Resources/ResourcePackage.cpp
Normal file
269
engine/src/Resources/ResourcePackage.cpp
Normal 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
|
||||||
@@ -21,6 +21,7 @@ set(RESOURCES_TEST_SOURCES
|
|||||||
test_audio_loader.cpp
|
test_audio_loader.cpp
|
||||||
test_shader_loader.cpp
|
test_shader_loader.cpp
|
||||||
test_material_loader.cpp
|
test_material_loader.cpp
|
||||||
|
test_resource_package.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(xcengine_resources_tests ${RESOURCES_TEST_SOURCES})
|
add_executable(xcengine_resources_tests ${RESOURCES_TEST_SOURCES})
|
||||||
|
|||||||
75
tests/Resources/test_resource_package.cpp
Normal file
75
tests/Resources/test_resource_package.cpp
Normal 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
|
||||||
Reference in New Issue
Block a user