Files
aee/source/core/system/director.cpp

324 lines
12 KiB
C++

#include "core/system/director.hpp"
#include <cstring>
#include <iostream>
#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/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 "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;
void Director::init() {
instance_ = new Director();
Gamepad::init();
}
void Director::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() {
// Llança el game thread
game_thread_ = std::thread(&Director::gameThreadFunc, this);
// Doble buffer: game_frame és el frame net del joc, presentation_buffer
// és el frame + overlay (es regenera cada iteració des de game_frame)
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)
// Bucle principal del director (no-bloquejant)
while (!game_thread_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). Lectura no
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
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;
}
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja).
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame.
bool new_frame = false;
if (!paused_) {
std::lock_guard lock(mutex_);
if (frame_ready_ && latest_frame_ != nullptr) {
memcpy(game_frame, latest_frame_, sizeof(game_frame));
frame_ready_ = false;
frame_consumed_ = true;
has_frame = true;
new_frame = true;
}
}
if (new_frame) {
frame_consumed_cv_.notify_one(); // desbloqueja el joc
}
// 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);
}
}
// Assegura que el game thread ix (despertar-lo per si està esperant)
quit_requested_ = true;
JG_QuitSignal();
{
std::lock_guard lock(mutex_);
frame_consumed_ = true;
}
frame_consumed_cv_.notify_all();
if (game_thread_.joinable()) {
game_thread_.join();
}
}
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 (sense reprendre la música,
// estem eixint): el game thread està bloquejat a publishFrame
// i necessita que Director consumeixca frames per despertar-lo
// i poder veure la senyal de quit.
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::publishFrame(Uint32* pixels) {
{
std::lock_guard lock(mutex_);
latest_frame_ = pixels;
frame_ready_ = true;
frame_consumed_ = false;
}
frame_produced_cv_.notify_one();
// Espera que el director consumeixca el frame
{
std::unique_lock lock(mutex_);
frame_consumed_cv_.wait(lock, [this] {
return frame_consumed_ || quit_requested_;
});
}
}
void Director::requestQuit() {
quit_requested_ = true;
JG_QuitSignal();
frame_consumed_cv_.notify_all();
frame_produced_cv_.notify_all();
}
auto Director::consumeKeyPressed() -> bool {
return key_pressed_.exchange(false);
}
void Director::gameThreadFunc() {
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 && !quit_requested_) {
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;
}
}
}
game_thread_done_ = true;
// Despertar el director per si esperava un frame
frame_produced_cv_.notify_all();
}