diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 2bdcfc32..58fedf9b 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -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 diff --git a/engine/include/XCEngine/Resources/ResourcePackage.h b/engine/include/XCEngine/Resources/ResourcePackage.h new file mode 100644 index 00000000..c8812f2b --- /dev/null +++ b/engine/include/XCEngine/Resources/ResourcePackage.h @@ -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 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 Read(const Containers::String& relativePath) const; + size_t GetSize(const Containers::String& relativePath) const; + + void Enumerate(const Containers::String& pattern, Containers::Array& 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 m_entries; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Resources/ResourcePackage.cpp b/engine/src/Resources/ResourcePackage.cpp new file mode 100644 index 00000000..9e9d4105 --- /dev/null +++ b/engine/src/Resources/ResourcePackage.cpp @@ -0,0 +1,269 @@ +#include "Resources/ResourcePackage.h" +#include +#include + +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); + 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(data); + Core::uint64 hash = 14695981039346656037ULL; + + for (size_t i = 0; i < size; ++i) { + hash ^= static_cast(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(m_files.Size()); + std::fwrite(&fileCount, 4, 1, file); + + Core::uint64 offset = static_cast(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 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(i + 1) / static_cast(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 ResourcePackage::Read(const Containers::String& relativePath) const { + Containers::Array 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(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& 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(dataOffset); + + return true; +} + +bool ResourcePackage::ReadManifest(FILE* file) { + return true; +} + +} // namespace Resources +} // namespace XCEngine diff --git a/tests/Resources/CMakeLists.txt b/tests/Resources/CMakeLists.txt index 5e58e6b9..ca2537af 100644 --- a/tests/Resources/CMakeLists.txt +++ b/tests/Resources/CMakeLists.txt @@ -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}) diff --git a/tests/Resources/test_resource_package.cpp b/tests/Resources/test_resource_package.cpp new file mode 100644 index 00000000..d369de95 --- /dev/null +++ b/tests/Resources/test_resource_package.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include + +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 data = pkg.Read("test.txt"); + EXPECT_EQ(data.Size(), 0u); +} + +TEST(ResourcePackage, Enumerate) { + ResourcePackage pkg; + Array files(10); + pkg.Enumerate("*.txt", files); + EXPECT_EQ(files.Size(), 0u); +} + +} // namespace