diff --git a/CMakeLists.txt b/CMakeLists.txt index 8085f2d..e17a016 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ set(APP_SOURCES source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp source/core/resources/resource_helper.cpp source/core/resources/resource_cache.cpp + source/core/resources/resource_pack.cpp source/core/resources/palette_loader.cpp source/utils/easing.cpp source/utils/utils.cpp @@ -160,24 +161,43 @@ target_include_directories(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3) -# Còpia recursiva de data/ al costat del binari perquè les rutes relatives -# ("data/player/player.gif") funcionin sense desplegament. Es defineix com -# a custom target amb `ALL` — s'executa a cada `cmake --build` encara que -# el binari no hagi canviat, de manera que editar YAML/GIF sense tocar codi -# igualment sincronitza el build. Abans era un POST_BUILD, i si només es -# modificava una dada la còpia no es disparava. -add_custom_target(sync_data ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_SOURCE_DIR}/data" - "${CMAKE_BINARY_DIR}/data" - COMMENT "Copying data/ next to the binary" +# --- EINA STANDALONE: pack_resources --- +# Executable auxiliar que empaqueta `data/` a `build/resources.pack`. +# EXCLUDE_FROM_ALL: només es compila quan algun target en depèn (ho fa +# `resource_pack`). Build manual: `cmake --build build --target pack_resources`. +add_executable(pack_resources EXCLUDE_FROM_ALL + tools/pack_resources/pack_resources.cpp + source/core/resources/resource_pack.cpp ) -add_dependencies(${PROJECT_NAME} sync_data) +target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") +target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) + +# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- +# A cada build re-empaquetem data/ si algun fitxer dins ha canviat. Evita +# debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake a re-globbar +# a la pròxima invocació (recull fitxers nous afegits a data/). +file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*") +set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack") + +add_custom_command( + OUTPUT ${RESOURCE_PACK} + COMMAND $ + "${CMAKE_SOURCE_DIR}/data" + "${RESOURCE_PACK}" + DEPENDS pack_resources ${DATA_FILES} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Empaquetant data/ → build/resources.pack" + VERBATIM +) +add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK}) +add_dependencies(${PROJECT_NAME} resource_pack) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:_DEBUG> $<$:RELEASE_BUILD> + # En Debug el fallback a disc apunta al source tree (sense re-packejar). + AEE_SOURCE_DIR="${CMAKE_SOURCE_DIR}/" ) # --- STATIC ANALYSIS / FORMAT TARGETS --- @@ -271,6 +291,7 @@ if(BUILD_TESTS) add_executable(test_entities_smoke tests/test_entities_smoke.cpp source/core/resources/resource_helper.cpp + source/core/resources/resource_pack.cpp source/game/map/map.cpp ${ENTITY_SOURCES} ) @@ -295,6 +316,7 @@ if(BUILD_TESTS) source/game/scenes/placeholder_input.cpp source/core/input/key_config.cpp source/core/resources/resource_helper.cpp + source/core/resources/resource_pack.cpp ) target_include_directories(test_scene_flow PRIVATE "${CMAKE_SOURCE_DIR}/source" diff --git a/Makefile b/Makefile index eb38c4f..3d2678d 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ endif CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH) -.PHONY: all debug release run run-debug clean rebuild show-version format format-check tidy tidy-fix cppcheck hooks-install help +.PHONY: all debug release run run-debug clean rebuild show-version pack format format-check tidy tidy-fix cppcheck hooks-install help # ============================================================================== # COMPILACIÓ @@ -53,6 +53,14 @@ clean: rebuild: clean all +# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild +# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa +# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`. +pack: + @cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS) + @cmake --build $(BUILDDIR) --target pack_resources + @./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack + # ============================================================================== # CODE QUALITY (delegados a cmake) # ============================================================================== @@ -100,6 +108,9 @@ help: @echo " Release:" @echo " make release - Release + empaquetat (TODO pendent)" @echo "" + @echo " Recursos:" + @echo " make pack - Empaquetar data/ a $(BUILDDIR)/resources.pack (format AE26)" + @echo "" @echo " Qualitat de codi:" @echo " make format - Formatar codi amb clang-format" @echo " make format-check - Verificar format sense modificar" diff --git a/data/fonts/ari-w9500-bold.fnt b/data/fonts/ari-w9500-bold.fnt deleted file mode 100644 index 066e489..0000000 --- a/data/fonts/ari-w9500-bold.fnt +++ /dev/null @@ -1,132 +0,0 @@ -# Font: ari-w9500-bold — generado desde ari-w9500-bold.ttf size 11 -# Generado con tools/font_gen/font_gen.py - -box_width 13 -box_height 11 -columns 15 - -# codepoint_decimal ancho_visual -32 3 # U+0020 -33 3 # ! -34 6 # " -35 9 # # -36 7 # $ -37 12 # % -38 8 # & -39 3 # ' -40 5 # ( -41 5 # ) -42 5 # * -43 7 # + -44 3 # , -45 5 # - -46 3 # . -47 6 # / -48 7 # 0 -49 6 # 1 -50 7 # 2 -51 7 # 3 -52 7 # 4 -53 7 # 5 -54 7 # 6 -55 7 # 7 -56 7 # 8 -57 7 # 9 -58 3 # : -59 3 # ; -60 7 # < -61 7 # = -62 7 # > -63 7 # ? -64 13 # @ -65 9 # A -66 8 # B -67 8 # C -68 8 # D -69 7 # E -70 7 # F -71 8 # G -72 8 # H -73 3 # I -74 6 # J -75 8 # K -76 7 # L -77 10 # M -78 9 # N -79 9 # O -80 8 # P -81 9 # Q -82 8 # R -83 8 # S -84 7 # T -85 8 # U -86 8 # V -87 11 # W -88 9 # X -89 9 # Y -90 9 # Z -91 5 # [ -92 6 # \ -93 5 # ] -94 6 # ^ -95 7 # _ -96 4 # ` -97 7 # a -98 7 # b -99 7 # c -100 7 # d -101 7 # e -102 5 # f -103 7 # g -104 7 # h -105 3 # i -106 3 # j -107 7 # k -108 3 # l -109 9 # m -110 7 # n -111 7 # o -112 7 # p -113 7 # q -114 5 # r -115 6 # s -116 5 # t -117 7 # u -118 7 # v -119 9 # w -120 7 # x -121 7 # y -122 7 # z -123 6 # { -124 3 # | -125 6 # } -126 8 # ~ -192 9 # À -193 9 # Á -200 7 # È -201 7 # É -204 3 # Ì -205 3 # Í -210 9 # Ò -211 9 # Ó -217 8 # Ù -218 8 # Ú -209 9 # Ñ -199 8 # Ç -224 7 # à -225 7 # á -232 7 # è -233 7 # é -236 3 # ì -237 4 # í -242 7 # ò -243 7 # ó -249 7 # ù -250 7 # ú -241 7 # ñ -231 7 # ç -161 3 # ¡ -191 6 # ¿ -171 9 # « -187 9 # » -183 3 # · diff --git a/data/fonts/ari-w9500-bold.gif b/data/fonts/ari-w9500-bold.gif deleted file mode 100644 index 4ca1fc6..0000000 Binary files a/data/fonts/ari-w9500-bold.gif and /dev/null differ diff --git a/data/fonts/ari-w9500-condensed-bold.fnt b/data/fonts/ari-w9500-condensed-bold.fnt deleted file mode 100644 index 178b56b..0000000 --- a/data/fonts/ari-w9500-condensed-bold.fnt +++ /dev/null @@ -1,132 +0,0 @@ -# Font: ari-w9500-condensed-bold — generado desde ari-w9500-condensed-bold.ttf size 11 -# Generado con tools/font_gen/font_gen.py - -box_width 12 -box_height 11 -columns 15 - -# codepoint_decimal ancho_visual -32 2 # U+0020 -33 3 # ! -34 6 # " -35 9 # # -36 6 # $ -37 10 # % -38 7 # & -39 3 # ' -40 5 # ( -41 5 # ) -42 5 # * -43 7 # + -44 3 # , -45 5 # - -46 3 # . -47 5 # / -48 6 # 0 -49 5 # 1 -50 6 # 2 -51 6 # 3 -52 6 # 4 -53 6 # 5 -54 6 # 6 -55 6 # 7 -56 6 # 8 -57 6 # 9 -58 3 # : -59 3 # ; -60 7 # < -61 7 # = -62 7 # > -63 6 # ? -64 12 # @ -65 8 # A -66 7 # B -67 7 # C -68 7 # D -69 6 # E -70 6 # F -71 7 # G -72 7 # H -73 3 # I -74 6 # J -75 7 # K -76 7 # L -77 8 # M -78 8 # N -79 8 # O -80 7 # P -81 8 # Q -82 7 # R -83 7 # S -84 7 # T -85 7 # U -86 7 # V -87 9 # W -88 7 # X -89 7 # Y -90 7 # Z -91 5 # [ -92 5 # \ -93 5 # ] -94 6 # ^ -95 6 # _ -96 4 # ` -97 6 # a -98 6 # b -99 6 # c -100 6 # d -101 6 # e -102 5 # f -103 6 # g -104 6 # h -105 3 # i -106 3 # j -107 6 # k -108 3 # l -109 8 # m -110 6 # n -111 6 # o -112 6 # p -113 6 # q -114 5 # r -115 6 # s -116 5 # t -117 6 # u -118 6 # v -119 8 # w -120 6 # x -121 6 # y -122 6 # z -123 6 # { -124 3 # | -125 6 # } -126 8 # ~ -192 8 # À -193 8 # Á -200 6 # È -201 6 # É -204 3 # Ì -205 3 # Í -210 8 # Ò -211 8 # Ó -217 7 # Ù -218 7 # Ú -209 8 # Ñ -199 7 # Ç -224 6 # à -225 6 # á -232 6 # è -233 6 # é -236 3 # ì -237 4 # í -242 6 # ò -243 6 # ó -249 6 # ù -250 6 # ú -241 6 # ñ -231 6 # ç -161 3 # ¡ -191 6 # ¿ -171 9 # « -187 9 # » -183 3 # · diff --git a/data/fonts/ari-w9500-condensed-bold.gif b/data/fonts/ari-w9500-condensed-bold.gif deleted file mode 100644 index 63c81c5..0000000 Binary files a/data/fonts/ari-w9500-condensed-bold.gif and /dev/null differ diff --git a/data/fonts/ari-w9500-condensed.fnt b/data/fonts/ari-w9500-condensed.fnt deleted file mode 100644 index 4264e73..0000000 --- a/data/fonts/ari-w9500-condensed.fnt +++ /dev/null @@ -1,132 +0,0 @@ -# Font: ari-w9500-condensed — generado desde ari-w9500-condensed.ttf size 11 -# Generado con tools/font_gen/font_gen.py - -box_width 9 -box_height 11 -columns 15 - -# codepoint_decimal ancho_visual -32 2 # U+0020 -33 2 # ! -34 4 # " -35 8 # # -36 5 # $ -37 8 # % -38 6 # & -39 2 # ' -40 4 # ( -41 4 # ) -42 4 # * -43 6 # + -44 2 # , -45 4 # - -46 2 # . -47 4 # / -48 5 # 0 -49 4 # 1 -50 5 # 2 -51 5 # 3 -52 5 # 4 -53 5 # 5 -54 5 # 6 -55 5 # 7 -56 5 # 8 -57 5 # 9 -58 2 # : -59 2 # ; -60 4 # < -61 6 # = -62 4 # > -63 5 # ? -64 9 # @ -65 6 # A -66 6 # B -67 6 # C -68 6 # D -69 6 # E -70 6 # F -71 6 # G -72 6 # H -73 2 # I -74 4 # J -75 6 # K -76 6 # L -77 6 # M -78 6 # N -79 7 # O -80 6 # P -81 7 # Q -82 6 # R -83 6 # S -84 6 # T -85 6 # U -86 6 # V -87 8 # W -88 6 # X -89 6 # Y -90 6 # Z -91 4 # [ -92 4 # \ -93 4 # ] -94 6 # ^ -95 5 # _ -96 3 # ` -97 5 # a -98 5 # b -99 5 # c -100 5 # d -101 5 # e -102 4 # f -103 5 # g -104 5 # h -105 2 # i -106 2 # j -107 5 # k -108 2 # l -109 6 # m -110 5 # n -111 5 # o -112 5 # p -113 5 # q -114 4 # r -115 5 # s -116 4 # t -117 5 # u -118 5 # v -119 6 # w -120 5 # x -121 5 # y -122 5 # z -123 5 # { -124 2 # | -125 5 # } -126 6 # ~ -192 6 # À -193 6 # Á -200 6 # È -201 6 # É -204 2 # Ì -205 2 # Í -210 7 # Ò -211 7 # Ó -217 6 # Ù -218 6 # Ú -209 6 # Ñ -199 6 # Ç -224 5 # à -225 5 # á -232 5 # è -233 5 # é -236 2 # ì -237 3 # í -242 5 # ò -243 5 # ó -249 5 # ù -250 5 # ú -241 5 # ñ -231 5 # ç -161 2 # ¡ -191 5 # ¿ -171 7 # « -187 7 # » -183 2 # · diff --git a/data/fonts/ari-w9500-condensed.gif b/data/fonts/ari-w9500-condensed.gif deleted file mode 100644 index 19b085c..0000000 Binary files a/data/fonts/ari-w9500-condensed.gif and /dev/null differ diff --git a/data/fonts/ari-w9500.fnt b/data/fonts/ari-w9500.fnt deleted file mode 100644 index 449e8a2..0000000 --- a/data/fonts/ari-w9500.fnt +++ /dev/null @@ -1,132 +0,0 @@ -# Font: ari-w9500 — generado desde ari-w9500.ttf size 11 -# Generado con tools/font_gen/font_gen.py - -box_width 12 -box_height 11 -columns 15 - -# codepoint_decimal ancho_visual -32 3 # U+0020 -33 2 # ! -34 4 # " -35 9 # # -36 6 # $ -37 10 # % -38 7 # & -39 2 # ' -40 4 # ( -41 4 # ) -42 4 # * -43 6 # + -44 2 # , -45 4 # - -46 2 # . -47 5 # / -48 6 # 0 -49 5 # 1 -50 6 # 2 -51 6 # 3 -52 6 # 4 -53 6 # 5 -54 6 # 6 -55 6 # 7 -56 6 # 8 -57 6 # 9 -58 2 # : -59 2 # ; -60 6 # < -61 6 # = -62 6 # > -63 6 # ? -64 12 # @ -65 8 # A -66 7 # B -67 8 # C -68 8 # D -69 7 # E -70 7 # F -71 8 # G -72 7 # H -73 2 # I -74 5 # J -75 7 # K -76 7 # L -77 8 # M -78 7 # N -79 8 # O -80 7 # P -81 8 # Q -82 7 # R -83 7 # S -84 6 # T -85 7 # U -86 7 # V -87 10 # W -88 8 # X -89 8 # Y -90 8 # Z -91 4 # [ -92 5 # \ -93 4 # ] -94 6 # ^ -95 6 # _ -96 3 # ` -97 6 # a -98 6 # b -99 6 # c -100 6 # d -101 6 # e -102 4 # f -103 6 # g -104 6 # h -105 2 # i -106 2 # j -107 6 # k -108 2 # l -109 8 # m -110 6 # n -111 6 # o -112 6 # p -113 6 # q -114 4 # r -115 5 # s -116 4 # t -117 6 # u -118 6 # v -119 8 # w -120 6 # x -121 6 # y -122 6 # z -123 5 # { -124 2 # | -125 5 # } -126 6 # ~ -192 8 # À -193 8 # Á -200 7 # È -201 7 # É -204 2 # Ì -205 2 # Í -210 8 # Ò -211 8 # Ó -217 7 # Ù -218 7 # Ú -209 7 # Ñ -199 8 # Ç -224 6 # à -225 6 # á -232 6 # è -233 6 # é -236 2 # ì -237 3 # í -242 6 # ò -243 6 # ó -249 6 # ù -250 6 # ú -241 6 # ñ -231 6 # ç -161 2 # ¡ -191 6 # ¿ -171 7 # « -187 7 # » -183 2 # · diff --git a/data/fonts/ari-w9500.gif b/data/fonts/ari-w9500.gif deleted file mode 100644 index 994cb1b..0000000 Binary files a/data/fonts/ari-w9500.gif and /dev/null differ diff --git a/source/core/input/key_config.cpp b/source/core/input/key_config.cpp index c9a915f..fa8967f 100644 --- a/source/core/input/key_config.cpp +++ b/source/core/input/key_config.cpp @@ -13,17 +13,18 @@ auto KeyConfig::get() -> KeyConfig& { } void KeyConfig::load(const std::string& file_path) { - const auto FULL = Resource::Helper::resolve(file_path); - std::ifstream file(FULL); - if (!file.is_open()) { - std::cerr << "KeyConfig: no s'ha pogut obrir " << FULL << '\n'; + auto bytes = Resource::Helper::loadFile(file_path); + if (bytes.empty()) { + std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n'; return; } try { - auto yaml = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) { - std::cerr << "KeyConfig: falta la secció 'keys' a " << FULL << '\n'; + std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n'; return; } bindings_.clear(); @@ -38,9 +39,9 @@ void KeyConfig::load(const std::string& file_path) { } bindings_[ID] = SC; } - std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << FULL << '\n'; + std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n'; } catch (const std::exception& e) { - std::cerr << "KeyConfig: error parsejant " << FULL << ": " << e.what() << '\n'; + std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n'; } } diff --git a/source/core/locale/locale.cpp b/source/core/locale/locale.cpp index 0a3241a..42c9647 100644 --- a/source/core/locale/locale.cpp +++ b/source/core/locale/locale.cpp @@ -34,20 +34,21 @@ auto Locale::get() -> Locale& { } void Locale::load(const std::string& file_path) { - const auto FULL = Resource::Helper::resolve(file_path); - std::ifstream file(FULL); - if (!file.is_open()) { - std::cerr << "Locale: no s'ha pogut obrir " << FULL << '\n'; + auto bytes = Resource::Helper::loadFile(file_path); + if (bytes.empty()) { + std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n'; return; } try { - auto yaml = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); strings_.clear(); flatten(yaml, "", strings_); - std::cout << "Locale: " << strings_.size() << " traduccions des de " << FULL << '\n'; + std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n'; } catch (const std::exception& e) { - std::cerr << "Locale: error parsejant " << FULL << ": " << e.what() << '\n'; + std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n'; } } diff --git a/source/core/resources/resource_helper.cpp b/source/core/resources/resource_helper.cpp index ef3e869..79c6d5c 100644 --- a/source/core/resources/resource_helper.cpp +++ b/source/core/resources/resource_helper.cpp @@ -3,15 +3,65 @@ #include #include #include +#include + +#include "core/resources/resource_pack.hpp" namespace Resource::Helper { namespace { std::string g_data_root; + Pack g_pack; + bool g_pack_loaded = false; + bool g_fallback_enabled = true; + + auto readFromDisk(const std::string& filepath) -> std::vector { + const auto FULL = resolve(filepath); + std::ifstream file(FULL, std::ios::binary | std::ios::ate); + if (!file) { + std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; + return {}; + } + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector data(static_cast(size)); + if (!file.read(reinterpret_cast(data.data()), size)) { + std::cerr << "Resource::Helper: failed to read " << FULL << '\n'; + return {}; + } + return data; + } } // namespace + auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool { + g_fallback_enabled = enable_fallback; + g_pack_loaded = g_pack.loadPack(pack_file); + + if (g_pack_loaded) { + std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount() + << " entries) from " << pack_file << '\n'; + } else if (enable_fallback) { + std::cout << "Resource::Helper: no pack at " << pack_file + << " — using filesystem fallback\n"; + } else { + std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file + << " and fallback disabled\n"; + return false; + } + return true; + } + + void shutdownResourceSystem() { + g_pack.clear(); + g_pack_loaded = false; + } + + auto hasPack() -> bool { + return g_pack_loaded; + } + void setDataRoot(const std::string& absolute_path) { g_data_root = absolute_path; if (!g_data_root.empty() && g_data_root.back() != '/') { @@ -26,20 +76,17 @@ namespace Resource::Helper { } auto loadFile(const std::string& filepath) -> std::vector { - const auto FULL = resolve(filepath); - std::ifstream file(FULL, std::ios::binary | std::ios::ate); - if (!file) { - std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; - return {}; + if (g_pack_loaded) { + // El pack està indexat relatiu a `data/` (la carpeta que + // s'empaqueta), però els callers passen "data/foo.yaml" amb + // prefix. Fem el strip ací perquè no hagin de tenir-ho en compte. + constexpr std::string_view PREFIX = "data/"; + std::string key = filepath; + if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); } + if (g_pack.hasResource(key)) { return g_pack.getResource(key); } } - std::streamsize size = file.tellg(); - file.seekg(0, std::ios::beg); - std::vector data(static_cast(size)); - if (!file.read(reinterpret_cast(data.data()), size)) { - std::cerr << "Resource::Helper: failed to read " << FULL << '\n'; - return {}; - } - return data; + if (g_fallback_enabled) { return readFromDisk(filepath); } + return {}; } } // namespace Resource::Helper diff --git a/source/core/resources/resource_helper.hpp b/source/core/resources/resource_helper.hpp index 2ddc4f2..3f2b3d8 100644 --- a/source/core/resources/resource_helper.hpp +++ b/source/core/resources/resource_helper.hpp @@ -6,23 +6,33 @@ namespace Resource::Helper { - // Estableix el prefix absolut des d'on es resolen totes les rutes - // relatives passades a `loadFile`/`resolve`. Pensat per a cridar-se una - // sola vegada al primer pas del Director amb `SDL_GetBasePath()`: així el - // binari troba `data/` sense dependre del CWD des d'on s'ha llançat (és - // obligatori per a .app de macOS i útil a Linux per no haver d'estar - // dins de `build/`). + // Inicialitza el sistema de recursos. Carrega el pack des de `pack_file` + // i decideix si `loadFile` pot caure a disc quan un recurs no és al pack. + // En Release el pack és obligatori: passar `enable_fallback=false` fa + // que `initializeResourceSystem` retorne false si no s'ha pogut carregar. + auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool; + + // Allibera el pack carregat a memòria. + void shutdownResourceSystem(); + + // True si el sistema es va inicialitzar amb un pack vàlid. + [[nodiscard]] auto hasPack() -> bool; + + // Estableix el prefix des d'on es resolen les rutes quan el fallback + // llig de disc. En Debug apunta al source tree (`${CMAKE_SOURCE_DIR}/`) + // perquè editar YAML/GIF dins `data/` es vegi reflectit sense re-packejar. + // En Release el fallback està desactivat i aquest camí no es fa servir. void setDataRoot(const std::string& absolute_path); - // Resol una ruta relativa contra el data root. Si la ruta ja és absoluta - // (comença per '/') la retorna sense modificar — així els fitxers de - // config (system_folder_ + "config.yaml") segueixen funcionant sense - // canvis al call site. + // Resol una ruta relativa contra el data root. Si ja és absoluta + // (comença per '/') la retorna sense modificar. S'usa per a rutes que + // NO passen pel pack (p.ex. fitxers de config dins system_folder_). [[nodiscard]] auto resolve(const std::string& filepath) -> std::string; - // Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack). - // Les rutes relatives es resolen via `resolve()`; les absolutes s'obrin - // directament. + // Carrega un recurs pel seu camí relatiu (p.ex. "fonts/hud.gif", + // "input/keys.yaml"). Prova primer el pack; si no hi és i el fallback + // està actiu, cau a disc (data root + ruta). Retorna vector buit si + // no el troba. auto loadFile(const std::string& filepath) -> std::vector; } // namespace Resource::Helper diff --git a/source/core/resources/resource_pack.cpp b/source/core/resources/resource_pack.cpp new file mode 100644 index 0000000..1ddf04c --- /dev/null +++ b/source/core/resources/resource_pack.cpp @@ -0,0 +1,209 @@ +#include "core/resources/resource_pack.hpp" + +#include +#include +#include +#include +#include + +namespace Resource { + + const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY"; + + namespace { + constexpr const char* MAGIC = "AE26"; + constexpr std::uint32_t VERSION = 1; + } // namespace + + auto Pack::calculateChecksum(const std::vector& data) -> std::uint32_t { + // djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai + // reutilitzem eines de diagnòstic entre projectes. + std::uint32_t checksum = 0x12345678; + for (unsigned char b : data) { + // cppcheck-suppress useStlAlgorithm + checksum = ((checksum << 5) + checksum) + b; + } + return checksum; + } + + void Pack::xorInPlace(std::vector& data, const std::string& key) { + if (key.empty()) { return; } + for (std::size_t i = 0; i < data.size(); ++i) { + data[i] ^= static_cast(key[i % key.length()]); + } + } + + auto Pack::loadPack(const std::string& pack_file) -> bool { + std::ifstream file(pack_file, std::ios::binary); + if (!file) { return false; } + + std::array header{}; + file.read(header.data(), 4); + if (std::string(header.data(), 4) != MAGIC) { + std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n'; + return false; + } + + std::uint32_t version = 0; + file.read(reinterpret_cast(&version), sizeof(version)); + if (version != VERSION) { + std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n'; + return false; + } + + std::uint32_t resource_count = 0; + file.read(reinterpret_cast(&resource_count), sizeof(resource_count)); + + resources_.clear(); + resources_.reserve(resource_count); + + for (std::uint32_t i = 0; i < resource_count; ++i) { + std::uint32_t filename_length = 0; + file.read(reinterpret_cast(&filename_length), sizeof(filename_length)); + + std::string filename(filename_length, '\0'); + file.read(filename.data(), filename_length); + + PackEntry 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; + } + + std::uint64_t data_size = 0; + file.read(reinterpret_cast(&data_size), sizeof(data_size)); + + data_.resize(data_size); + file.read(reinterpret_cast(data_.data()), static_cast(data_size)); + + xorInPlace(data_, DEFAULT_ENCRYPT_KEY); + return true; + } + + auto Pack::savePack(const std::string& pack_file) const -> bool { + std::ofstream file(pack_file, std::ios::binary); + if (!file) { + std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n'; + return false; + } + + file.write(MAGIC, 4); + + std::uint32_t version = VERSION; + file.write(reinterpret_cast(&version), sizeof(version)); + + auto resource_count = static_cast(resources_.size()); + file.write(reinterpret_cast(&resource_count), sizeof(resource_count)); + + for (const auto& [filename, entry] : resources_) { + auto filename_length = static_cast(filename.length()); + file.write(reinterpret_cast(&filename_length), sizeof(filename_length)); + file.write(filename.c_str(), filename_length); + 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)); + } + + std::vector encrypted = data_; + xorInPlace(encrypted, DEFAULT_ENCRYPT_KEY); + + std::uint64_t data_size = encrypted.size(); + file.write(reinterpret_cast(&data_size), sizeof(data_size)); + file.write(reinterpret_cast(encrypted.data()), static_cast(data_size)); + + return true; + } + + auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool { + std::ifstream file(filepath, std::ios::binary | std::ios::ate); + if (!file) { + std::cerr << "Resource::Pack: could not open file: " << filepath << '\n'; + return false; + } + + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector file_data(file_size); + if (!file.read(reinterpret_cast(file_data.data()), file_size)) { + std::cerr << "Resource::Pack: could not read file: " << filepath << '\n'; + return false; + } + + PackEntry entry; + entry.filename = filename; + entry.offset = data_.size(); + entry.size = file_data.size(); + entry.checksum = calculateChecksum(file_data); + + data_.insert(data_.end(), file_data.begin(), file_data.end()); + resources_[filename] = entry; + return true; + } + + auto Pack::addDirectory(const std::string& directory) -> bool { + if (!std::filesystem::exists(directory)) { + std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n'; + return false; + } + + // cppcheck-suppress useStlAlgorithm + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (!entry.is_regular_file()) { continue; } + + std::string filepath = entry.path().string(); + std::string filename = std::filesystem::relative(entry.path(), directory).string(); + std::ranges::replace(filename, '\\', '/'); + + if (!addFile(filename, filepath)) { return false; } + } + return true; + } + + auto Pack::getResource(const std::string& filename) const -> std::vector { + auto it = resources_.find(filename); + if (it == resources_.end()) { return {}; } + + const PackEntry& entry = it->second; + if (entry.offset + entry.size > data_.size()) { + std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n'; + return {}; + } + + std::vector result( + data_.begin() + static_cast(entry.offset), + data_.begin() + static_cast(entry.offset + entry.size)); + + std::uint32_t checksum = calculateChecksum(result); + if (checksum != entry.checksum) { + std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n'; + } + return result; + } + + auto Pack::hasResource(const std::string& filename) const -> bool { + return resources_.contains(filename); + } + + void Pack::clear() { + resources_.clear(); + data_.clear(); + } + + auto Pack::getResourceCount() const -> std::size_t { + return resources_.size(); + } + + auto Pack::getResourceList() const -> std::vector { + std::vector result; + result.reserve(resources_.size()); + for (const auto& [filename, entry] : resources_) { + result.push_back(filename); + } + return result; + } + +} // namespace Resource diff --git a/source/core/resources/resource_pack.hpp b/source/core/resources/resource_pack.hpp new file mode 100644 index 0000000..da26fa4 --- /dev/null +++ b/source/core/resources/resource_pack.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Resource { + + // Entrada d'un recurs dins el pack. + struct PackEntry { + std::string filename; + std::uint64_t offset{0}; + std::uint64_t size{0}; + std::uint32_t checksum{0}; + }; + + // Pack binari de recursos carregat a memòria. Format: + // Header: "AE26" (4 bytes) + version uint32 + resource_count uint32 + // Index: per cada recurs -> filename_len uint32 + filename + // + offset uint64 + size uint64 + checksum uint32 + // Payload: data_size uint64 + bytes xifrats amb XOR (DEFAULT_ENCRYPT_KEY) + // La classe té dos rols: (a) loader en temps d'execució — carregar un + // .pack del disc i obtindre els bytes d'un recurs per nom; (b) builder + // usat per l'eina `pack_resources` per a empaquetar `data/`. + class Pack { + public: + Pack() = default; + ~Pack() = default; + + Pack(const Pack&) = delete; + auto operator=(const Pack&) -> Pack& = delete; + Pack(Pack&&) = default; + auto operator=(Pack&&) -> Pack& = default; + + // I/O del fitxer. `loadPack` retorna false silenciosament si el + // fitxer no existeix (el caller decideix si cal fallback). + auto loadPack(const std::string& pack_file) -> bool; + auto savePack(const std::string& pack_file) const -> bool; + + // Builders usats per l'eina `pack_resources`. + auto addFile(const std::string& filename, const std::string& filepath) -> bool; + auto addDirectory(const std::string& directory) -> bool; + + [[nodiscard]] auto getResource(const std::string& filename) const -> std::vector; + [[nodiscard]] auto hasResource(const std::string& filename) const -> bool; + + void clear(); + [[nodiscard]] auto getResourceCount() const -> std::size_t; + [[nodiscard]] auto getResourceList() const -> std::vector; + + static const std::string DEFAULT_ENCRYPT_KEY; + + private: + std::unordered_map resources_; + std::vector data_; + + static auto calculateChecksum(const std::vector& data) -> std::uint32_t; + static void xorInPlace(std::vector& data, const std::string& key); + }; + +} // namespace Resource diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 2f50007..5ab36d0 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -40,13 +40,26 @@ Director::Director() { std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); } - // Rutes dels recursos relatives a l'executable, no al CWD. SDL_GetBasePath - // retorna la carpeta del binari al desplegament portable i Contents/ - // Resources/ dins d'un .app a macOS. Ha de fer-se abans de qualsevol - // loader que passi per Resource::Helper::resolve (Locale, KeyConfig, - // Resource::Cache, PaletteLoader, etc.). + // Sistema de recursos: el pack viu al costat del binari (build/), el + // regenera CMake a cada build. En Debug habilitem el fallback a disc + // apuntant al source tree perquè editar YAML/GIF dins `data/` es vegi + // sense re-packejar; en Release el pack és obligatori. + std::string pack_file = "resources.pack"; if (const char* base = SDL_GetBasePath()) { + pack_file = std::string(base) + "resources.pack"; +#ifndef _DEBUG Resource::Helper::setDataRoot(base); +#endif + } +#ifdef _DEBUG + Resource::Helper::setDataRoot(AEE_SOURCE_DIR); + constexpr bool ENABLE_FALLBACK = true; +#else + constexpr bool ENABLE_FALLBACK = false; +#endif + if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) { + std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n"); + std::exit(1); } // Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega @@ -207,6 +220,7 @@ Director::~Director() { // desfà les llibreries abans que corren els destructors estàtics. current_scene_.reset(); Resource::Cache::get().clearAudio(); + Resource::Helper::shutdownResourceSystem(); GamepadManager::get().shutdown(); Audio::destroy(); Screen::get().shutdown(); diff --git a/source/game/entities/entity_loader.cpp b/source/game/entities/entity_loader.cpp index 4ff809d..34c8a85 100644 --- a/source/game/entities/entity_loader.cpp +++ b/source/game/entities/entity_loader.cpp @@ -1,6 +1,5 @@ #include "game/entities/entity_loader.hpp" -#include #include #include @@ -31,23 +30,26 @@ namespace EntityLoader { auto it = cache.find(name); if (it != cache.end()) { return it->second; } - const std::string FULL = Resource::Helper::resolve(templatePath(name)); - std::ifstream file(FULL); - if (!file.is_open()) { - throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL); + const auto PATH = templatePath(name); + auto bytes = Resource::Helper::loadFile(PATH); + if (bytes.empty()) { + throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + PATH); } - fkyaml::node node = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + fkyaml::node node = fkyaml::node::deserialize(begin, end); cache.emplace(name, node); return node; } auto readSpawns(const std::string& level_yaml_path) -> std::vector { - const auto FULL = Resource::Helper::resolve(level_yaml_path); - std::ifstream file(FULL); - if (!file.is_open()) { - throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL); + auto bytes = Resource::Helper::loadFile(level_yaml_path); + if (bytes.empty()) { + throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + level_yaml_path); } - fkyaml::node root = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + fkyaml::node root = fkyaml::node::deserialize(begin, end); std::vector spawns; if (!root.contains("entities")) { return spawns; } const auto& list = root["entities"]; diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 6cec2bb..dbe5787 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -47,18 +46,19 @@ namespace { } // namespace void Player::loadFrom(const std::string& yaml_path) { - const auto FULL = Resource::Helper::resolve(yaml_path); - std::ifstream file(FULL); - if (!file.is_open()) { - throw std::runtime_error("Player: no s'ha pogut obrir " + FULL); + auto bytes = Resource::Helper::loadFile(yaml_path); + if (bytes.empty()) { + throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path); } - auto yaml = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); sprite_w = yaml["frameWidth"].get_value(); sprite_h = yaml["frameHeight"].get_value(); if (!yaml.contains("collision")) { - throw std::runtime_error("Player: manca bloc `collision:` a " + FULL); + throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path); } const auto& c = yaml["collision"]; collision_size = c["size"].get_value(); diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index fc48bf9..32c417a 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -1,6 +1,5 @@ #include "game/map/map.hpp" -#include #include #include @@ -22,14 +21,15 @@ namespace { namespace Map { void loadFrom(const std::string& yaml_path) { - const auto FULL = Resource::Helper::resolve(yaml_path); - std::ifstream file(FULL); - if (!file.is_open()) { - throw std::runtime_error("Map: no s'ha pogut obrir " + FULL); + auto bytes = Resource::Helper::loadFile(yaml_path); + if (bytes.empty()) { + throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path); } - auto yaml = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); if (!yaml.contains("geometry")) { - throw std::runtime_error("Map: manca bloc `geometry:` a " + FULL); + throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path); } const auto& g = yaml["geometry"]; @@ -78,7 +78,7 @@ namespace Map { std::cout << "Map: geometria " << grid_cols << "x" << grid_rows << " sarcs " << sarc_w << "x" << sarc_h << " pitch " << pitch_x << "x" << pitch_y - << " des de " << FULL << '\n'; + << " des de " << yaml_path << '\n'; } auto getSarcRect(int col, int row) -> SDL_FRect { diff --git a/source/game/map/tilemap.cpp b/source/game/map/tilemap.cpp index 7cdf5a8..857a2d2 100644 --- a/source/game/map/tilemap.cpp +++ b/source/game/map/tilemap.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -68,13 +67,13 @@ namespace { } // namespace Tilemap::Tilemap(const std::string& yaml_path) { - const auto FULL = Resource::Helper::resolve(yaml_path); - std::ifstream file(FULL); - if (!file.is_open()) { - throw std::runtime_error("Tilemap: no s'ha pogut obrir " + FULL); + auto bytes = Resource::Helper::loadFile(yaml_path); + if (bytes.empty()) { + throw std::runtime_error("Tilemap: no s'ha pogut obrir " + yaml_path); } - - auto yaml = fkyaml::node::deserialize(file); + const auto* begin = reinterpret_cast(bytes.data()); + const auto* end = begin + bytes.size(); + auto yaml = fkyaml::node::deserialize(begin, end); const auto TILESET_PATH = yaml["tileset"].get_value(); const auto TILE_SIZE = yaml["tile_size"].get_value(); @@ -113,7 +112,7 @@ Tilemap::Tilemap(const std::string& yaml_path) { } std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles (" - << width_px_ << "x" << height_px_ << " px) des de " << FULL << '\n'; + << width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n'; } void Tilemap::render() const { diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 0c36559..e88e93d 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -107,9 +107,9 @@ namespace Defaults { // Placeholders (LOGO / TITLE / INTRO): font temporal fins que els // placeholders es substitueixin per les seues escenes finals. - constexpr const char* PLACEHOLDER_NAME = "ari-w9500"; - constexpr const char* PLACEHOLDER_GIF = "data/fonts/ari-w9500.gif"; - constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500.fnt"; + constexpr const char* PLACEHOLDER_NAME = "ari-w9500-condensed-display"; + constexpr const char* PLACEHOLDER_GIF = "data/fonts/ari-w9500-condensed-display.gif"; + constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500-condensed-display.fnt"; constexpr int PLACEHOLDER_KERNING = 1; // Menú de servei (F9): títol en display normal, items en la diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index c9f2f96..1c4a10c 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.20.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.21.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas { diff --git a/tools/pack_resources/pack_resources.cpp b/tools/pack_resources/pack_resources.cpp new file mode 100644 index 0000000..89bb28d --- /dev/null +++ b/tools/pack_resources/pack_resources.cpp @@ -0,0 +1,98 @@ +#include +#include +#include + +#include "core/resources/resource_pack.hpp" + +namespace { + + void showHelp() { + std::cout << "aee_2026 - Resource Packer\n"; + std::cout << "==========================\n"; + std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n"; + std::cout << "Options:\n"; + std::cout << " --help Show this help message\n"; + std::cout << " --list List contents of an existing pack file\n\n"; + std::cout << "Arguments:\n"; + std::cout << " input_dir Directory to pack (default: data)\n"; + std::cout << " output_file Pack file name (default: resources.pack)\n\n"; + std::cout << "Examples:\n"; + std::cout << " pack_resources # data -> resources.pack\n"; + std::cout << " pack_resources data build/resources.pack # data -> build/resources.pack\n"; + std::cout << " pack_resources --list build/resources.pack\n"; + } + + void listPackContents(const std::string& pack_file) { + Resource::Pack pack; + if (!pack.loadPack(pack_file)) { + std::cerr << "Error: cannot open pack file: " << pack_file << '\n'; + return; + } + auto resources = pack.getResourceList(); + std::cout << "Pack file: " << pack_file << '\n'; + std::cout << "Resources: " << resources.size() << '\n'; + for (const auto& r : resources) { std::cout << " " << r << '\n'; } + } + +} // namespace + +int main(int argc, char* argv[]) { + std::string data_dir = "data"; + std::string output_file = "resources.pack"; + bool list_mode = false; + bool data_dir_set = false; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + showHelp(); + return 0; + } + if (arg == "--list") { + list_mode = true; + if (i + 1 < argc) { output_file = argv[++i]; } + continue; + } + if (!arg.empty() && arg[0] != '-') { + if (!data_dir_set) { + data_dir = arg; + data_dir_set = true; + } else { + output_file = arg; + } + } + } + + if (list_mode) { + listPackContents(output_file); + return 0; + } + + std::cout << "aee_2026 - Resource Packer\n==========================\n"; + std::cout << "Input directory: " << data_dir << '\n'; + std::cout << "Output file: " << output_file << '\n'; + + if (!std::filesystem::exists(data_dir)) { + std::cerr << "Error: input directory does not exist: " << data_dir << '\n'; + return 1; + } + + Resource::Pack pack; + std::cout << "Scanning and packing resources...\n"; + if (!pack.addDirectory(data_dir)) { + std::cerr << "Error: failed to add directory to pack\n"; + return 1; + } + std::cout << "Found " << pack.getResourceCount() << " resources\n"; + + std::cout << "Saving pack file...\n"; + if (!pack.savePack(output_file)) { + std::cerr << "Error: failed to save pack file\n"; + return 1; + } + + auto file_size = std::filesystem::file_size(std::filesystem::path(output_file)); + std::cout << "Pack file created: " << output_file << " (" + << (static_cast(file_size) / 1024.0 / 1024.0) << " MB)\n"; + return 0; +}