#include "director.hpp" #include #include #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/audio/audio_adapter.hpp" #include "core/defaults/audio.hpp" #include "core/defaults/window.hpp" #include "core/input/input.hpp" #include "core/input/mouse.hpp" #include "core/locale/locale.hpp" #include "core/rendering/sdl_manager.hpp" #include "core/resources/resource_helper.hpp" #include "core/resources/resource_loader.hpp" #include "core/system/notifier.hpp" #include "core/system/service_menu.hpp" #include "core/utils/path_utils.hpp" #include "debug_overlay.hpp" #include "game/config_yaml.hpp" #include "game/scenes/game_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" #include "global_events.hpp" #include "project.h" #include "scene.hpp" #include "scene_context.hpp" #ifndef _WIN32 #include #include #endif // Using declarations per simplificar el codi using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; // Constructor Director::Director(int argc, char* argv[]) : cfg_(&ConfigYaml::engine_config) { std::cout << "Game start\n"; // Inicialitzar opciones con valors per defecte ConfigYaml::init(); // Convertir arguments a std::vector i comprovar-los std::vector args(argv, argv + argc); executable_path_ = checkProgramArguments(args); // Inicialitzar sistema de rutes Utils::initializePathSystem(args[0].c_str()); // Obtenir ruta base dels recursos std::string resource_base = Utils::getResourceBasePath(); // Inicialitzar sistema de recursos #ifdef RELEASE_BUILD // Mode release: paquet obligatori, sin fallback std::string pack_path = resource_base + "/resources.pack"; if (!Resource::Helper::initializeResourceSystem(pack_path, false)) { std::cerr << "ERROR FATAL: No es pot load " << pack_path << "\n"; std::cerr << "El juego no pot continuar sin los recursos.\n"; std::exit(1); } // Validar integritat del paquet if (!Resource::Loader::get().validatePack()) { std::cerr << "ERROR FATAL: El paquet de recursos está corromput\n"; std::exit(1); } std::cout << "Sistema de recursos inicialitzat (mode release)\n"; #else // Mode desenvolupament: intentar paquet con fallback a data/ std::string pack_path = resource_base + "/resources.pack"; Resource::Helper::initializeResourceSystem(pack_path, true); if (Resource::Helper::isPackLoaded()) { std::cout << "Sistema de recursos inicialitzat (mode dev con paquet)\n"; } else { std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n"; } // Establir ruta base per al fallback Resource::Loader::get().setBasePath(resource_base); #endif // Crear carpetes del sistema createSystemFolder("jailgames"); createSystemFolder(std::string("jailgames/") + Project::NAME); // Establir ruta del file de configuración ConfigYaml::setConfigFile(system_folder_ + "/config.yaml"); // Carregar o crear configuración ConfigYaml::loadFromFile(); // Carregar locale segons la config (per defecte "ca"). Si la càrrega // falla, Locale::text() retorna la clau crua i el joc segueix funcionant. Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml"); // Inicialitzar sistema de input Input::init("data/gamecontrollerdb.txt"); // Aplicar configuración de controls dels jugadors Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer2Bindings(cfg_->player2); if (cfg_->console) { std::cout << "Configuración carregada\n"; std::cout << " Finestra: " << cfg_->window.width << "×" << cfg_->window.height << '\n'; std::cout << " Input: " << Input::get()->getNumGamepads() << " gamepad(s) detectat(s)\n"; } std::cout << '\n'; // === Bootstrap de finestra, audio i subsistemes de runtime === int initial_width = static_cast(std::round( Defaults::Window::WIDTH * cfg_->window.zoom_factor)); int initial_height = static_cast(std::round( Defaults::Window::HEIGHT * cfg_->window.zoom_factor)); sdl_ = std::make_unique(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [] { ConfigYaml::saveToFile(); }); // CRÍTIC: forçar ocultació del cursor DESPRÉS d'inicialitzar SDL, // perquè la creació de la finestra el reactiva. if (!cfg_->window.fullscreen) { Mouse::forceHide(); } const Audio::Config AUDIO_CONFIG{ .enabled = Defaults::Audio::ENABLED, .volume = Defaults::Audio::VOLUME, .music_enabled = Defaults::Audio::MUSIC_ENABLED, .music_volume = Defaults::Audio::MUSIC_VOLUME, .sound_enabled = Defaults::Audio::SOUND_ENABLED, .sound_volume = Defaults::Audio::SOUND_VOLUME, }; Audio::init(AUDIO_CONFIG); Audio::get()->applySettings(AUDIO_CONFIG); AudioResource::getMusic("title.ogg"); AudioResource::getMusic("game.ogg"); if (cfg_->console) { std::cout << "Música precacheada\n"; } context_ = std::make_unique(); #ifdef _DEBUG context_->setNextScene(SceneType::TITLE); #else context_->setNextScene(SceneType::LOGO); #endif debug_overlay_ = std::make_unique( sdl_->getRenderer(), cfg_->rendering); System::Notifier::init(sdl_->getRenderer()); System::ServiceMenu::init(sdl_->getRenderer()); last_ticks_ms_ = SDL_GetTicks(); } Director::~Director() { // Guardar opciones ConfigYaml::saveToFile(); // Destruir subsistemes en ordre invers a la construcció. El Notifier // referencia el renderer, així que ha de morir abans que sdl_. // SDL_Quit() el crida SDL automàticament després de SDL_AppQuit; no // l'hem de cridar nosaltres. current_scene_.reset(); debug_overlay_.reset(); System::ServiceMenu::destroy(); System::Notifier::destroy(); context_.reset(); sdl_.reset(); Input::destroy(); Audio::destroy(); std::cout << "\nBye!\n"; } // Comprovar arguments del programa auto Director::checkProgramArguments(std::vector const& args) -> std::string { for (std::size_t i = 1; i < args.size(); ++i) { const std::string& argument = args[i]; if (argument == "--console") { cfg_->console = true; std::cout << "Mode consola activat\n"; } else if (argument == "--reset-config") { ConfigYaml::init(); ConfigYaml::saveToFile(); std::cout << "Configuración restablida als valors per defecte\n"; } } return args[0]; // Retornar ruta de l'executable } // Crear carpeta del sistema (específic per plataforma) void Director::createSystemFolder(const std::string& folder) { #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; // CRÍTIC: Crear ~/.config 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: No es pot crear la carpeta ~/.config\n"); exit(EXIT_FAILURE); } } #endif // Comprovar si la carpeta existeix. Zero-init de toda la struct stat // para evitar -Wmissing-field-initializers (struct con muchos campos // específicos del SO que no queremos enumerar manualmente). struct stat st{}; 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("ERROR: Permisos denegats creant %s\n", system_folder_.c_str()); exit(EXIT_FAILURE); case EEXIST: // La carpeta ya existeix (race condition), continuar break; case ENAMETOOLONG: printf("ERROR: Ruta massa llarga: %s\n", system_folder_.c_str()); exit(EXIT_FAILURE); default: perror("mkdir"); exit(EXIT_FAILURE); } } } if (cfg_->console) { std::cout << "Carpeta del sistema: " << system_folder_ << '\n'; } } auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context) -> std::unique_ptr { switch (type) { case SceneType::LOGO: return std::make_unique(sdl, context); case SceneType::TITLE: return std::make_unique(sdl, context); case SceneType::GAME: return std::make_unique(sdl, context); case SceneType::EXIT: default: return nullptr; } } auto Director::advanceScene() -> SDL_AppResult { current_scene_.reset(); const SceneType NEXT = context_->nextScene(); if (NEXT == SceneType::EXIT) { SceneManager::actual = SceneType::EXIT; return SDL_APP_SUCCESS; } SceneManager::actual = NEXT; current_scene_ = buildScene(NEXT, *sdl_, *context_); if (!current_scene_) { SceneManager::actual = SceneType::EXIT; return SDL_APP_SUCCESS; } return SDL_APP_CONTINUE; } auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult { // 1. Window events (resize, minimize, focus...) if (sdl_->handleWindowEvent(event)) { return SDL_APP_CONTINUE; } // 2. Events globals (F1-F6, ESC, QUIT, gamepad hotplug). // GlobalEvents marca context_->nextScene() = EXIT en ESC doble o QUIT; // activem la bandera per fer-ho fluir cap a SDL_APP_SUCCESS al pròxim tick. if (GlobalEvents::handle(event, *sdl_, *context_)) { if (context_->nextScene() == SceneType::EXIT) { wants_quit_ = true; } return SDL_APP_CONTINUE; } // 3. F11 → toggle del debug overlay (cas especial fora de GlobalEvents). if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) { debug_overlay_->toggle(); return SDL_APP_CONTINUE; } // 4. Esdeveniment específic de l'escena actual. if (current_scene_) { current_scene_->handleEvent(event); } return SDL_APP_CONTINUE; } auto Director::iterate() -> SDL_AppResult { if (wants_quit_) { return SDL_APP_SUCCESS; } // Pivotar a la següent escena si l'actual ha acabat (o és la primera). if (!current_scene_ || current_scene_->isFinished()) { SDL_AppResult pivot = advanceScene(); if (pivot != SDL_APP_CONTINUE) { return pivot; } } // Delta time real, capeado a 50ms per evitar grans salts. const Uint64 NOW = SDL_GetTicks(); float delta_time = static_cast(NOW - last_ticks_ms_) / 1000.0F; last_ticks_ms_ = NOW; delta_time = std::min(delta_time, 0.05F); Mouse::updateCursorVisibility(); Input::get()->update(); current_scene_->update(delta_time); debug_overlay_->update(delta_time); if (auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->update(delta_time); } if (auto* menu = System::ServiceMenu::get(); menu != nullptr) { menu->update(delta_time); } Audio::update(); // Si la swapchain no està disponible (finestra minimitzada, etc.), // saltar-se draw+present aquest frame. if (!sdl_->clear(0, 0, 0)) { return SDL_APP_CONTINUE; } sdl_->updateRenderingContext(); current_scene_->draw(); debug_overlay_->draw(); // sempre per damunt de l'escena if (const auto* notifier = System::Notifier::get(); notifier != nullptr) { notifier->draw(); // toast: per damunt de tot } if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) { menu->draw(); // service menu: per damunt fins i tot dels toasts } sdl_->present(); return SDL_APP_CONTINUE; }