commit eb49ffa4bdaf70b4fe57c163a5f950cf6c6a67f8 Author: ShadesAndGrays Date: Fri Jul 10 10:51:48 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca733b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +asset/ + diff --git a/makefile b/makefile new file mode 100644 index 0000000..ce1237f --- /dev/null +++ b/makefile @@ -0,0 +1,54 @@ +.PHONY: all build clean lib + +CXX = g++ +WSL = wsl --shell-type login +EMXX = $(WSL) em++ +EMAR = $(WSL) emar + +CXXFLAGS = -std=c++20 -O2 + +SRC_FILES = ./src/main.cpp \ + ./src/lib.cpp + + +LIB_FILES = ./src/lib.cpp + +all: build-lib build + +run: build + ./bin/assetpacker.exe + +build-lib: + @mkdir -p bin + @mkdir -p bin/include/asset_packer + @cp -r ./src/include/* ./bin/include/asset_packer + $(CXX) $(CXXFLAGS) -I./src/include -c $(LIB_FILES) -o ./bin/asset_packer.o + ar rcs ./bin/libassetpacker.a ./bin/asset_packer.o + +build-lib-web: + @mkdir -p bin + @mkdir -p bin/include/asset_packer + @cp -r ./src/include/* ./bin/include/asset_packer + $(EMXX) $(CXXFLAGS) -I./src/include -c $(LIB_FILES) -o ./bin/asset_packer.web.o + $(EMAR) rcs ./bin/libassetpacker.web.a ./bin/asset_packer.web.o + +build: + @mkdir -p bin + @cp -r ./asset/ ./bin/ + $(CXX) $(CXXFLAGS) $(SRC_FILES) -I./src/include -o ./bin/assetpacker.exe + +compile_commands.json: clean + @echo "Generating compile_commands.json..." + bear -- $(MAKE) + +clean: + @rm -r -f ./bin + @rm -f compile_commands.json + +test: + @mkdir -p bin + @cp -r ./asset/ ./bin/stuff/ + $(CXX) $(CXXFLAGS) -I./src/include $(LIB_FILES) ./src/test.cpp -o ./bin/test.exe + ./bin/test.exe + + diff --git a/src/include/asset.hpp b/src/include/asset.hpp new file mode 100644 index 0000000..876a235 --- /dev/null +++ b/src/include/asset.hpp @@ -0,0 +1,33 @@ +// asset.hpp +#pragma once + +#include +#include +#include +#include + +// Struct size is exactly 80 bytes +struct AssetMetadata { + char name[64]; // Fixed size string for the filename + uint64_t offset; // Where the raw file bytes begin in the blob + uint64_t size; // How many bytes long the file is +}; + +class AssetReader{ + private: + std::string blob_path; + std::unordered_map index_table; + public: + AssetReader(const std::string& path): blob_path(path){} + ~AssetReader() = default; + bool load_index(); + std::vector extract_asset(const std::string& asset_name); + +}; + +class AssetPacker{ + public: + AssetPacker() = default; + ~AssetPacker() = default; + void pack_assets(const std::string& output_blob_path, const std::vector& files_to_pack); +}; diff --git a/src/lib.cpp b/src/lib.cpp new file mode 100644 index 0000000..2a89b1f --- /dev/null +++ b/src/lib.cpp @@ -0,0 +1,104 @@ +// lib.cpp +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +bool AssetReader::load_index(){ + std::ifstream blob(blob_path,std::ios::binary); + if(!blob) return false; + + uint32_t file_count = 0; + blob.read(reinterpret_cast(&file_count),sizeof(file_count)); + for (uint32_t i = 0; i < file_count; i++){ + AssetMetadata meta; + blob.read(reinterpret_cast(&meta), sizeof(AssetMetadata)); + index_table[std::string(meta.name)] = meta; + } + + blob.close(); + + return true; +} + +std::vector AssetReader::extract_asset(const std::string& asset_name){ + if (!index_table.contains(asset_name)){ + std::cerr << "Asset not found in blob: " << asset_name << "\n"; + return {}; + } + AssetMetadata meta = index_table[asset_name]; + std::ifstream blob(blob_path,std::ios::binary); + + blob.seekg(meta.offset); + std::vector buffer(meta.size); + blob.read(reinterpret_cast(buffer.data()),meta.size); + blob.close(); + + return buffer; +} + + + +void AssetPacker::pack_assets(const std::string& output_blob_path, const std::vector& files_to_pack){ + std::ofstream blob(output_blob_path,std::ios::binary); + if (!blob){std::cerr << "Failed to create blob file!" << std::endl; return;} + + uint32_t file_count = files_to_pack.size(); + blob.write(reinterpret_cast(&file_count), sizeof(file_count)); + // Write how many files there are + + + uint64_t current_data_offset = sizeof(file_count) + (file_count * sizeof(AssetMetadata)); // create and offset pointer for the number of file and our files metadata + + std::vector metadata_table; + + for (auto &file_path : files_to_pack){ + + std::ifstream file(file_path,std::ios::binary | std::ios::ate); + + if (!file){std::cerr << "Skipping missing file " << file_path << std::endl; continue; } + + uint64_t file_size = file.tellg(); + + // create metadata + AssetMetadata meta{}; + size_t length = file_path.copy(meta.name, sizeof(meta.name) - 1); + meta.name[length] = '\0'; // Explicitly guarantee null-termination + + meta.offset = current_data_offset; + meta.size = file_size; + + metadata_table.push_back(meta); + current_data_offset += file_size; + + file.close(); + } + + // write meta data + for (const auto& meta : metadata_table) { + blob.write(reinterpret_cast(&meta), sizeof(AssetMetadata)); + } + + // second pass + for (const auto& file_path: files_to_pack){ + std::ifstream file (file_path,std::ios::binary); + if (!file) continue; + blob << file.rdbuf(); + file.close(); + } + + for (auto meta :metadata_table){ + std:: cout << std::format("stored {}",meta.name) << std::endl; + } + blob.close(); + std::cout << "Successfully packed " << metadata_table.size() << " files into " << output_blob_path << "\n"; + +} + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..03a57f1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,49 @@ +#include + +#include +#include +#include + +namespace fs = std::filesystem; + +int main(int argc, const char** argv){ + + std::string asset_folder_path; + std::string blob_path; + + if (argc == 1){ + std::cerr << argc-1 << " arguments found " << std::endl; + std::cerr << "Format: ./asset_packer ASSET_FOLDER_PATH [BLOB_PATH]" << std::endl; + return -1; + } + + if (std::string(argv[1]) == "--help" ||std::string(argv[1]) == "--help" ){ + std::cerr << "Format: ./asset_packer ASSET_FOLDER_PATH [BLOB_PATH] " << std::endl; + return -1; + } + + if (argc == 2){ + asset_folder_path = argv[1]; + blob_path = "data.dat"; + } + + else if (argc == 3){ + blob_path = argv[2]; + } + + std::vector files_to_pack = {}; + + fs::path target_path = asset_folder_path; + if (fs::exists(target_path) && fs::is_directory(target_path)){ + for (const auto& entry : fs::directory_iterator(target_path)){ + if(entry.is_regular_file()){ + files_to_pack.push_back(entry.path().relative_path().string()); + } + } + } + + AssetPacker asset_packer; + asset_packer.pack_assets(blob_path, files_to_pack); + + return 0; +} diff --git a/src/test.cpp b/src/test.cpp new file mode 100644 index 0000000..b14ef8f --- /dev/null +++ b/src/test.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include +#include + +// Include your library headers here +#include + +void run_asset_pipeline_tests() { + std::cout << "[TEST] Initializing Asset Pipeline Tests...\n"; + + // Setup temporary testing files + std::string test_blob = "test_data.dat"; + std::string text_file = "test_config.json"; + std::string fake_png = "test_player.png"; + + // 1. Create dummy files on disk to pack + std::ofstream f1(text_file); + f1 << "{ \"speed\": 250 }"; + f1.close(); + + std::ofstream f2(fake_png, std::ios::binary); + f2 << "FAKE_PNG_BINARY_DATA_HEADER"; + f2.close(); + + // ------------------------------------------------------------- + // TEST 1: Packing Verification + // ------------------------------------------------------------- + AssetPacker packer; + std::vector files_to_pack = { text_file, fake_png }; + + // This should run without throwing errors or truncating files + packer.pack_assets(test_blob, files_to_pack); + assert(std::filesystem::exists(test_blob) && "FAIL: Blob file was not created!"); + + // ------------------------------------------------------------- + // TEST 2: Reading and Integrity Verification (Round-Trip) + // ------------------------------------------------------------- + AssetReader reader(test_blob); + bool index_loaded = reader.load_index(); + assert(index_loaded && "FAIL: Reader could not parse blob index!"); + + // Extract the JSON back out + std::vector extracted_json_bytes = reader.extract_asset(text_file); + std::string extracted_json(extracted_json_bytes.begin(), extracted_json_bytes.end()); + + // Assert that the file contents match exactly what we put in + assert(extracted_json == "{ \"speed\": 250 }" && "FAIL: Extracted data does not match original data!"); + std::cout << " -> Pass: Data round-trip integrity verified.\n"; + + // ------------------------------------------------------------- + // TEST 3: Graceful Missing Asset Handling + // ------------------------------------------------------------- + std::vector missing = reader.extract_asset("non_existent_file.png"); + assert(missing.empty() && "FAIL: Requesting a missing file should return an empty buffer!"); + std::cout << " -> Pass: Missing file boundaries handled gracefully.\n"; + + // Clean up test file clutter from the hard drive + std::filesystem::remove(text_file); + std::filesystem::remove(fake_png); + std::filesystem::remove(test_blob); + + std::cout << "[TEST] ALL ASSET PIPELINE TESTS PASSED SUCCESSFULY!\n\n"; +} + +int main() { + run_asset_pipeline_tests(); + return 0; +}