#include "core/system/director.hpp" #include #include // Para mkdir, stat, S_IRWXU #include // Para getuid #include // Para errno, EEXIST, EACCES, ENAMETOO... #include // Para printf, perror #include // Para exit, EXIT_FAILURE, srand #include // Para basic_ostream, operator<<, cout #include // Para make_unique, unique_ptr #include // Para operator+, allocator, char_traits #include "core/audio/audio.hpp" // Para Audio #include "core/input/input.hpp" // Para Input, InputAction #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen #include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_helper.hpp" // Para ResourceHelper #include "core/resources/resource_list.hpp" // Para Asset, AssetType #include "core/resources/resource_loader.hpp" // Para ResourceLoader #include "game/gameplay/cheevos.hpp" // Para Cheevos #include "game/options.hpp" // Para Options, options, OptionsVideo #include "game/scene_manager.hpp" // Para SceneManager #include "game/scenes/boot_loader.hpp" // Para BootLoader #include "game/scenes/credits.hpp" // Para Credits #include "game/scenes/ending.hpp" // Para Ending #include "game/scenes/ending2.hpp" // Para Ending2 #include "game/scenes/game.hpp" // Para Game, GameMode #include "game/scenes/game_over.hpp" // Para GameOver #include "game/scenes/loading_screen.hpp" // Para LoadingScreen #include "game/scenes/logo.hpp" // Para Logo #include "game/scenes/title.hpp" // Para Title #include "game/ui/console.hpp" // Para Console #include "game/ui/notifier.hpp" // Para Notifier #include "utils/defines.hpp" // Para WINDOW_CAPTION #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug #include "game/editor/map_editor.hpp" // Para MapEditor #endif #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) #include #endif namespace { auto getExecutablePath() -> std::string { #ifdef __EMSCRIPTEN__ // En Emscripten els assets estan al root del filesystem virtual (/data, /config) return ""; #else std::string base = SDL_GetBasePath(); if (!base.empty() && base.back() == '/') { base.pop_back(); } return base; #endif } } // namespace // Constructor Director::Director() : executable_path_(getExecutablePath()) { std::cout << "Game start" << '\n'; // Crea la carpeta del sistema donde guardar datos createSystemFolder("jailgames"); createSystemFolder("jailgames/jaildoctors_dilemma"); // Crea el subdirectorio shaders/ dentro de system_folder_ sin modificar system_folder_ { std::string shaders_dir = system_folder_ + "/shaders"; struct stat st = {.st_dev = 0}; if (stat(shaders_dir.c_str(), &st) == -1) { errno = 0; #ifdef _WIN32 mkdir(shaders_dir.c_str()); #else mkdir(shaders_dir.c_str(), S_IRWXU); #endif } } // Determinar el prefijo de ruta según la plataforma #ifdef MACOS_BUNDLE const std::string PREFIX = "/../Resources"; #else const std::string PREFIX; #endif // Preparar ruta al pack (en macOS bundle está en Contents/Resources/) std::string pack_path = executable_path_ + PREFIX + "/resources.pack"; #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) // ============================================================ // RELEASE BUILD: Pack-first architecture // ============================================================ std::cout << "\n** RELEASE MODE: Pack-first initialization\n"; // 1. Initialize resource pack system (required, no fallback) std::cout << "Initializing resource pack: " << pack_path << '\n'; if (!Resource::Helper::initializeResourceSystem(pack_path, false)) { std::cerr << "ERROR: Failed to load resources.pack (required in release builds)\n"; exit(EXIT_FAILURE); } // 2. Validate pack integrity std::cout << "Validating pack integrity..." << '\n'; if (!Resource::Loader::get().validatePack()) { std::cerr << "ERROR: Pack validation failed\n"; exit(EXIT_FAILURE); } // 3. Load assets.yaml from pack std::cout << "Loading assets configuration from pack..." << '\n'; std::string assets_config = Resource::Loader::get().loadAssetsConfig(); if (assets_config.empty()) { std::cerr << "ERROR: Failed to load assets.yaml from pack\n"; exit(EXIT_FAILURE); } // 4. Initialize Asset system with config from pack // NOTE: In release, don't use executable_path or PREFIX - paths in pack are relative // Pass empty string to avoid issues when running from different directories Resource::List::init(""); // Empty executable_path in release Resource::List::get()->loadFromString(assets_config, "", system_folder_); // Empty PREFIX for pack std::cout << "Asset system initialized from pack\n"; #else // ============================================================ // DEVELOPMENT BUILD: Filesystem-first architecture // ============================================================ std::cout << "\n** DEVELOPMENT MODE: Filesystem-first initialization\n"; // 1. Initialize Asset system from filesystem Resource::List::init(executable_path_); // 2. Load asset configuration from disk // Note: Asset verification happens during Resource::Cache::load() setFileList(); // 3. Initialize resource pack system (optional, with fallback) std::cout << "Initializing resource pack (development mode): " << pack_path << '\n'; Resource::Helper::initializeResourceSystem(pack_path, true); #endif // Configura la ruta y carga las opciones desde un fichero Options::setConfigFile(Resource::List::get()->get("config.yaml")); Options::loadFromFile(); #ifdef __EMSCRIPTEN__ // A la versió web el navegador gestiona la finestra: forcem zoom x3 // perquè la textura 256x192 no es vegi minúscula al canvas HTML, // i desactivem el borde per aprofitar al màxim l'espai del canvas. Options::video.fullscreen = false; Options::video.integer_scale = true; Options::window.zoom = 4; Options::video.border.enabled = true; Options::video.border.height = 8; Options::video.border.width = 8; #endif // Configura la ruta y carga los presets de PostFX Options::setPostFXFile(Resource::List::get()->get("postfx.yaml")); Options::loadPostFXFromFile(); // Configura la ruta y carga los presets del shader CrtPi Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml")); Options::loadCrtPiFromFile(); // En mode quiosc, forçar pantalla completa independentment de la configuració if (Options::kiosk.enabled) { Options::video.fullscreen = true; } // Inicializa JailAudio Audio::init(); // Crea los objetos Screen::init(); // Inicializa el singleton del cache sin disparar la carga. La carga real // la hace Director::iterate() llamando a Cache::loadStep() en cada frame, // de forma que la ventana, los eventos y la barra de progreso están vivos // desde el primer tick. Resource::Cache::init(); Resource::Cache::get()->beginLoad(); // Special handling for gamecontrollerdb.txt - SDL needs filesystem path #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) // In release, construct the path manually (not from Asset which has empty executable_path) std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt"; Input::init(gamecontroller_db); #else // In development, use Asset as normal Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // Carga configuración de controles #endif // Aplica las teclas y botones del gamepad configurados desde Options Input::get()->applyKeyboardBindingsFromOptions(); Input::get()->applyGamepadBindingsFromOptions(); std::cout << "\n"; // Fin de inicialización mínima de sistemas // Construeix l'escena inicial (BootLoader). finishBoot() la canviarà a // LOGO (o la que digui Debug) quan Cache::loadStep() complete la càrrega. SceneManager::current = SceneManager::Scene::BOOT_LOADER; switchToActiveScene(); } // Inicialitzacions que depenen del cache poblat. Es crida des d'iterate() // just quan Cache::loadStep() retorna true, amb la finestra i el bucle ja vius. void Director::finishBoot() { Notifier::init("", "8bithud"); RenderInfo::init(); Console::init("8bithud"); Screen::get()->setNotificationsEnabled(true); #ifdef _DEBUG Debug::init(); #ifdef __EMSCRIPTEN__ // A wasm el debug.yaml viu a SYSTEM_FOLDER (MEMFS no persistent) i no està // disponible. Saltem el loadFromFile i entrem directament a la GAME. SceneManager::current = SceneManager::Scene::GAME; #else Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml")); Debug::get()->loadFromFile(); SceneManager::current = Debug::get()->getInitialScene(); #endif MapEditor::init(); #else // En release, pasamos a LOGO siempre tras la carga SceneManager::current = SceneManager::Scene::LOGO; #endif // Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos) #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) { // En release el locale está en el pack, no en el filesystem std::string locale_key = Resource::List::get()->get(Options::language + ".yaml"); auto locale_bytes = Resource::Helper::loadFile(locale_key); std::string locale_content(locale_bytes.begin(), locale_bytes.end()); Locale::initFromContent(locale_content); } #else Locale::init(Resource::List::get()->get(Options::language + ".yaml")); #endif // Special handling for cheevos.bin - also needs filesystem path #if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__) std::string cheevos_path = system_folder_ + "/cheevos.bin"; Cheevos::init(cheevos_path); #else Cheevos::init(Resource::List::get()->get("cheevos.bin")); #endif } Director::~Director() { // Guarda las opciones a un fichero Options::saveToFile(); // Destruir l'escena activa ABANS dels singletons. Si no, el unique_ptr membre // destrueix l'escena al final del destructor — quan Audio, Screen, Resource... // ja són morts — i qualsevol accés en els seus destructors és un UAF. active_scene_.reset(); // Destruye los singletones Cheevos::destroy(); Locale::destroy(); #ifdef _DEBUG MapEditor::destroy(); Debug::destroy(); #endif Input::destroy(); Console::destroy(); RenderInfo::destroy(); Notifier::destroy(); Resource::Cache::destroy(); Resource::Helper::shutdownResourceSystem(); // Shutdown resource pack system Audio::destroy(); Screen::destroy(); Resource::List::destroy(); SDL_Quit(); std::cout << "\nBye!" << '\n'; } // Crea la carpeta del sistema donde guardar datos void Director::createSystemFolder(const std::string& folder) { #ifdef __EMSCRIPTEN__ // En Emscripten utilitzem MEMFS (no persistent entre sessions). // No cal crear directoris: MEMFS els crea automàticament en escriure-hi. system_folder_ = "/config/" + folder; return; #else #ifdef _WIN32 system_folder_ = std::string(getenv("APPDATA")) + "/" + folder; #elif __APPLE__ struct passwd* pw = getpwuid(getuid()); const char* homedir = pw->pw_dir; system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder; #elif __linux__ struct passwd* pw = getpwuid(getuid()); const char* homedir = pw->pw_dir; system_folder_ = std::string(homedir) + "/.config/" + folder; { // Intenta crear ".config", per si no existeix std::string config_base_folder = std::string(homedir) + "/.config"; int ret = mkdir(config_base_folder.c_str(), S_IRWXU); if (ret == -1 && errno != EEXIST) { printf("ERROR CREATING CONFIG BASE FOLDER."); exit(EXIT_FAILURE); } } #endif struct stat st = {.st_dev = 0}; if (stat(system_folder_.c_str(), &st) == -1) { errno = 0; #ifdef _WIN32 int ret = mkdir(system_folder_.c_str()); #else int ret = mkdir(system_folder_.c_str(), S_IRWXU); #endif if (ret == -1) { switch (errno) { case EACCES: printf("the parent directory does not allow write"); exit(EXIT_FAILURE); case EEXIST: printf("pathname already exists"); exit(EXIT_FAILURE); case ENAMETOOLONG: printf("pathname is too long"); exit(EXIT_FAILURE); default: perror("mkdir"); exit(EXIT_FAILURE); } } } #endif // __EMSCRIPTEN__ } // Carga la configuración de assets desde assets.yaml void Director::setFileList() { // Determinar el prefijo de ruta según la plataforma #ifdef MACOS_BUNDLE const std::string PREFIX = "/../Resources"; #else const std::string PREFIX; #endif // Construir ruta al archivo de configuración de assets std::string config_path = executable_path_ + PREFIX + "/config/assets.yaml"; // Cargar todos los assets desde el archivo de configuración // La verificación de existencia de archivos se realiza durante Resource::Cache::load() Resource::List::get()->loadFromFile(config_path, PREFIX, system_folder_); } // Construeix l'escena segons SceneManager::current i la deixa en active_scene_. // Substitueix els vells runLogo(), runTitle(), runGame(), etc. void Director::switchToActiveScene() { // Si la escena anterior va demanar RESTART_CURRENT, restaurem la que estava activa if (SceneManager::current == SceneManager::Scene::RESTART_CURRENT) { SceneManager::current = SceneManager::scene_before_restart; } // Destrueix l'escena anterior (pot parar música, etc. al seu destructor) active_scene_.reset(); switch (SceneManager::current) { case SceneManager::Scene::BOOT_LOADER: active_scene_ = std::make_unique(); break; case SceneManager::Scene::LOGO: active_scene_ = std::make_unique(); break; case SceneManager::Scene::LOADING_SCREEN: active_scene_ = std::make_unique(); break; case SceneManager::Scene::TITLE: active_scene_ = std::make_unique(); break; case SceneManager::Scene::CREDITS: active_scene_ = std::make_unique<Credits>(); break; case SceneManager::Scene::DEMO: active_scene_ = std::make_unique<Game>(Game::Mode::DEMO); break; case SceneManager::Scene::GAME: Audio::get()->stopMusic(); active_scene_ = std::make_unique<Game>(Game::Mode::GAME); break; case SceneManager::Scene::GAME_OVER: active_scene_ = std::make_unique<GameOver>(); break; case SceneManager::Scene::ENDING: active_scene_ = std::make_unique<Ending>(); break; case SceneManager::Scene::ENDING2: active_scene_ = std::make_unique<Ending2>(); break; default: break; } current_scene_ = SceneManager::current; } // SDL_AppIterate: executa un frame de l'escena activa auto Director::iterate() -> SDL_AppResult { if (SceneManager::current == SceneManager::Scene::QUIT) { return SDL_APP_SUCCESS; } // Fase de boot: anem cridant loadStep() fins que el cache estiga ple. // Durant aquesta fase l'escena activa és BootLoader (una barra de progrés). if (boot_loading_) { try { // Budget de 50ms: durant el boot el joc va a ~15-20 FPS, suficient // per veure la barra avançar suau i processar events del WM/ESC, // i evita el 50% d'ineficiència que provocaria un budget < vsync. if (Resource::Cache::get()->loadStep(50 /*ms*/)) { if (Options::loading.show && Options::loading.wait_for_input) { boot_waiting_for_input_ = true; // Esperar tecla antes de continuar } else { finishBoot(); } boot_loading_ = false; // finishBoot() ja ha fixat SceneManager::current a LOGO (o la que // digui Debug). El canvi d'escena es fa just a sota. } } catch (const std::exception& e) { std::cerr << "Fatal error during resource load: " << e.what() << '\n'; SceneManager::current = SceneManager::Scene::QUIT; return SDL_APP_FAILURE; } } // Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) { switchToActiveScene(); } if (active_scene_) { active_scene_->iterate(); } return SDL_APP_CONTINUE; } // SDL_AppEvent: despatxa un event a l'escena activa auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult { #ifndef __EMSCRIPTEN__ // A la versió web no tenim event de quit del navegador if (event.type == SDL_EVENT_QUIT) { SceneManager::current = SceneManager::Scene::QUIT; return SDL_APP_SUCCESS; } #endif // Si estamos esperando input tras la carga: consumir tecla/botón y arrancar if (boot_waiting_for_input_) { const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat; const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN; if (IS_KEY || IS_BUTTON) { boot_waiting_for_input_ = false; finishBoot(); } return SDL_APP_CONTINUE; } if (active_scene_) { active_scene_->handleEvent(event); } return SDL_APP_CONTINUE; }