// IWYU pragma: no_include #include "core/system/director.hpp" #include // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit #include // Para errno #include // Para srand, exit, rand, EXIT_FAILURE #include // Para std::strerror #include // Para time #include // Para ifstream, ofstream #include // Para basic_ostream, operator<<, cerr #include // Para make_unique, unique_ptr #include // Para runtime_error #include // Para allocator, basic_string, char_traits, operator+, string, operator== #ifdef _WIN32 #include // Per _execv #else #include // Per execv #endif #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 void loadYamlField(const fkyaml::node& yaml, const std::string& key, T& dest) { if (!yaml.contains(key)) { return; } try { dest = yaml[key].get_value(); } 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(int /*argc*/, char** argv) : argv_(argv) { 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(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 // gamecontrollerdb.txt no pot anar al pack (SDL_AddGamepadMappingsFromFile només llegeix del FS). // Sempre viu al costat del binari, fora del índex d'assets. Input::init(executable_path_ + "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; } } // Allibera tots els singletons i SDL (compartit entre close() i relaunch()) void Director::shutdownSubsystems() { // 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 // Libera todos los recursos de SDL SDL_Quit(); } // Cierra todo y libera recursos del sistema y de los singletons void Director::close() { shutdownSubsystems(); std::cout << "\nBye!\n"; // Apaga el sistema shutdownSystem(Section::options == Section::Options::SHUTDOWN); } // Reemplaça el procés actual per ell mateix (reinici en calent). En cas d'èxit no torna. // Si no es pot reiniciar (Emscripten, argv invàlid), retorna i el caller fa el reset clàssic. void Director::relaunch() const { #ifdef __EMSCRIPTEN__ // Al navegador el reinici real seria location.reload(); aquí caiem al reset intern. return; #else if (argv_ == nullptr || argv_[0] == nullptr) { return; } std::cout << "Relaunching " << argv_[0] << "...\n"; shutdownSubsystems(); #ifdef _WIN32 _execv(argv_[0], argv_); #else execv(argv_[0], argv_); #endif // Si arribem aquí, execv ha fallat. Tots els subsistemes ja estan destruïts: no // podem reprendre el bucle. Sortim amb error. std::cerr << "Relaunch failed: " << std::strerror(errno) << "\n"; std::exit(EXIT_FAILURE); #endif } // Carga los parametros void Director::loadParams() { // Carga los parametros para configurar el juego #ifdef ANBERNIC const std::string PARAM_FILE_PATH = Asset::get()->getPath("classic.txt"); #else const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_preset + ".txt"); #endif loadParamsFromFile(PARAM_FILE_PATH); } // Carga el fichero de puntuaciones void Director::loadScoreFile() { auto manager = std::make_unique(Options::settings.hi_score_table); #ifdef _DEBUG manager->clear(); #else manager->loadFromFile(Asset::get()->getPath("score.bin")); #endif } // Carga el indice de ficheros desde el pack (o filesystem como fallback) void Director::loadAssets() { #ifdef MACOS_BUNDLE const std::string PREFIX = "/../Resources"; #else const std::string PREFIX; #endif // El índice ahora vive dins el pack a "config/assets.txt" (ResourceHelper li trau el "data/" prefix). // ResourceHelper::loadFile fa fallback automàtic al filesystem si el pack no està o no conté el fitxer. auto buffer = ResourceHelper::loadFile("/data/config/assets.txt"); if (buffer.empty()) { throw std::runtime_error("No s'ha pogut carregar l'índex d'assets (data/config/assets.txt)"); } Asset::get()->loadFromBuffer(buffer, 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(file)), std::istreambuf_iterator()); 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: intenta reinici real via execv; si no es pot (Emscripten o argv invàlid), // cau al reset intern (recarrega recursos i torna a LOGO, sense recrear Screen/Params). if (Section::name == Section::Name::RESET) { relaunch(); // En èxit no torna; el binari es reemplaça resetActiveSection(); // Fallback: 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(); break; case Section::Name::LOGO: logo_ = std::make_unique(); break; case Section::Name::INTRO: intro_ = std::make_unique(); break; case Section::Name::TITLE: title_ = std::make_unique(); 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'; } } }