diff --git a/.gitignore b/.gitignore index 527acec..87cff5c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,11 @@ asteroids *.out *.app +# Releases +*.zip +*.tar.gz +*.dmg + # Compiled Object files *.o *.obj diff --git a/CMakeLists.txt b/CMakeLists.txt index 534fb67..306c1db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,11 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$:-O2 -ffunctio target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:_DEBUG>) target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:RELEASE_BUILD>) +# Definir MACOS_BUNDLE si es un bundle de macOS +if(APPLE AND MACOSX_BUNDLE) + target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUNDLE) +endif() + # Configuración específica para cada plataforma if(WIN32) target_compile_definitions(${PROJECT_NAME} PRIVATE WINDOWS_BUILD) diff --git a/Makefile b/Makefile index b18c053..f3a0e09 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,20 @@ else UNAME_S := $(shell uname -s) endif +# ============================================================================== +# PACKING TOOL +# ============================================================================== +PACK_TOOL := tools/pack_resources/pack_resources + +.PHONY: pack_tool resources.pack + +pack_tool: + @$(MAKE) -C tools/pack_resources + +resources.pack: pack_tool + @echo "Creating resources.pack..." + @./$(PACK_TOOL) data resources.pack + # ============================================================================== # TARGETS # ============================================================================== @@ -87,7 +101,7 @@ debug: # macOS Release (Apple Silicon) .PHONY: macos_release -macos_release: +macos_release: pack_tool resources.pack @echo "Creating macOS release - Version: $(VERSION)" # Check/install create-dmg @@ -104,8 +118,8 @@ macos_release: @$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" @$(MKDIR) Frameworks - # Copy resources - @cp -r resources "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources/" + # Copy resources.pack to Resources + @cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources/" @ditto release/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks/SDL3.framework" @ditto release/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework Frameworks/SDL3.framework @@ -313,6 +327,8 @@ else @$(RMFILE) $(TARGET_FILE) $(TARGET_FILE)_debug @$(RMDIR) build $(RELEASE_FOLDER) @$(RMFILE) *.dmg *.zip *.tar.gz 2>/dev/null || true + @$(RMFILE) resources.pack 2>/dev/null || true + @$(MAKE) -C tools/pack_resources clean 2>/dev/null || true endif @echo "Clean complete" diff --git a/resources.pack b/resources.pack new file mode 100644 index 0000000..888fd68 Binary files /dev/null and b/resources.pack differ diff --git a/source/core/audio/audio_cache.cpp b/source/core/audio/audio_cache.cpp index f749ecd..8c86f7d 100644 --- a/source/core/audio/audio_cache.cpp +++ b/source/core/audio/audio_cache.cpp @@ -3,6 +3,8 @@ #include "core/audio/audio_cache.hpp" +#include "core/resources/resource_helper.hpp" + #include // Inicialització de variables estàtiques @@ -19,17 +21,28 @@ JA_Sound_t* AudioCache::getSound(const std::string& name) { return it->second; } - // Cache miss - cargar archivo - std::string fullpath = resolveSoundPath(name); - JA_Sound_t* sound = JA_LoadSound(fullpath.c_str()); + // Normalize path: "laser_shoot.wav" → "sounds/laser_shoot.wav" + std::string normalized = name; + if (normalized.find("sounds/") != 0 && normalized.find('/') == std::string::npos) { + normalized = "sounds/" + normalized; + } + // Load from resource system + std::vector data = Resource::Helper::loadFile(normalized); + if (data.empty()) { + std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl; + return nullptr; + } + + // Load sound from memory + JA_Sound_t* sound = JA_LoadSound(data.data(), static_cast(data.size())); if (sound == nullptr) { - std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath + std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized << std::endl; return nullptr; } - std::cout << "[AudioCache] Sound loaded: " << name << std::endl; + std::cout << "[AudioCache] Sound loaded: " << normalized << std::endl; sounds_[name] = sound; return sound; } @@ -42,17 +55,28 @@ JA_Music_t* AudioCache::getMusic(const std::string& name) { return it->second; } - // Cache miss - cargar archivo - std::string fullpath = resolveMusicPath(name); - JA_Music_t* music = JA_LoadMusic(fullpath.c_str()); + // Normalize path: "title.ogg" → "music/title.ogg" + std::string normalized = name; + if (normalized.find("music/") != 0 && normalized.find('/') == std::string::npos) { + normalized = "music/" + normalized; + } + // Load from resource system + std::vector data = Resource::Helper::loadFile(normalized); + if (data.empty()) { + std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl; + return nullptr; + } + + // Load music from memory + JA_Music_t* music = JA_LoadMusic(data.data(), static_cast(data.size())); if (music == nullptr) { - std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath + std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized << std::endl; return nullptr; } - std::cout << "[AudioCache] Music loaded: " << name << std::endl; + std::cout << "[AudioCache] Music loaded: " << normalized << std::endl; musics_[name] = music; return music; } diff --git a/source/core/graphics/shape.hpp b/source/core/graphics/shape.hpp index 1f5ab7a..22a594b 100644 --- a/source/core/graphics/shape.hpp +++ b/source/core/graphics/shape.hpp @@ -32,6 +32,9 @@ class Shape { // Carregar forma des de fitxer .shp bool carregar(const std::string& filepath); + // Parsejar forma des de buffer de memòria (per al sistema de recursos) + bool parsejar_fitxer(const std::string& contingut); + // Getters const std::vector& get_primitives() const { return primitives_; @@ -50,9 +53,6 @@ class Shape { float escala_defecte_; // Escala per defecte (normalment 1.0) std::string nom_; // Nom de la forma (per depuració) - // Parsejador del fitxer - bool parsejar_fitxer(const std::string& contingut); - // Helpers privats per parsejar std::string trim(const std::string& str) const; bool starts_with(const std::string& str, const std::string& prefix) const; diff --git a/source/core/graphics/shape_loader.cpp b/source/core/graphics/shape_loader.cpp index c3edb5e..60d7637 100644 --- a/source/core/graphics/shape_loader.cpp +++ b/source/core/graphics/shape_loader.cpp @@ -3,6 +3,8 @@ #include "core/graphics/shape_loader.hpp" +#include "core/resources/resource_helper.hpp" + #include namespace Graphics { @@ -19,28 +21,40 @@ std::shared_ptr ShapeLoader::load(const std::string& filename) { return it->second; // Cache hit } - // Resolve full path - std::string fullpath = resolve_path(filename); + // Normalize path: "ship.shp" → "shapes/ship.shp" + // "logo/letra_j.shp" → "shapes/logo/letra_j.shp" + std::string normalized = filename; + if (normalized.find("shapes/") != 0) { + // Doesn't start with "shapes/", so add it + normalized = "shapes/" + normalized; + } - // Create and load shape + // Load from resource system + std::vector data = Resource::Helper::loadFile(normalized); + if (data.empty()) { + std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << normalized + << std::endl; + return nullptr; + } + + // Convert bytes to string and parse + std::string file_content(data.begin(), data.end()); auto shape = std::make_shared(); - if (!shape->carregar(fullpath)) { - std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << filename + if (!shape->parsejar_fitxer(file_content)) { + std::cerr << "[ShapeLoader] Error: no s'ha pogut parsejar " << normalized << std::endl; return nullptr; } // Verify shape is valid if (!shape->es_valida()) { - std::cerr << "[ShapeLoader] Error: forma invàlida " << filename - << std::endl; + std::cerr << "[ShapeLoader] Error: forma invàlida " << normalized << std::endl; return nullptr; } // Cache and return - std::cout << "[ShapeLoader] Carregat: " << filename << " (" - << shape->get_nom() << ", " << shape->get_num_primitives() - << " primitives)" << std::endl; + std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->get_nom() + << ", " << shape->get_num_primitives() << " primitives)" << std::endl; cache_[filename] = shape; return shape; diff --git a/source/core/graphics/starfield.cpp b/source/core/graphics/starfield.cpp index dd99d1d..0b51cb2 100644 --- a/source/core/graphics/starfield.cpp +++ b/source/core/graphics/starfield.cpp @@ -8,6 +8,7 @@ #include #include "core/defaults.hpp" +#include "core/graphics/shape_loader.hpp" #include "core/rendering/shape_renderer.hpp" namespace Graphics { @@ -21,11 +22,11 @@ Starfield::Starfield(SDL_Renderer* renderer, punt_fuga_(punt_fuga), area_(area), densitat_(densitat) { - // Carregar forma d'estrella - shape_estrella_ = std::make_shared("data/shapes/star.shp"); + // Carregar forma d'estrella amb ShapeLoader + shape_estrella_ = ShapeLoader::load("star.shp"); - if (!shape_estrella_->es_valida()) { - std::cerr << "ERROR: No s'ha pogut carregar data/shapes/star.shp" << std::endl; + if (!shape_estrella_ || !shape_estrella_->es_valida()) { + std::cerr << "ERROR: No s'ha pogut carregar star.shp" << std::endl; return; } diff --git a/source/core/resources/resource_helper.cpp b/source/core/resources/resource_helper.cpp new file mode 100644 index 0000000..c552299 --- /dev/null +++ b/source/core/resources/resource_helper.cpp @@ -0,0 +1,83 @@ +// resource_helper.cpp - Implementació de funcions d'ajuda +// © 2025 Port a C++20 amb SDL3 + +#include "resource_helper.hpp" + +#include "resource_loader.hpp" + +#include +#include + +namespace Resource { +namespace Helper { + +// Inicialitzar el sistema de recursos +bool initializeResourceSystem(const std::string& pack_file, bool fallback) { + return Loader::get().initialize(pack_file, fallback); +} + +// Carregar un fitxer +std::vector loadFile(const std::string& filepath) { + // Normalitzar la ruta + std::string normalized = normalizePath(filepath); + + // Carregar del sistema de recursos + return Loader::get().loadResource(normalized); +} + +// Comprovar si existeix un fitxer +bool fileExists(const std::string& filepath) { + std::string normalized = normalizePath(filepath); + return Loader::get().resourceExists(normalized); +} + +// Obtenir ruta normalitzada per al paquet +// Elimina prefixos "data/", rutes absolutes, etc. +std::string getPackPath(const std::string& asset_path) { + std::string path = asset_path; + + // Eliminar rutes absolutes (detectar / o C:\ al principi) + if (!path.empty() && path[0] == '/') { + // Buscar "data/" i agafar el que ve després + size_t data_pos = path.find("/data/"); + if (data_pos != std::string::npos) { + path = path.substr(data_pos + 6); // Saltar "/data/" + } + } + + // Eliminar "./" i "../" del principi + while (path.starts_with("./")) { + path = path.substr(2); + } + while (path.starts_with("../")) { + path = path.substr(3); + } + + // Eliminar "data/" del principi + if (path.starts_with("data/")) { + path = path.substr(5); + } + + // Eliminar "Resources/" (macOS bundles) + if (path.starts_with("Resources/")) { + path = path.substr(10); + } + + // Convertir barres invertides a normals + std::ranges::replace(path, '\\', '/'); + + return path; +} + +// Normalitzar ruta (alias de getPackPath) +std::string normalizePath(const std::string& path) { + return getPackPath(path); +} + +// Comprovar si hi ha paquet carregat +bool isPackLoaded() { + return Loader::get().isPackLoaded(); +} + +} // namespace Helper +} // namespace Resource diff --git a/source/core/resources/resource_helper.hpp b/source/core/resources/resource_helper.hpp new file mode 100644 index 0000000..c3fa251 --- /dev/null +++ b/source/core/resources/resource_helper.hpp @@ -0,0 +1,28 @@ +// resource_helper.hpp - Funcions d'ajuda per gestió de recursos +// © 2025 Port a C++20 amb SDL3 +// API simplificada i normalització de rutes + +#pragma once + +#include +#include + +namespace Resource { +namespace Helper { + +// Inicialització del sistema +bool initializeResourceSystem(const std::string& pack_file, bool fallback); + +// Càrrega de fitxers +std::vector loadFile(const std::string& filepath); +bool fileExists(const std::string& filepath); + +// Normalització de rutes +std::string getPackPath(const std::string& asset_path); +std::string normalizePath(const std::string& path); + +// Estat +bool isPackLoaded(); + +} // namespace Helper +} // namespace Resource diff --git a/source/core/resources/resource_loader.cpp b/source/core/resources/resource_loader.cpp new file mode 100644 index 0000000..4426f55 --- /dev/null +++ b/source/core/resources/resource_loader.cpp @@ -0,0 +1,143 @@ +// resource_loader.cpp - Implementació del carregador de recursos +// © 2025 Port a C++20 amb SDL3 + +#include "resource_loader.hpp" + +#include +#include +#include + +namespace Resource { + +// Singleton +Loader& Loader::get() { + static Loader instance; + return instance; +} + +// Inicialitzar el sistema de recursos +bool Loader::initialize(const std::string& pack_file, bool enable_fallback) { + fallback_enabled_ = enable_fallback; + + // Intentar carregar el paquet + pack_ = std::make_unique(); + + if (!pack_->loadPack(pack_file)) { + if (!fallback_enabled_) { + std::cerr << "[ResourceLoader] ERROR FATAL: No es pot carregar " << pack_file + << " i el fallback està desactivat\n"; + return false; + } + + std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de fitxers\n"; + pack_.reset(); // No hi ha paquet + return true; + } + + std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n"; + return true; +} + +// Carregar un recurs +std::vector Loader::loadResource(const std::string& filename) { + // Intentar carregar del paquet primer + if (pack_) { + if (pack_->hasResource(filename)) { + auto data = pack_->getResource(filename); + if (!data.empty()) { + return data; + } + std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename + << "\n"; + } + + // Si no està al paquet i no hi ha fallback, falla + if (!fallback_enabled_) { + std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: " + << filename << "\n"; + return {}; + } + } + + // Fallback al sistema de fitxers + if (fallback_enabled_) { + return loadFromFilesystem(filename); + } + + return {}; +} + +// Comprovar si existeix un recurs +bool Loader::resourceExists(const std::string& filename) { + // Comprovar al paquet + if (pack_ && pack_->hasResource(filename)) { + return true; + } + + // Comprovar al sistema de fitxers si està activat el fallback + if (fallback_enabled_) { + std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename; + return std::filesystem::exists(fullpath); + } + + return false; +} + +// Validar el paquet +bool Loader::validatePack() { + if (!pack_) { + std::cerr << "[ResourceLoader] Advertència: no hi ha paquet carregat per validar\n"; + return false; + } + + return pack_->validatePack(); +} + +// Comprovar si hi ha paquet carregat +bool Loader::isPackLoaded() const { + return pack_ != nullptr; +} + +// Establir la ruta base +void Loader::setBasePath(const std::string& path) { + base_path_ = path; + std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n"; +} + +// Obtenir la ruta base +std::string Loader::getBasePath() const { + return base_path_; +} + +// Carregar des del sistema de fitxers (fallback) +std::vector Loader::loadFromFilesystem(const std::string& filename) { + // The filename is already normalized (e.g., "shapes/logo/letra_j.shp") + // We need to prepend base_path + "data/" + std::string fullpath; + + if (base_path_.empty()) { + fullpath = "data/" + filename; + } else { + fullpath = base_path_ + "/data/" + filename; + } + + std::ifstream file(fullpath, std::ios::binary | std::ios::ate); + if (!file) { + std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n"; + return {}; + } + + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector data(file_size); + if (!file.read(reinterpret_cast(data.data()), file_size)) { + std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n"; + return {}; + } + + std::cout << "[ResourceLoader] Carregat des del sistema de fitxers: " << fullpath << "\n"; + return data; +} + +} // namespace Resource diff --git a/source/core/resources/resource_loader.hpp b/source/core/resources/resource_loader.hpp new file mode 100644 index 0000000..5e5797e --- /dev/null +++ b/source/core/resources/resource_loader.hpp @@ -0,0 +1,53 @@ +// resource_loader.hpp - Carregador de recursos (Singleton) +// © 2025 Port a C++20 amb SDL3 +// Coordina càrrega des del paquet i/o sistema de fitxers + +#pragma once + +#include "resource_pack.hpp" + +#include +#include +#include + +namespace Resource { + +// Singleton per gestionar la càrrega de recursos +class Loader { +public: + // Singleton + static Loader& get(); + + // Inicialització + bool initialize(const std::string& pack_file, bool enable_fallback); + + // Càrrega de recursos + std::vector loadResource(const std::string& filename); + bool resourceExists(const std::string& filename); + + // Validació + bool validatePack(); + bool isPackLoaded() const; + + // Estat + void setBasePath(const std::string& path); + std::string getBasePath() const; + +private: + Loader() = default; + ~Loader() = default; + + // No es pot copiar ni moure + Loader(const Loader&) = delete; + Loader& operator=(const Loader&) = delete; + + // Dades + std::unique_ptr pack_; + bool fallback_enabled_ = false; + std::string base_path_; + + // Funcions auxiliars + std::vector loadFromFilesystem(const std::string& filename); +}; + +} // namespace Resource diff --git a/source/core/resources/resource_pack.cpp b/source/core/resources/resource_pack.cpp new file mode 100644 index 0000000..84a2cdd --- /dev/null +++ b/source/core/resources/resource_pack.cpp @@ -0,0 +1,309 @@ +// resource_pack.cpp - Implementació del sistema d'empaquetament +// © 2025 Port a C++20 amb SDL3 + +#include "resource_pack.hpp" + +#include +#include +#include +#include + +namespace Resource { + +// Calcular checksum CRC32 simplificat +uint32_t Pack::calculateChecksum(const std::vector& data) const { + uint32_t checksum = 0x12345678; + for (unsigned char byte : data) { + checksum = ((checksum << 5) + checksum) + byte; + } + return checksum; +} + +// Encriptació XOR (simètrica) +void Pack::encryptData(std::vector& data, const std::string& key) { + if (key.empty()) { + return; + } + for (size_t i = 0; i < data.size(); ++i) { + data[i] ^= key[i % key.length()]; + } +} + +void Pack::decryptData(std::vector& data, const std::string& key) { + // XOR és simètric + encryptData(data, key); +} + +// Llegir fitxer complet a memòria +std::vector Pack::readFile(const std::string& filepath) { + std::ifstream file(filepath, std::ios::binary | std::ios::ate); + if (!file) { + std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n'; + return {}; + } + + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector data(file_size); + if (!file.read(reinterpret_cast(data.data()), file_size)) { + std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n'; + return {}; + } + + return data; +} + +// Afegir un fitxer individual al paquet +bool Pack::addFile(const std::string& filepath, const std::string& pack_name) { + auto file_data = readFile(filepath); + if (file_data.empty()) { + return false; + } + + ResourceEntry entry{ + .filename = pack_name, + .offset = data_.size(), + .size = file_data.size(), + .checksum = calculateChecksum(file_data)}; + + // Afegir dades al bloc de dades + data_.insert(data_.end(), file_data.begin(), file_data.end()); + + resources_[pack_name] = entry; + + std::cout << "[ResourcePack] Afegit: " << pack_name << " (" << file_data.size() + << " bytes)\n"; + return true; +} + +// Afegir tots els fitxers d'un directori recursivament +bool Pack::addDirectory(const std::string& dir_path, + const std::string& base_path) { + namespace fs = std::filesystem; + + if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) { + std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n'; + return false; + } + + std::string current_base = base_path.empty() ? "" : base_path + "/"; + + for (const auto& entry : fs::recursive_directory_iterator(dir_path)) { + if (!entry.is_regular_file()) { + continue; + } + + std::string full_path = entry.path().string(); + std::string relative_path = entry.path().lexically_relative(dir_path).string(); + + // Convertir barres invertides a normals (Windows) + std::ranges::replace(relative_path, '\\', '/'); + + // Saltar fitxers de desenvolupament + if (relative_path.find(".world") != std::string::npos || + relative_path.find(".tsx") != std::string::npos || + relative_path.find(".DS_Store") != std::string::npos || + relative_path.find(".git") != std::string::npos) { + std::cout << "[ResourcePack] Saltant: " << relative_path << '\n'; + continue; + } + + std::string pack_name = current_base + relative_path; + addFile(full_path, pack_name); + } + + return true; +} + +// Guardar paquet a disc +bool Pack::savePack(const std::string& pack_file) { + std::ofstream file(pack_file, std::ios::binary); + if (!file) { + std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n'; + return false; + } + + // Escriure capçalera + file.write(MAGIC_HEADER, 4); + file.write(reinterpret_cast(&VERSION), sizeof(VERSION)); + + // Escriure nombre de recursos + auto resource_count = static_cast(resources_.size()); + file.write(reinterpret_cast(&resource_count), sizeof(resource_count)); + + // Escriure metadades de recursos + for (const auto& [name, entry] : resources_) { + // Nom del fitxer + auto name_len = static_cast(entry.filename.length()); + file.write(reinterpret_cast(&name_len), sizeof(name_len)); + file.write(entry.filename.c_str(), name_len); + + // Offset, mida, checksum + file.write(reinterpret_cast(&entry.offset), sizeof(entry.offset)); + file.write(reinterpret_cast(&entry.size), sizeof(entry.size)); + file.write(reinterpret_cast(&entry.checksum), sizeof(entry.checksum)); + } + + // Encriptar dades + std::vector encrypted_data = data_; + encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY); + + // Escriure mida de dades i dades encriptades + auto data_size = static_cast(encrypted_data.size()); + file.write(reinterpret_cast(&data_size), sizeof(data_size)); + file.write(reinterpret_cast(encrypted_data.data()), encrypted_data.size()); + + std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size() + << " recursos, " << data_size << " bytes)\n"; + + return true; +} + +// Carregar paquet des de disc +bool Pack::loadPack(const std::string& pack_file) { + std::ifstream file(pack_file, std::ios::binary); + if (!file) { + std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n'; + return false; + } + + // Llegir capçalera + char magic[4]; + file.read(magic, 4); + if (std::string(magic, 4) != MAGIC_HEADER) { + std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER + << ")\n"; + return false; + } + + uint32_t version; + file.read(reinterpret_cast(&version), sizeof(version)); + if (version != VERSION) { + std::cerr << "[ResourcePack] Error: versió incompatible (esperava " << VERSION + << ", trobat " << version << ")\n"; + return false; + } + + // Llegir nombre de recursos + uint32_t resource_count; + file.read(reinterpret_cast(&resource_count), sizeof(resource_count)); + + // Llegir metadades de recursos + resources_.clear(); + for (uint32_t i = 0; i < resource_count; ++i) { + // Nom del fitxer + uint32_t name_len; + file.read(reinterpret_cast(&name_len), sizeof(name_len)); + + std::string filename(name_len, '\0'); + file.read(&filename[0], name_len); + + // Offset, mida, checksum + ResourceEntry entry; + entry.filename = filename; + file.read(reinterpret_cast(&entry.offset), sizeof(entry.offset)); + file.read(reinterpret_cast(&entry.size), sizeof(entry.size)); + file.read(reinterpret_cast(&entry.checksum), sizeof(entry.checksum)); + + resources_[filename] = entry; + } + + // Llegir dades encriptades + uint64_t data_size; + file.read(reinterpret_cast(&data_size), sizeof(data_size)); + + data_.resize(data_size); + file.read(reinterpret_cast(data_.data()), data_size); + + // Desencriptar + decryptData(data_, DEFAULT_ENCRYPT_KEY); + + std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size() + << " recursos)\n"; + + return true; +} + +// Obtenir un recurs del paquet +std::vector Pack::getResource(const std::string& filename) { + auto it = resources_.find(filename); + if (it == resources_.end()) { + std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n'; + return {}; + } + + const auto& entry = it->second; + + // Extreure dades + if (entry.offset + entry.size > data_.size()) { + std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n'; + return {}; + } + + std::vector resource_data(data_.begin() + entry.offset, + data_.begin() + entry.offset + entry.size); + + // Verificar checksum + uint32_t computed_checksum = calculateChecksum(resource_data); + if (computed_checksum != entry.checksum) { + std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename + << " (esperat " << entry.checksum << ", calculat " << computed_checksum + << ")\n"; + // No falla, però adverteix + } + + return resource_data; +} + +// Comprovar si existeix un recurs +bool Pack::hasResource(const std::string& filename) const { + return resources_.find(filename) != resources_.end(); +} + +// Obtenir llista de tots els recursos +std::vector Pack::getResourceList() const { + std::vector list; + list.reserve(resources_.size()); + + for (const auto& [name, entry] : resources_) { + list.push_back(name); + } + + std::ranges::sort(list); + return list; +} + +// Validar integritat del paquet +bool Pack::validatePack() const { + bool valid = true; + + for (const auto& [name, entry] : resources_) { + // Verificar offset i mida + if (entry.offset + entry.size > data_.size()) { + std::cerr << "[ResourcePack] Error de validació: " << name + << " té offset/mida invàlid\n"; + valid = false; + continue; + } + + // Extreure i verificar checksum + std::vector resource_data(data_.begin() + entry.offset, + data_.begin() + entry.offset + entry.size); + + uint32_t computed_checksum = calculateChecksum(resource_data); + if (computed_checksum != entry.checksum) { + std::cerr << "[ResourcePack] Error de validació: " << name + << " té checksum invàlid\n"; + valid = false; + } + } + + if (valid) { + std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n"; + } + + return valid; +} + +} // namespace Resource diff --git a/source/core/resources/resource_pack.hpp b/source/core/resources/resource_pack.hpp new file mode 100644 index 0000000..1ed6e5f --- /dev/null +++ b/source/core/resources/resource_pack.hpp @@ -0,0 +1,67 @@ +// resource_pack.hpp - Sistema d'empaquetament de recursos +// © 2025 Port a C++20 amb SDL3 +// Basat en el sistema de "pollo" amb adaptacions per Orni Attack + +#pragma once + +#include +#include +#include +#include + +namespace Resource { + +// Capçalera del fitxer de paquet +struct PackHeader { + char magic[4]; // "ORNI" + uint32_t version; // Versió del format (1) +}; + +// Entrada de recurs dins el paquet +struct ResourceEntry { + std::string filename; // Nom del recurs (amb barres normals) + uint64_t offset; // Posició dins el bloc de dades + uint64_t size; // Mida en bytes + uint32_t checksum; // Checksum CRC32 per verificació +}; + +// Classe principal per gestionar paquets de recursos +class Pack { +public: + Pack() = default; + ~Pack() = default; + + // Afegir fitxers al paquet + bool addFile(const std::string& filepath, const std::string& pack_name); + bool addDirectory(const std::string& dir_path, const std::string& base_path = ""); + + // Guardar i carregar paquets + bool savePack(const std::string& pack_file); + bool loadPack(const std::string& pack_file); + + // Accés a recursos + std::vector getResource(const std::string& filename); + bool hasResource(const std::string& filename) const; + std::vector getResourceList() const; + + // Validació + bool validatePack() const; + +private: + // Constants + static constexpr const char* MAGIC_HEADER = "ORNI"; + static constexpr uint32_t VERSION = 1; + static constexpr const char* DEFAULT_ENCRYPT_KEY = "ORNI_RESOURCES_2025"; + + // Dades del paquet + std::unordered_map resources_; + std::vector data_; + + // Funcions auxiliars + std::vector readFile(const std::string& filepath); + uint32_t calculateChecksum(const std::vector& data) const; + void encryptData(std::vector& data, const std::string& key); + void decryptData(std::vector& data, const std::string& key); +}; + +} // namespace Resource diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index c45be91..f557157 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -11,6 +11,9 @@ #include "core/audio/audio_cache.hpp" #include "core/defaults.hpp" #include "core/rendering/sdl_manager.hpp" +#include "core/resources/resource_helper.hpp" +#include "core/resources/resource_loader.hpp" +#include "core/utils/path_utils.hpp" #include "game/escenes/escena_joc.hpp" #include "game/escenes/escena_logo.hpp" #include "game/escenes/escena_titol.hpp" @@ -37,6 +40,44 @@ Director::Director(std::vector const& args) { // Comprovar arguments del programa executable_path_ = checkProgramArguments(args); + // Inicialitzar sistema de rutes + Utils::initializePathSystem(args[0].c_str()); + + // Obtenir ruta base dels recursos + std::string resource_base = Utils::getResourceBasePath(); + + // Inicialitzar sistema de recursos +#ifdef RELEASE_BUILD + // Mode release: paquet obligatori, sense fallback + std::string pack_path = resource_base + "/resources.pack"; + if (!Resource::Helper::initializeResourceSystem(pack_path, false)) { + std::cerr << "ERROR FATAL: No es pot carregar " << pack_path << "\n"; + std::cerr << "El joc no pot continuar sense els recursos.\n"; + std::exit(1); + } + + // Validar integritat del paquet + if (!Resource::Loader::get().validatePack()) { + std::cerr << "ERROR FATAL: El paquet de recursos està corromput\n"; + std::exit(1); + } + + std::cout << "Sistema de recursos inicialitzat (mode release)\n"; +#else + // Mode desenvolupament: intentar paquet amb fallback a data/ + std::string pack_path = resource_base + "/resources.pack"; + Resource::Helper::initializeResourceSystem(pack_path, true); + + if (Resource::Helper::isPackLoaded()) { + std::cout << "Sistema de recursos inicialitzat (mode dev amb paquet)\n"; + } else { + std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n"; + } + + // Establir ruta base per al fallback + Resource::Loader::get().setBasePath(resource_base); +#endif + // Crear carpetes del sistema createSystemFolder("jailgames"); createSystemFolder(std::string("jailgames/") + Project::NAME); diff --git a/source/core/utils/path_utils.cpp b/source/core/utils/path_utils.cpp new file mode 100644 index 0000000..e21fcb3 --- /dev/null +++ b/source/core/utils/path_utils.cpp @@ -0,0 +1,92 @@ +// path_utils.cpp - Implementació de utilitats de rutes +// © 2025 Port a C++20 amb SDL3 + +#include "path_utils.hpp" + +#include +#include +#include + +namespace Utils { + +// Variables globals per guardar argv[0] +static std::string executable_path_; +static std::string executable_directory_; + +// Inicialitzar el sistema de rutes amb argv[0] +void initializePathSystem(const char* argv0) { + if (!argv0) { + std::cerr << "[PathUtils] ADVERTÈNCIA: argv[0] és nullptr\n"; + executable_path_ = ""; + executable_directory_ = "."; + return; + } + + executable_path_ = argv0; + + // Extreure el directori + std::filesystem::path path(argv0); + executable_directory_ = path.parent_path().string(); + + if (executable_directory_.empty()) { + executable_directory_ = "."; + } + + std::cout << "[PathUtils] Executable: " << executable_path_ << "\n"; + std::cout << "[PathUtils] Directori: " << executable_directory_ << "\n"; +} + +// Obtenir el directori de l'executable +std::string getExecutableDirectory() { + if (executable_directory_.empty()) { + std::cerr << "[PathUtils] ADVERTÈNCIA: Sistema de rutes no inicialitzat\n"; + return "."; + } + return executable_directory_; +} + +// Detectar si estem dins un bundle de macOS +bool isMacOSBundle() { +#ifdef MACOS_BUNDLE + return true; +#else + // Detecció en temps d'execució + // Cercar ".app/Contents/MacOS" a la ruta de l'executable + std::string exe_dir = getExecutableDirectory(); + return exe_dir.find(".app/Contents/MacOS") != std::string::npos; +#endif +} + +// Obtenir la ruta base dels recursos +std::string getResourceBasePath() { + std::string exe_dir = getExecutableDirectory(); + + if (isMacOSBundle()) { + // Bundle de macOS: recursos a ../Resources des de MacOS/ + std::cout << "[PathUtils] Detectat bundle de macOS\n"; + return exe_dir + "/../Resources"; + } else { + // Executable normal: recursos al mateix directori + return exe_dir; + } +} + +// Normalitzar ruta (convertir barres, etc.) +std::string normalizePath(const std::string& path) { + std::string normalized = path; + + // Convertir barres invertides a normals + std::ranges::replace(normalized, '\\', '/'); + + // Simplificar rutes amb filesystem + try { + std::filesystem::path fs_path(normalized); + normalized = fs_path.lexically_normal().string(); + } catch (const std::exception& e) { + std::cerr << "[PathUtils] Error normalitzant ruta: " << e.what() << "\n"; + } + + return normalized; +} + +} // namespace Utils diff --git a/source/core/utils/path_utils.hpp b/source/core/utils/path_utils.hpp new file mode 100644 index 0000000..d4657e5 --- /dev/null +++ b/source/core/utils/path_utils.hpp @@ -0,0 +1,24 @@ +// path_utils.hpp - Utilitats de gestió de rutes +// © 2025 Port a C++20 amb SDL3 +// Detecció de directoris i bundles multiplataforma + +#pragma once + +#include + +namespace Utils { + +// Inicialització amb argv[0] +void initializePathSystem(const char* argv0); + +// Obtenció de rutes +std::string getExecutableDirectory(); +std::string getResourceBasePath(); + +// Detecció de plataforma +bool isMacOSBundle(); + +// Normalització +std::string normalizePath(const std::string& path); + +} // namespace Utils diff --git a/source/game/stage_system/stage_loader.cpp b/source/game/stage_system/stage_loader.cpp index 8432429..f83b466 100644 --- a/source/game/stage_system/stage_loader.cpp +++ b/source/game/stage_system/stage_loader.cpp @@ -2,24 +2,37 @@ // © 2025 Orni Attack #include "stage_loader.hpp" + +#include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" -#include + #include +#include #include namespace StageSystem { std::unique_ptr StageLoader::carregar(const std::string& path) { try { - // Llegir fitxer YAML - std::ifstream file(path); - if (!file.is_open()) { - std::cerr << "[StageLoader] Error: no es pot obrir el fitxer " << path << std::endl; + // Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml" + std::string normalized = path; + if (normalized.starts_with("data/")) { + normalized = normalized.substr(5); + } + + // Load from resource system + std::vector data = Resource::Helper::loadFile(normalized); + if (data.empty()) { + std::cerr << "[StageLoader] Error: no es pot carregar " << normalized << std::endl; return nullptr; } + // Convert to string + std::string yaml_content(data.begin(), data.end()); + std::stringstream stream(yaml_content); + // Parse YAML - fkyaml::node yaml = fkyaml::node::deserialize(file); + fkyaml::node yaml = fkyaml::node::deserialize(stream); auto config = std::make_unique(); // Parse metadata diff --git a/tools/pack_resources/Makefile b/tools/pack_resources/Makefile new file mode 100644 index 0000000..974d349 --- /dev/null +++ b/tools/pack_resources/Makefile @@ -0,0 +1,20 @@ +# Makefile per a pack_resources +# © 2025 Orni Attack + +CXX = clang++ +CXXFLAGS = -std=c++20 -Wall -Wextra -I../../source +TARGET = pack_resources + +SOURCES = pack_resources.cpp \ + ../../source/core/resources/resource_pack.cpp + +$(TARGET): $(SOURCES) + @echo "Compilant $(TARGET)..." + @$(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCES) + @echo "✓ $(TARGET) compilat" + +clean: + @rm -f $(TARGET) + @echo "✓ Netejat" + +.PHONY: clean diff --git a/tools/pack_resources/pack_resources b/tools/pack_resources/pack_resources new file mode 100755 index 0000000..2715f9a Binary files /dev/null and b/tools/pack_resources/pack_resources differ diff --git a/tools/pack_resources/pack_resources.cpp b/tools/pack_resources/pack_resources.cpp new file mode 100644 index 0000000..675dc48 --- /dev/null +++ b/tools/pack_resources/pack_resources.cpp @@ -0,0 +1,92 @@ +// pack_resources.cpp - Utilitat per crear paquets de recursos +// © 2025 Orni Attack + +#include "../../source/core/resources/resource_pack.hpp" + +#include +#include + +void print_usage(const char* program_name) { + std::cout << "Ús: " << program_name << " [opcions] [directori_entrada] [fitxer_sortida]\n"; + std::cout << "\nOpcions:\n"; + std::cout << " --list Llistar contingut d'un paquet\n"; + std::cout << "\nExemples:\n"; + std::cout << " " << program_name << " data resources.pack\n"; + std::cout << " " << program_name << " --list resources.pack\n"; + std::cout << "\nSi no s'especifiquen arguments, empaqueta 'data/' a 'resources.pack'\n"; +} + +int main(int argc, char* argv[]) { + std::string input_dir = "data"; + std::string output_file = "resources.pack"; + + // Processar arguments + if (argc == 2 && std::string(argv[1]) == "--help") { + print_usage(argv[0]); + return 0; + } + + // Mode --list + if (argc == 3 && std::string(argv[1]) == "--list") { + Resource::Pack pack; + if (!pack.loadPack(argv[2])) { + std::cerr << "ERROR: No es pot carregar " << argv[2] << "\n"; + return 1; + } + + std::cout << "Contingut de " << argv[2] << ":\n"; + auto resources = pack.getResourceList(); + std::cout << "Total: " << resources.size() << " recursos\n\n"; + + for (const auto& name : resources) { + std::cout << " " << name << "\n"; + } + + return 0; + } + + // Mode empaquetar + if (argc >= 3) { + input_dir = argv[1]; + output_file = argv[2]; + } + + // Verificar que existeix el directori + if (!std::filesystem::exists(input_dir)) { + std::cerr << "ERROR: Directori no trobat: " << input_dir << "\n"; + return 1; + } + + if (!std::filesystem::is_directory(input_dir)) { + std::cerr << "ERROR: " << input_dir << " no és un directori\n"; + return 1; + } + + // Crear paquet + std::cout << "Creant paquet de recursos...\n"; + std::cout << " Entrada: " << input_dir << "\n"; + std::cout << " Sortida: " << output_file << "\n\n"; + + Resource::Pack pack; + if (!pack.addDirectory(input_dir)) { + std::cerr << "ERROR: No s'ha pogut afegir el directori\n"; + return 1; + } + + if (!pack.savePack(output_file)) { + std::cerr << "ERROR: No s'ha pogut guardar el paquet\n"; + return 1; + } + + // Resum + auto resources = pack.getResourceList(); + std::cout << "\n"; + std::cout << "✓ Paquet creat amb èxit!\n"; + std::cout << " Recursos: " << resources.size() << "\n"; + + // Mostrar mida del fitxer + auto file_size = std::filesystem::file_size(output_file); + std::cout << " Mida: " << (file_size / 1024) << " KB\n"; + + return 0; +}