diff --git a/CLAUDE.md b/CLAUDE.md index f8c5307..9afdcf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` ( - Collision detection, scoring, lives, level progression - Visible animation cadence (once translated to ms, must look identical) - Difficulty curves and cinematic timings -- Cheat codes (`reviu`, `alone`, `obert`) — currently broken, should be restored +- Cheat codes (`reviu`, `alone`, `obert`) - Original palettes, fades, music cues **Free to change** (internal representation): @@ -235,10 +235,10 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text ### Known Issues & Technical Debt 1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround -2. **Cheats are broken (`reviu`, `alone`, `obert`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. **Now fixable** — scheduled for Phase 1 of modernization since jail is no longer off-limits. +2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII. 3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX. -4. **Raw `malloc`/`free` in gameplay structs**: `Sprite`/`Entitat` use `malloc` for `Frame[]` and `Animacio[]`; `jfile.cpp` uses a global `scratch[255]` buffer (UB under concurrent calls); `jail_audio.cpp` mixes `new`/`malloc`/`SDL_malloc`. Scheduled for Phase 1 (RAII pass). -5. **Blocking loops in cinematics and fades**: `ModuleSequence::doIntro()` has 15+ `while(!JG_ShouldUpdate())` spin-waits; `JD8_FadeOut`/`JD8_FadeToPal` run 32 internal iterations calling `JD8_Flip`. Incompatible with `SDL_AppIterate`. Scheduled for Phase 2 (state-machine refactor). +4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector` i `std::vector` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque. +5. ~~**Blocking loops in cinematics and fades**~~: Fixed. La migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 1–9) ha eliminat tots els `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()`. Les cinemàtiques ara són tick-based amb acumuladors ms. `JD8_FadeOut`/`JD8_FadeToPal` encara tenen el seu bucle intern de 32 passos (usat per a transicions fora d'escena com al final de `ModuleGame`); el wrapper tick-based `scenes::PaletteFade` el consumeix un pas per tick quan es crida des d'una escena. 6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::run()`. Ported from the `jaildoctors_dilemma` codebase. 7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5. Replaced by a cooperative `GameFiber` (ucontext on POSIX, Fibers API on Windows). `JD8_Flip()` calls `GameFiber::yield()`, Director calls `GameFiber::resume()` once per frame. Zero threads, zero mutexes. Emscripten fiber backend still pending for Phase 7. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5258762..514dd82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,6 @@ set(APP_SOURCES source/game/mapa.cpp source/game/marcador.cpp source/game/modulegame.cpp - source/game/modulesequence.cpp source/game/momia.cpp source/game/prota.cpp source/game/sprite.cpp diff --git a/source/core/jail/jdraw8.cpp b/source/core/jail/jdraw8.cpp index 09febce..e36170a 100644 --- a/source/core/jail/jdraw8.cpp +++ b/source/core/jail/jdraw8.cpp @@ -45,13 +45,10 @@ JD8_Surface JD8_NewSurface() { } JD8_Surface JD8_LoadSurface(const char* file) { - int filesize = 0; - char* buffer = file_getfilebuffer(file, filesize); + auto buffer = file_readfile(file); unsigned short w, h; - Uint8* pixels = LoadGif((unsigned char*)buffer, &w, &h); - - free(buffer); + Uint8* pixels = LoadGif(reinterpret_cast(buffer.data()), &w, &h); if (pixels == NULL) { printf("Unable to load bitmap: %s\n", SDL_GetError()); @@ -66,13 +63,8 @@ JD8_Surface JD8_LoadSurface(const char* file) { } JD8_Palette JD8_LoadPalette(const char* file) { - int filesize = 0; - char* buffer = NULL; - buffer = file_getfilebuffer(file, filesize); - - JD8_Palette palette = (JD8_Palette)LoadPalette((unsigned char*)buffer); - - return palette; + auto buffer = file_readfile(file); + return (JD8_Palette)LoadPalette(reinterpret_cast(buffer.data())); } void JD8_SetScreenPalette(JD8_Palette palette) { diff --git a/source/core/jail/jfile.cpp b/source/core/jail/jfile.cpp index e8f179b..8e8dfcb 100644 --- a/source/core/jail/jfile.cpp +++ b/source/core/jail/jfile.cpp @@ -147,12 +147,12 @@ FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool bi return f; } -char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate) { +std::vector file_readfile(const char* resourcename) { + int filesize = 0; FILE* f = file_getfilepointer(resourcename, filesize, true); - if (!f) return nullptr; - char* buffer = static_cast(malloc(zero_terminate ? filesize + 1 : filesize)); - fread(buffer, filesize, 1, f); - if (zero_terminate) buffer[filesize] = 0; + if (!f) return {}; + std::vector buffer(filesize); + fread(buffer.data(), filesize, 1, f); fclose(f); return buffer; } diff --git a/source/core/jail/jfile.hpp b/source/core/jail/jfile.hpp index b04cf23..c6703d6 100644 --- a/source/core/jail/jfile.hpp +++ b/source/core/jail/jfile.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + #define SOURCE_FILE 0 #define SOURCE_FOLDER 1 @@ -12,7 +14,12 @@ void file_setresourcefolder(const char* str); void file_setsource(const int src); FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary = false); -char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate = false); + +// Llig tot el contingut d'un recurs (fitxer solt o entrada del .jrf). +// Retorna un vector buit si el recurs no existeix. El vector es destrueix +// automàticament en eixir d'àmbit — no fa falta cap free() manual. Mida = +// bytes llegits (el buffer no està null-terminated). +std::vector file_readfile(const char* resourcename); const char* file_getconfigvalue(const char* key); void file_setconfigvalue(const char* key, const char* value); diff --git a/source/core/locale/locale.cpp b/source/core/locale/locale.cpp index d908721..a05edb9 100644 --- a/source/core/locale/locale.cpp +++ b/source/core/locale/locale.cpp @@ -27,14 +27,12 @@ namespace Locale { } bool load(const char* filename) { - int size = 0; - char* buffer = file_getfilebuffer(filename, size, true); - if (!buffer || size <= 0) { + auto buffer = file_readfile(filename); + if (buffer.empty()) { std::cerr << "Locale: unable to load " << filename << '\n'; return false; } - std::string content(buffer, size); - free(buffer); + std::string content(buffer.data(), buffer.size()); try { auto yaml = fkyaml::node::deserialize(content); diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 3635fce..5341aa6 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -62,15 +62,13 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t { // --- Càrrega de font --- void Text::loadFont(const char* fnt_file) { - int filesize = 0; - char* buffer = file_getfilebuffer(fnt_file, filesize, true); - if (!buffer) { + auto buffer = file_readfile(fnt_file); + if (buffer.empty()) { std::cerr << "Text: unable to load font file: " << fnt_file << '\n'; return; } - std::istringstream stream(std::string(buffer, filesize)); - free(buffer); + std::istringstream stream(std::string(buffer.data(), buffer.size())); std::string line; int glyph_index = 0; @@ -128,15 +126,14 @@ void Text::loadFont(const char* fnt_file) { } void Text::loadBitmap(const char* gif_file) { - int filesize = 0; - char* buffer = file_getfilebuffer(gif_file, filesize); - if (!buffer) { + auto buffer = file_readfile(gif_file); + if (buffer.empty()) { std::cerr << "Text: unable to load bitmap: " << gif_file << '\n'; return; } // Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian) - auto* raw = reinterpret_cast(buffer); + auto* raw = reinterpret_cast(buffer.data()); int w = raw[6] | (raw[7] << 8); int h = raw[8] | (raw[9] << 8); @@ -144,7 +141,6 @@ void Text::loadBitmap(const char* gif_file) { Uint8* pixels = LoadGif(raw, &gw, &gh); if (!pixels) { std::cerr << "Text: unable to decode GIF: " << gif_file << '\n'; - free(buffer); return; } @@ -152,7 +148,6 @@ void Text::loadBitmap(const char* gif_file) { bitmap_height_ = h; bitmap_ = pixels; - free(buffer); std::cout << "Text: bitmap loaded " << w << "x" << h << '\n'; } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 3ab5f86..34066b0 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -18,7 +18,6 @@ #include "core/system/fiber.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" -#include "game/modulesequence.hpp" #include "game/options.hpp" #include "scenes/banner_scene.hpp" #include "scenes/credits_scene.hpp" @@ -38,10 +37,11 @@ Director* Director::instance_ = nullptr; namespace { -// Entry del fiber del joc. Executa la màquina d'estats que alterna entre -// ModuleSequence (state=1) i ModuleGame (state=0) fins que el joc demana -// eixir. Quan el joc crida JD8_Flip() des de dins d'aquest fiber, el -// control torna automàticament al Director. +// Entry del fiber del joc. Alterna entre la capa `scenes::` (cinemàtiques +// i menús, triats per `info::ctx.num_piramide` via el SceneRegistry) i +// ModuleGame (gameplay) fins que el joc demana eixir. Quan el codi de +// gameplay o una escena crida JD8_Flip(), el control torna automàticament +// al Director via `GameFiber::yield()`. void gameFiberEntry() { info::ctx.num_habitacio = Options::game.habitacio_inicial; info::ctx.num_piramide = Options::game.piramide_inicial; @@ -60,51 +60,41 @@ void gameFiberEntry() { int gameState = 1; while (gameState != -1 && !JG_Quitting()) { - // Mode "Scene nova": si el state actual és de seqüència i el - // registry té una escena migrada per al num_piramide, l'executem - // amb un mini-loop tick-based. El codi de la Scene no toca - // fibers; el JD8_Flip() entre ticks és el que cedeix al Director. - if (gameState == 1) { - // Replica del redirect que el `ModuleSequence::Go()` vell feia - // al principi: si el jugador arriba a la piràmide Secreta (6) - // sense prou diners, salta directament als slides de fracàs (7). - // Cal fer-ho ací perquè el SceneRegistry consulta num_piramide - // abans del fallback legacy; mentres doSecreta no estiga migrada - // també continuarà al Go() vell amb num_piramide ja corregida. - if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) { - info::ctx.num_piramide = 7; - } - - if (auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) { - scene->onEnter(); - Uint32 last = SDL_GetTicks(); - while (!scene->done() && !JG_Quitting()) { - JI_Update(); // refresca key_pressed/any_key per a les escenes - const Uint32 now = SDL_GetTicks(); - scene->tick(static_cast(now - last)); - last = now; - JD8_Flip(); // presenta i cedix al Director - } - gameState = scene->nextState(); - continue; - } + if (gameState == 0) { + // Gameplay pur (ModuleGame encara és cooperatiu-clàssic: conté + // el seu while intern i crida JD8_Flip manualment). Fora + // d'abast de la migració scenes:: — es tractarà en una fase + // posterior quan el fiber es puga eliminar. + auto* mg = new ModuleGame(); + gameState = mg->Go(); + delete mg; + continue; } - // Fallback al codi legacy (encara no migrat a Scene). - switch (gameState) { - case 0: { - auto* moduleGame = new ModuleGame(); - gameState = moduleGame->Go(); - delete moduleGame; - break; - } - case 1: { - auto* moduleSequence = new ModuleSequence(); - gameState = moduleSequence->Go(); - delete moduleSequence; - break; - } + // gameState == 1: dispatch a la capa scenes::. + // El vell `ModuleSequence::Go()` feia aquest redirect al principi: + // si el jugador arriba a la Secreta (6) sense prou diners, salta + // als slides de fracàs (7) abans de buscar l'escena al registry. + if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) { + info::ctx.num_piramide = 7; } + + auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide); + if (!scene) { + // State no registrat — indica un bug del dispatcher o del + // registre d'escenes. Eixim ordenadament en lloc de cremar CPU. + break; + } + scene->onEnter(); + Uint32 last = SDL_GetTicks(); + while (!scene->done() && !JG_Quitting()) { + JI_Update(); // refresca key_pressed/any_key per a les escenes + const Uint32 now = SDL_GetTicks(); + scene->tick(static_cast(now - last)); + last = now; + JD8_Flip(); // presenta i cedix al Director + } + gameState = scene->nextState(); } } @@ -114,10 +104,10 @@ void Director::init() { instance_ = new Director(); Gamepad::init(); - // Registre d'escenes migrades. Cada entrada = una funció del vell - // ModuleSequence reescrita com a `scenes::*Scene`. Mentre vagen - // caient, el fallback al switch legacy de gameFiberEntry deixa de - // rebre aquests states. + // Registre d'escenes. Cada entrada = un state_key (`num_piramide`) + // amb una factory de `scenes::Scene`. El gameFiberEntry consulta + // aquest registry per a tots els states de seqüència; si una clau + // no apareix ací, el fiber eixirà del loop. auto& registry = scenes::SceneRegistry::instance(); registry.registerScene(0, [] { return std::make_unique(); }); registry.registerScene(100, [] { return std::make_unique(); }); @@ -136,8 +126,8 @@ void Director::init() { registry.registerScene(8, [] { return std::make_unique(); }); // State 255 (intro): dues variants segons `Options::game.use_new_logo`. // La factory tria a runtime — així es pot togglar des del menú sense - // re-registrar. Les dues escenes acaben delegant a doIntroSprites - // legacy fins al Step 9 de la migració. + // re-registrar. Les dues escenes construeixen una IntroSpritesScene + // com a sub-escena per a la part d'animacions de sprites. registry.registerScene(255, []() -> std::unique_ptr { if (Options::game.use_new_logo) { return std::make_unique(); diff --git a/source/game/modulegame.cpp b/source/game/modulegame.cpp index 1a74fed..c6198ef 100644 --- a/source/game/modulegame.cpp +++ b/source/game/modulegame.cpp @@ -45,9 +45,9 @@ int ModuleGame::Go() { const char* music = info::ctx.num_piramide == 3 ? "00000008.ogg" : (info::ctx.num_piramide == 2 ? "00000007.ogg" : (info::ctx.num_piramide == 6 ? "00000002.ogg" : "00000006.ogg")); const char* current_music = JA_GetMusicFilename(); if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !(strcmp(music, current_music) == 0)) { - int size; - char* buffer = file_getfilebuffer(music, size); - JA_PlayMusic(JA_LoadMusic((Uint8*)buffer, size, music)); + auto buffer = file_readfile(music); + JA_PlayMusic(JA_LoadMusic(reinterpret_cast(buffer.data()), + static_cast(buffer.size()), music)); } JD8_FadeToPal(JD8_LoadPalette(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif")); diff --git a/source/game/modulesequence.cpp b/source/game/modulesequence.cpp deleted file mode 100644 index 1d6fa65..0000000 --- a/source/game/modulesequence.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "game/modulesequence.hpp" - -#include "core/jail/jdraw8.hpp" -#include "core/jail/jgame.hpp" - -ModuleSequence::ModuleSequence() { -} - -ModuleSequence::~ModuleSequence(void) { -} - -int ModuleSequence::Go() { - if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) info::ctx.num_piramide = 7; - - // Totes les branques del vell switch han estat migrades a scenes: - // case 0 (Menú) → scenes::MenuScene - // case 1 i 7 (Slides) → scenes::SlidesScene - // case 2..5 (Banner) → scenes::BannerScene - // case 6 (Secreta) → scenes::SecretaScene - // case 8 (Credits) → scenes::CreditsScene - // case 100 (Mort) → scenes::MortScene - // case 255 (Intro) → scenes::IntroScene / scenes::IntroNewLogoScene - // (amb scenes::IntroSpritesScene com a sub-escena) - // El gameFiberEntry les dispatcha via SceneRegistry abans de caure a - // aquest Go() — així que en la pràctica ja no s'arriba ací. El codi - // que queda sota (JD8_FadeOut + transicions de num_piramide) serà - // eliminat al Step 10 del pla, junt amb ModuleSequence::Go() sencer. - - JD8_FadeOut(); - - if (JG_Quitting()) { - return -1; - } else { - if (info::ctx.num_piramide == 255) { - info::ctx.num_piramide = 0; - return 1; - } else if (info::ctx.num_piramide == 0) { - info::ctx.num_piramide = 1; - return 1; - } else if (info::ctx.num_piramide == 7) { - info::ctx.num_piramide = 8; - return 1; - } else if (info::ctx.num_piramide == 8) { - info::ctx.num_piramide = 255; - return 1; - } else if (info::ctx.num_piramide == 100) { - info::ctx.num_piramide = 0; - return 1; - } else { - return 0; - } - } -} diff --git a/source/game/modulesequence.hpp b/source/game/modulesequence.hpp deleted file mode 100644 index 4b208c1..0000000 --- a/source/game/modulesequence.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "game/info.hpp" - -class ModuleSequence { - public: - ModuleSequence(); - ~ModuleSequence(void); - - int Go(); - - // Totes les doX() han sigut migrades a escenes dins source/scenes/. - // El Step 10 del pla eliminarà ModuleSequence sencer, inclòs Go(), - // quan el gameFiberEntry deixe de cridar-lo com a fallback. -}; diff --git a/source/scenes/scene_utils.cpp b/source/scenes/scene_utils.cpp index 102d9c9..babb16f 100644 --- a/source/scenes/scene_utils.cpp +++ b/source/scenes/scene_utils.cpp @@ -9,13 +9,13 @@ namespace scenes { void playMusic(const char* filename, int loop) { if (!filename) return; - int size = 0; - char* buffer = file_getfilebuffer(filename, size); - if (!buffer) return; - // JA_LoadMusic fa una còpia del OGG comprimit (SDL_malloc), així que - // el `buffer` original es queda huérfano. Leak conegut heredat del - // codi original — es tractarà quan jfile tinga una API std::vector. - JA_PlayMusic(JA_LoadMusic(reinterpret_cast(buffer), size, filename), loop); + auto buffer = file_readfile(filename); + if (buffer.empty()) return; + // JA_LoadMusic fa una còpia interna del OGG comprimit (via SDL_malloc) + // per a stb_vorbis. El `buffer` local es destruirà en sortir d'àmbit. + JA_PlayMusic(JA_LoadMusic(reinterpret_cast(buffer.data()), + static_cast(buffer.size()), filename), + loop); } } // namespace scenes