Files
coffee-crisis-ae/source/core/system/director.cpp
T

532 lines
21 KiB
C++

// IWYU pragma: no_include <bits/chrono.h>
#include "core/system/director.hpp"
#include <SDL3/SDL.h> // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit
#include <cstdlib> // Para srand, exit, rand, EXIT_FAILURE
#include <ctime> // Para time
#include <fstream> // Para ifstream, ofstream
#include <iostream> // Para basic_ostream, operator<<, cerr
#include <memory> // Para make_unique, unique_ptr
#include <stdexcept> // Para runtime_error
#include <string> // Para allocator, basic_string, char_traits, operator+, string, operator==
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/input.hpp" // Para Input
#include "core/locale/lang.hpp" // Para setLanguage
#include "core/rendering/screen.hpp" // Para Screen
#include "core/resources/asset.hpp" // Para Asset
#include "core/resources/resource.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para initializeResourceSystem
#include "core/system/global_events.hpp" // Para GlobalEvents::handle
#include "core/system/section.hpp" // Para Name, Options, name, options, AttractMode, attract_mode
#include "core/system/shutdown.hpp" // Para resultToString, shutdownSystem, ShutdownResult
#include "core/system/system_utils.hpp" // Para createApplicationFolder, resultToString, Result
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/entities/player.hpp" // Para Player
#include "game/gameplay/manage_hiscore_table.hpp" // Para ManageHiScoreTable
#include "game/options.hpp" // Para Settings, loadFromFile, saveToFile, settings, setConfigFile, setControllersFile
#include "game/scenes/credits.hpp" // Para Credits
#include "game/scenes/game.hpp" // Para Game
#include "game/scenes/hiscore_table.hpp" // Para HiScoreTable
#include "game/scenes/instructions.hpp" // Para Instructions
#include "game/scenes/intro.hpp" // Para Intro
#include "game/scenes/logo.hpp" // Para Logo
#include "game/scenes/preload.hpp" // Para Preload
#include "game/scenes/title.hpp" // Para Title
#include "game/ui/notifier.hpp" // Para Notifier
#include "game/ui/service_menu.hpp" // Para ServiceMenu
#include "utils/param.hpp" // Para loadParamsFromFile
namespace {
// Llig un camp opcional d'un YAML cap a `dest`. Si no existeix, no toca `dest`.
// Si existeix però el tipus no encaixa, deixa el valor per defecte i avisa per stderr
// (un debug.yaml mal escrit no ha de tombar l'arrencada, però l'usuari ha de saber-ho).
template <typename T>
void loadYamlField(const fkyaml::node& yaml, const std::string& key, T& dest) {
if (!yaml.contains(key)) { return; }
try {
dest = yaml[key].get_value<T>();
} catch (...) {
std::cerr << "debug.yaml: valor invàlid per a '" << key << "', es manté el valor per defecte\n";
}
}
auto parseInitialSection(const std::string& value) -> Section::Name {
if (value == "logo") { return Section::Name::LOGO; }
if (value == "intro") { return Section::Name::INTRO; }
if (value == "title") { return Section::Name::TITLE; }
if (value == "credits") { return Section::Name::CREDITS; }
if (value == "instructions") { return Section::Name::INSTRUCTIONS; }
if (value == "hiscore") { return Section::Name::HI_SCORE_TABLE; }
return Section::Name::GAME; // "game" i qualsevol valor desconegut
}
auto parseInitialOptions(const std::string& value) -> Section::Options {
if (value == "none") { return Section::Options::NONE; }
if (value == "2p") { return Section::Options::GAME_PLAY_2P; }
if (value == "both") { return Section::Options::GAME_PLAY_BOTH; }
return Section::Options::GAME_PLAY_1P; // "1p" i qualsevol valor desconegut
}
} // namespace
// Constructor
Director::Director() {
Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;
// Establece el nivel de prioridad de la categoría de registro
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
SDL_SetLogPriority(SDL_LOG_CATEGORY_TEST, SDL_LOG_PRIORITY_ERROR);
// Inicia la semilla aleatoria usando el tiempo actual en segundos
std::srand(static_cast<unsigned int>(std::time(nullptr)));
std::cout << "Game start\n";
// Obtener la ruta del ejecutable desde SDL
#ifdef __EMSCRIPTEN__
// En Emscripten los assets viven en la raíz del MEMFS virtual (/data, /config),
// preloaded vía --preload-file en el linker. No hay ruta de ejecutable.
executable_path_ = "";
#else
const char* base_path = SDL_GetBasePath();
executable_path_ = (base_path != nullptr) ? base_path : "";
#endif
// Crea la carpeta del sistema donde guardar los datos persistentes
createSystemFolder("jailgames");
createSystemFolder("jailgames/coffee_crisis_arcade_edition");
// Establecer sección inicial según modo de compilación
#ifdef RECORDING
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_1P;
#elif _DEBUG
loadDebugConfig();
#else
Section::name = Section::Name::LOGO;
Section::options = Section::Options::NONE;
#endif
init();
}
Director::~Director() {
// Libera las secciones primero: sus destructores pueden tocar Audio/Resource/Screen,
// que close() destruye a continuación.
resetActiveSection();
close();
}
// Inicializa todo
void Director::init() {
// Configuración inicial de parametros
Asset::init(executable_path_); // Inicializa el sistema de gestión de archivos
// Determinar ruta del pack según la plataforma
#ifdef MACOS_BUNDLE
std::string pack_path = executable_path_ + "../Resources/resources.pack";
#else
std::string pack_path = executable_path_ + "resources.pack";
#endif
// Inicializar sistema de recursos con o sin fallback según el tipo de build
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// Release nativo: Sin fallback - Solo resources.pack (estricto)
ResourceHelper::initializeResourceSystem(pack_path, false);
#else
// Desarrollo o Emscripten: Con fallback - carga desde filesystem/MEMFS
ResourceHelper::initializeResourceSystem(pack_path, true);
#endif
loadAssets(); // Crea el índice de archivos
Input::init(Asset::get()->getPath("gamecontrollerdb.txt"), Asset::get()->getPath("controllers.json")); // Carga configuración de controles
Options::setConfigFile(Asset::get()->getPath("config.yaml")); // Establece el fichero de configuración
Options::setControllersFile(Asset::get()->getPath("controllers.json")); // Establece el fichero de configuración de mandos
Options::setPostFXFile(Asset::get()->getPath("postfx.yaml")); // Establece el fichero de presets PostFX
Options::setCrtPiFile(Asset::get()->getPath("crtpi.yaml")); // Establece el fichero de presets CrtPi
Options::loadFromFile(); // Carga el archivo de configuración
Options::loadPostFXFromFile(); // Carga los presets PostFX
Options::loadCrtPiFromFile(); // Carga los presets CrtPi
#ifdef __EMSCRIPTEN__
// En la versión web el navegador gestiona la ventana: ventana (no
// fullscreen — el canvas ya marca el área), integer scale para píxeles nítidos.
Options::window.zoom = 3;
Options::video.fullscreen = false;
Options::video.integer_scale = true;
// Precarga silenciosa: pantalla negra mientras el .data termina de descargarse.
Options::loading.show = false;
Options::loading.wait_for_input = false;
#endif
loadParams(); // Carga los parámetros del programa
loadScoreFile(); // Carga el archivo de puntuaciones
// Inicialización de subsistemas principales
Lang::setLanguage(Options::settings.language); // Carga el archivo de idioma
Screen::init(); // Inicializa la pantalla y el sistema de renderizado
Audio::init(); // Activa el sistema de audio
#ifdef _DEBUG
Resource::init(debug_config.resource_loading == "lazy" ? Resource::LoadingMode::LAZY_LOAD : Resource::LoadingMode::PRELOAD);
#else
Resource::init(Resource::LoadingMode::PRELOAD);
#endif
if (Resource::get()->getLoadingMode() == Resource::LoadingMode::PRELOAD) {
// Guarda la sección destino (la que fijó loadDebugConfig o el default)
// y redirige el arranque a la escena PRELOAD hasta que loadStep termine.
Section::post_preload = Section::name;
Section::name = Section::Name::PRELOAD;
Resource::get()->beginLoad();
} else {
// LAZY_LOAD: el constructor de Resource ya cargó lo esencial síncronamente;
// no hay fase de preload, pasamos directamente a post-boot.
finishBoot();
boot_loading_ = false;
}
// ServiceMenu/Notifier/getSingletons se mueven a finishBoot() — dependen
// de Resource y se inicializan al terminar la carga incremental.
}
// Inicializaciones que dependen del Resource cargado. Se llama desde iterate()
// cuando Resource::loadStep() devuelve true, con la ventana y el bucle vivos.
void Director::finishBoot() {
ServiceMenu::init(); // Inicializa el menú de servicio
Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones
Screen::get()->getSingletons(); // Obtiene los punteros al resto de singletones
// Restaura el vsync a la preferencia del usuario (beginLoad lo había puesto a false)
Screen::get()->setVSync(Options::video.vsync);
// Si NO estamos en modo "wait for input", transiciona ya al destino.
// Si wait_for_input está activo (y la pantalla es visible), nos quedamos
// en PRELOAD hasta que el usuario pulse tecla/botón.
if (!(Options::loading.show && Options::loading.wait_for_input)) {
Section::name = Section::post_preload;
}
}
// Cierra todo y libera recursos del sistema y de los singletons
void Director::close() {
// Guarda las opciones actuales en el archivo de configuración
Options::saveToFile();
// Libera los singletons y recursos en orden inverso al de inicialización
Notifier::destroy(); // Libera el sistema de notificaciones
ServiceMenu::destroy(); // Libera el sistema de menú de servicio
Input::destroy(); // Libera el sistema de entrada
Resource::destroy(); // Libera el sistema de recursos gráficos y de texto
Audio::destroy(); // Libera el sistema de audio
Screen::destroy(); // Libera el sistema de pantalla y renderizado
Asset::destroy(); // Libera el gestor de archivos
std::cout << "\nBye!\n";
// Libera todos los recursos de SDL
SDL_Quit();
// Apaga el sistema
shutdownSystem(Section::options == Section::Options::SHUTDOWN);
}
// Carga los parametros
void Director::loadParams() {
// Carga los parametros para configurar el juego
#ifdef ANBERNIC
const std::string PARAM_FILE_PATH = Asset::get()->getPath("param_320x240.txt");
#else
const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_file);
#endif
loadParamsFromFile(PARAM_FILE_PATH);
}
// Carga el fichero de puntuaciones
void Director::loadScoreFile() {
auto manager = std::make_unique<ManageHiScoreTable>(Options::settings.hi_score_table);
#ifdef _DEBUG
manager->clear();
#else
manager->loadFromFile(Asset::get()->getPath("score.bin"));
#endif
}
// Carga el indice de ficheros desde un fichero
void Director::loadAssets() {
#ifdef MACOS_BUNDLE
const std::string PREFIX = "/../Resources";
#else
const std::string PREFIX;
#endif
// Cargar la configuración de assets (también aplicar el prefijo al archivo de configuración)
std::string config_path = executable_path_ + PREFIX + "/config/assets.txt";
Asset::get()->loadFromFile(config_path, PREFIX, system_folder_);
// Si falta algun fichero, sale del programa
if (!Asset::get()->check()) {
throw std::runtime_error("Falta algun fichero");
}
}
// Carga debug.yaml desde la carpeta del sistema (solo en _DEBUG)
void Director::loadDebugConfig() {
const std::string DEBUG_FILE = system_folder_ + "/debug.yaml";
std::ifstream file(DEBUG_FILE);
if (!file.good()) {
// Crear fichero por defecto
std::ofstream out(DEBUG_FILE);
if (out.is_open()) {
out << "# Coffee Crisis Arcade Edition - Debug Configuration\n";
out << "# This file is only read in DEBUG builds.\n";
out << "#\n";
out << "# initial_section: logo, intro, title, game, credits, instructions, hiscore\n";
out << "# initial_options: none, 1p, 2p, both\n";
out << "# initial_stage: 0-based stage index (only when section is game)\n";
out << "# show_render_info: show FPS/driver/preset overlay\n";
out << "# resource_loading: preload, lazy\n";
out << "\n";
out << "initial_section: game\n";
out << "initial_options: 1p\n";
out << "initial_stage: 0\n";
out << "show_render_info: true\n";
out << "resource_loading: preload\n";
out << "autoplay: false\n";
out << "invincibility: false\n";
out.close();
}
// Usar defaults de DebugConfig
} else {
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
loadYamlField(yaml, "initial_section", debug_config.initial_section);
loadYamlField(yaml, "initial_options", debug_config.initial_options);
loadYamlField(yaml, "initial_stage", debug_config.initial_stage);
loadYamlField(yaml, "show_render_info", debug_config.show_render_info);
loadYamlField(yaml, "resource_loading", debug_config.resource_loading);
loadYamlField(yaml, "autoplay", debug_config.autoplay);
loadYamlField(yaml, "invincibility", debug_config.invincibility);
} catch (...) {
std::cout << "Error parsing debug.yaml, using defaults" << '\n';
}
}
Section::name = parseInitialSection(debug_config.initial_section);
Section::options = parseInitialOptions(debug_config.initial_options);
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
auto result = SystemUtils::createApplicationFolder(folder, system_folder_);
if (result != SystemUtils::Result::SUCCESS) {
std::cerr << "Error creando carpeta del sistema: "
<< SystemUtils::resultToString(result) << '\n';
exit(EXIT_FAILURE);
}
}
// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
void Director::resetActiveSection() {
preload_.reset();
logo_.reset();
intro_.reset();
title_.reset();
game_.reset();
instructions_.reset();
hi_score_table_.reset();
credits_.reset();
}
// Destruye la sección anterior y construye la nueva cuando Section::name cambia
void Director::handleSectionTransition() {
// RESET: recarga recursos y vuelve a LOGO (el propio reset() cambia Section::name)
if (Section::name == Section::Name::RESET) {
resetActiveSection(); // libera recursos actuales antes del reload
reset();
}
if (Section::name == last_built_section_name_) {
return; // ya tenemos la sección correcta viva
}
// Destruye la sección anterior
resetActiveSection();
// Construye la nueva
switch (Section::name) {
case Section::Name::PRELOAD:
preload_ = std::make_unique<Preload>();
break;
case Section::Name::LOGO:
logo_ = std::make_unique<Logo>();
break;
case Section::Name::INTRO:
intro_ = std::make_unique<Intro>();
break;
case Section::Name::TITLE:
title_ = std::make_unique<Title>();
break;
case Section::Name::GAME: {
Player::Id player_id = Player::Id::PLAYER1;
switch (Section::options) {
case Section::Options::GAME_PLAY_1P:
player_id = Player::Id::PLAYER1;
break;
case Section::Options::GAME_PLAY_2P:
player_id = Player::Id::PLAYER2;
break;
case Section::Options::GAME_PLAY_BOTH:
player_id = Player::Id::BOTH_PLAYERS;
break;
default:
break;
}
#ifdef _DEBUG
const int CURRENT_STAGE = debug_config.initial_stage;
#else
constexpr int CURRENT_STAGE = 0;
#endif
game_ = std::make_unique<Game>(player_id, CURRENT_STAGE, Game::DEMO_OFF);
break;
}
case Section::Name::GAME_DEMO: {
const auto PLAYER_ID = static_cast<Player::Id>((rand() % 2) + 1);
constexpr auto CURRENT_STAGE = 0;
game_ = std::make_unique<Game>(PLAYER_ID, CURRENT_STAGE, Game::DEMO_ON);
break;
}
case Section::Name::INSTRUCTIONS:
instructions_ = std::make_unique<Instructions>();
break;
case Section::Name::CREDITS:
credits_ = std::make_unique<Credits>();
break;
case Section::Name::HI_SCORE_TABLE:
hi_score_table_ = std::make_unique<HiScoreTable>();
break;
case Section::Name::RESET:
case Section::Name::QUIT:
default:
break;
}
last_built_section_name_ = Section::name;
}
// Reinicia objetos y vuelve a la sección inicial
void Director::reset() {
Options::saveToFile();
Options::loadFromFile();
Lang::setLanguage(Options::settings.language);
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
Resource::get()->reload();
ServiceMenu::get()->reset();
Section::name = Section::Name::LOGO;
}
// Avanza un frame de la sección activa (llamado desde SDL_AppIterate)
auto Director::iterate() -> SDL_AppResult {
if (Section::name == Section::Name::QUIT) {
return SDL_APP_SUCCESS;
}
// Fase de boot: carga incremental frame a frame con presupuesto de 50ms.
// Durante esta fase la escena activa es Preload (una barra de progreso).
if (boot_loading_) {
try {
if (Resource::get()->loadStep(50 /*ms*/)) {
finishBoot();
boot_loading_ = false;
// Los SDL_EVENT_GAMEPAD_ADDED iniciales ya los ha drenado la rama
// durante la carga: marcamos startup completo ahora para que los
// ADDED/REMOVED posteriores sí generen notificación.
GlobalEvents::markStartupComplete();
}
} catch (const std::exception& e) {
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
Section::name = Section::Name::QUIT;
return SDL_APP_FAILURE;
}
}
// Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva)
handleSectionTransition();
// Ejecuta un frame de la sección activa
if (preload_) {
Preload::iterate();
} else if (logo_) {
logo_->iterate();
} else if (intro_) {
intro_->iterate();
} else if (title_) {
title_->iterate();
} else if (game_) {
game_->iterate();
} else if (instructions_) {
instructions_->iterate();
} else if (hi_score_table_) {
hi_score_table_->iterate();
} else if (credits_) {
credits_->iterate();
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Procesa un evento SDL (llamado desde SDL_AppEvent)
auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
// Eventos globales (SDL_EVENT_QUIT, resize, render target reset, hotplug, service menu, ratón)
GlobalEvents::handle(event);
// Reenvía a la sección activa
if (preload_) {
Preload::handleEvent(event);
} else if (logo_) {
logo_->handleEvent(event);
} else if (intro_) {
intro_->handleEvent(event);
} else if (title_) {
title_->handleEvent(event);
} else if (game_) {
game_->handleEvent(event);
} else if (instructions_) {
instructions_->handleEvent(event);
} else if (hi_score_table_) {
hi_score_table_->handleEvent(event);
} else if (credits_) {
credits_->handleEvent(event);
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Apaga el sistema de forma segura
void Director::shutdownSystem(bool should_shutdown) {
if (should_shutdown) {
auto result = SystemShutdown::shutdownSystem(5, true); // 5 segundos, forzar apps
if (result != SystemShutdown::ShutdownResult::SUCCESS) {
std::cerr << SystemShutdown::resultToString(result) << '\n';
}
}
}