499 lines
19 KiB
C++
499 lines
19 KiB
C++
#include "core/system/director.hpp"
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <sys/stat.h> // Para mkdir, stat, S_IRWXU
|
|
#include <unistd.h> // Para getuid
|
|
|
|
#include <cerrno> // Para errno, EEXIST, EACCES, ENAMETOO...
|
|
#include <cstdio> // Para printf, perror
|
|
#include <cstdlib> // Para exit, EXIT_FAILURE, srand
|
|
#include <iostream> // Para basic_ostream, operator<<, cout
|
|
#include <memory> // Para make_unique, unique_ptr
|
|
#include <string> // 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 <pwd.h>
|
|
#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")); // NOLINT(readability-static-accessed-through-instance)
|
|
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")); // NOLINT(readability-static-accessed-through-instance)
|
|
Options::loadPostFXFromFile();
|
|
|
|
// Configura la ruta y carga los presets del shader CrtPi
|
|
Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
|
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")); // NOLINT(readability-static-accessed-through-instance) 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"); // NOLINT(readability-static-accessed-through-instance)
|
|
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")); // NOLINT(readability-static-accessed-through-instance)
|
|
#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) { // NOLINT(readability-convert-member-functions-to-static)
|
|
#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() { // NOLINT(readability-convert-member-functions-to-static)
|
|
// 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<BootLoader>();
|
|
break;
|
|
|
|
case SceneManager::Scene::LOGO:
|
|
active_scene_ = std::make_unique<Logo>();
|
|
break;
|
|
|
|
case SceneManager::Scene::LOADING_SCREEN:
|
|
active_scene_ = std::make_unique<LoadingScreen>();
|
|
break;
|
|
|
|
case SceneManager::Scene::TITLE:
|
|
active_scene_ = std::make_unique<Title>();
|
|
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;
|
|
} |