#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" #include "scenes/banner_scene.hpp" #include "scenes/mort_scene.hpp" #include "scenes/scene.hpp" #include "scenes/scene_registry.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()) { // 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) { if (auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) { scene->onEnter(); Uint32 last = SDL_GetTicks(); while (!scene->done() && !JG_Quitting()) { const Uint32 now = SDL_GetTicks(); scene->tick(static_cast(now - last)); last = now; JD8_Flip(); // presenta i cedix al Director } gameState = scene->nextState(); 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; } } } } } // namespace 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. auto& registry = scenes::SceneRegistry::instance(); registry.registerScene(100, [] { return std::make_unique(); }); // BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix // pel switch intern llegint info::ctx.num_piramide). for (int p = 2; p <= 5; ++p) { registry.registerScene(p, [] { return std::make_unique(); }); } 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::setup() { // Els buffers són membres (director.hpp); només els inicialitzem. std::memset(game_frame_, 0, sizeof(game_frame_)); std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_)); has_frame_ = false; } bool Director::iterate() { if (GameFiber::is_done() || quit_requested_) { // 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(); } return 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) const Uint32 frame_start = SDL_GetTicks(); 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 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()) { return false; } std::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_) { std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_)); Screen::get()->present(presentation_buffer_); } // Límit de framerate segons VSync. // Nota: quan el runtime posseïx el main loop (SDL_AppIterate / // emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode // que es basa en el timing intern de SDL en lloc del delay explícit. const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC; const Uint32 elapsed = SDL_GetTicks() - frame_start; if (elapsed < target_ms) { SDL_Delay(target_ms - elapsed); } return true; } void Director::teardown() { // Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del // sistema), li donem l'oportunitat de tornar net. JG_QuitSignal(); while (!GameFiber::is_done()) { GameFiber::resume(); } } void Director::run() { setup(); while (true) { pollAllEvents(); if (!iterate()) break; } teardown(); } void Director::pollAllEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { handleEvent(event); } } void Director::handleEvent(const SDL_Event& 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); return; } // 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(); return; } // 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; return; } // 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; return; } // 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; return; } // 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; return; } // 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; return; } if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { return; // 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; return; } // 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; } return; // 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 return; } 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); }