#include "core/system/director.hpp" #include #include #include "core/input/gamepad.hpp" #include "core/input/global_inputs.hpp" #include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" #include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" #include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" #include "core/system/fiber.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" #include "game/modulesequence.hpp" #include "game/options.hpp" // Cheats del joc original — declarats a jinput.cpp extern void JI_moveCheats(Uint8 new_key); 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. void gameFiberEntry() { info::ctx.num_habitacio = Options::game.habitacio_inicial; info::ctx.num_piramide = Options::game.piramide_inicial; info::ctx.diners = 0; info::ctx.diamants = 0; info::ctx.vida = Options::game.vides; info::ctx.momies = 0; info::ctx.nou_personatge = false; info::ctx.pepe_activat = false; FILE* ini = fopen("trick.ini", "rb"); if (ini != nullptr) { info::ctx.nou_personatge = true; fclose(ini); } int gameState = 1; while (gameState != -1 && !JG_Quitting()) { 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; } } } } } // namespace void Director::init() { instance_ = new Director(); Gamepad::init(); GameFiber::init(gameFiberEntry); } void Director::destroy() { GameFiber::destroy(); Gamepad::destroy(); delete instance_; instance_ = nullptr; } auto Director::get() -> Director* { return instance_; } void Director::togglePause() { paused_ = !paused_; if (paused_) { JA_PauseMusic(); } else { JA_ResumeMusic(); } } void Director::run() { // Doble buffer: game_frame és el frame net del joc, presentation_buffer // és el frame + overlay (es regenera cada iteració des de game_frame). // El doble buffer encara té sentit perquè el Director pot presentar // més frames que els que genera el joc (p.ex. durant pauses o mentre // el joc està en un "wait" intern que triga milisegons). Uint32 game_frame[320 * 200]{}; Uint32 presentation_buffer[320 * 200]{}; bool has_frame = false; constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior) while (!GameFiber::is_done() && !quit_requested_) { Uint32 frame_start = SDL_GetTicks(); handleEvents(); Gamepad::update(); KeyRemap::update(); GlobalInputs::handle(); Mouse::updateCursorVisibility(); // Bombeig de l'àudio: reomple l'stream de música i para els canals // drenats. Substituïx el callback de SDL_AddTimer de la versió // antiga — imprescindible per al futur port a emscripten. JA_Update(); // Dispara els crèdits cinematogràfics la primera vegada que el joc // arriba al menú del títol (info::ctx.num_piramide == 0). static bool credits_triggered = false; if (!credits_triggered && info::ctx.num_piramide == 0) { if (Options::game.show_title_credits) { Overlay::startCredits(); } credits_triggered = true; } // Si l'overlay ja no bloqueja ESC (timeout), desbloquegem if (esc_blocked_ && !Overlay::isEscConsumed()) { esc_blocked_ = false; } // Cedeix el control al fiber del joc. Quan retorne (per un // JD8_Flip() dins del joc) tindrem un nou frame a pixel_data. // Si estem en pausa, no executem el fiber: es queda congelat al // seu últim yield i continuem presentant l'últim frame conegut. if (!paused_) { GameFiber::resume(); if (GameFiber::is_done()) break; memcpy(game_frame, JD8_GetFramebuffer(), sizeof(game_frame)); has_frame = true; } // Presenta sempre: parteix del frame net del joc, afegeix overlay i envia if (has_frame) { memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer)); Screen::get()->present(presentation_buffer); } // Límit de framerate segons VSync Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC; Uint32 elapsed = SDL_GetTicks() - frame_start; if (elapsed < target_ms) { SDL_Delay(target_ms - elapsed); } } // Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press), // li donem l'oportunitat de tornar net: marquem quit i reprenem el // fiber fins que detecte JG_Quitting() i retorne de forma natural. JG_QuitSignal(); while (!GameFiber::is_done()) { GameFiber::resume(); } } void Director::handleEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_EVENT_QUIT) { JG_QuitSignal(); requestQuit(); } // Hot-plug de gamepad if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) { Gamepad::handleEvent(event); continue; } // Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) { Overlay::cancelCredits(); continue; } // Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 && event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) { menu_keys_held_[event.key.scancode] = false; continue; } // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { Menu::captureKey(event.key.scancode); menu_keys_held_[event.key.scancode] = true; continue; } // Pausa: F11 (o tecla configurada) pausa/reprén la simulació if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == Options::keys_gui.pause_toggle) { togglePause(); Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume")); menu_keys_held_[event.key.scancode] = true; continue; } // Menú: F12 (o tecla configurada) obre/tanca el menú flotant if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == Options::keys_gui.menu_toggle) { Menu::toggle(); JI_SetInputBlocked(Menu::isOpen()); menu_keys_held_[event.key.scancode] = true; continue; } // Si el menú està obert, consumeix tot l'input de teclat if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { Menu::close(); JI_SetInputBlocked(false); // Empassa l'ESC fins al release perquè el joc no la veja per polling esc_swallow_until_release_ = true; } else { Menu::handleKey(event.key.scancode); // El menú pot haver-se tancat (p.ex. Backspace al nivell arrel) if (!Menu::isOpen()) { JI_SetInputBlocked(false); } } menu_keys_held_[event.key.scancode] = true; continue; } if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { continue; // no deixem passar KEY_UP al joc tampoc } // Allibera el bloqueig d'ESC quan l'usuari la deixa anar if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) { esc_swallow_until_release_ = false; continue; } // ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) { esc_blocked_ = true; // Bloqueja ESC per polling immediatament if (!Overlay::isEscConsumed()) { // Primera pulsació: mostra notificació Overlay::handleEscape(); } else { // Segona pulsació: senyal d'eixida al joc esc_blocked_ = false; key_pressed_ = true; JG_QuitSignal(); // Si estem en pausa, la desactivem: el fiber del joc està // congelat i necessita ser reprès per veure la senyal de // quit i poder tornar de forma natural. paused_ = false; } continue; // no processa més aquest event } if (event.type == SDL_EVENT_KEY_UP) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { // Ja processat a KEY_DOWN, només deixem netejar el bloqueig // quan l'overlay faça timeout continue; } else { // Comprova si és una tecla GUI (no passa al joc) const auto sc = event.key.scancode; const bool is_gui_key = (sc == Options::keys_gui.dec_zoom || sc == Options::keys_gui.inc_zoom || sc == Options::keys_gui.fullscreen || sc == Options::keys_gui.toggle_shader || sc == Options::keys_gui.toggle_aspect_ratio || sc == Options::keys_gui.toggle_supersampling || sc == Options::keys_gui.next_shader || sc == Options::keys_gui.next_shader_preset || sc == Options::keys_gui.toggle_stretch_filter || sc == Options::keys_gui.toggle_render_info); if (!is_gui_key) { key_pressed_ = true; JI_moveCheats(sc); } } } Mouse::handleEvent(event); } } void Director::requestQuit() { quit_requested_ = true; JG_QuitSignal(); } auto Director::consumeKeyPressed() -> bool { return key_pressed_.exchange(false); }