#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/rendering/sdl_manager.hpp" #include "core/resources/resource_helper.hpp" #include "core/resources/resource_loader.hpp" #include "core/system/notifier.hpp" #include "core/utils/path_utils.hpp" #include "debug_overlay.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(std::vector const& args, Config::EngineConfig& cfg, Config::ConfigPersistence persistence) : cfg_(&cfg), persistence_(std::move(persistence)) { std::cout << "Orni Attack - Inici\n"; // Inicialitzar opciones con valors per defecte persistence_.init_defaults(); // Comprovar arguments del programa 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 persistence_.set_path(system_folder_ + "/config.yaml"); // Carregar o crear configuración persistence_.load(); // 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'; } Director::~Director() { // Guardar opciones persistence_.save(); // Destruir subsistemes en ordre invers a la construcció. Crític: el // renderer i la finestra (dins de sdl_) han de morir abans de cridar // SDL_Quit(). Si fossin destruïts pel destructor implícit del Director // morirïen DESPRÉS dels Input/Audio/SDL_Quit que vénen a sota. current_scene_.reset(); debug_overlay_.reset(); context_.reset(); sdl_.reset(); // Cleanup input Input::destroy(); // Cleanup audio Audio::destroy(); // Cleanup SDL SDL_Quit(); std::cout << "\nAdéu!\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") { persistence_.init_defaults(); persistence_.save(); 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'; } } // Bucle principal del juego auto Director::run() -> int { // Calculate initial size from saved zoom_factor 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)); // Crear gestor SDL amb la engine_config + callback de persistència // per a quan toggleVSync (F4) muti vsync. Mantenim sdl_manager agnòstic. sdl_ = std::make_unique(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [this] { persistence_.save(); }); // CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL // Això evita que SDL mostre el cursor automàticament durante la creació de la finestra if (!cfg_->window.fullscreen) { Mouse::forceHide(); } // Inicializar sistema de audio (config inyectada desde Defaults) 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); // Aplicar volúmenes iniciales al motor // Precachear música para evitar lag al empezar AudioResource::getMusic("title.ogg"); AudioResource::getMusic("game.ogg"); if (cfg_->console) { std::cout << "Música precacheada\n"; } // Crear context de escenes context_ = std::make_unique(); #ifdef _DEBUG context_->setNextScene(SceneType::TITLE); #else context_->setNextScene(SceneType::LOGO); #endif // Overlay de debug (FPS + VSync). Vive en el Director porque es global // a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG). debug_overlay_ = std::make_unique( sdl_->getRenderer(), cfg_->rendering); // Sistema de notificacions toast: singleton accessible des d'on calgui // (F1-F5 a sdl_manager, ESC a global_events). El renderer ha de viure // tant com el Notifier; el destruim explícitament abans de tornar. System::Notifier::init(sdl_->getRenderer()); // Comptador de delta time per al primer iterate(). last_ticks_ms_ = SDL_GetTicks(); // Bucle principal: poll d'events + iterate() per frame. La primera escena // es construeix lazy dins d'iterate() via advanceScene(). En migrar a // SDL_MAIN_USE_CALLBACKS, aquest while desapareixerà i SDL_AppEvent/ // SDL_AppIterate cridaran handleEvent()/iterate() directament. while (true) { SDL_Event event; while (SDL_PollEvent(&event)) { SDL_AppResult r = handleEvent(event); if (r != SDL_APP_CONTINUE) { System::Notifier::destroy(); return (r == SDL_APP_SUCCESS) ? 0 : 1; } } SDL_AppResult r = iterate(); if (r != SDL_APP_CONTINUE) { System::Notifier::destroy(); return (r == SDL_APP_SUCCESS) ? 0 : 1; } } } 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); } 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 } sdl_->present(); return SDL_APP_CONTINUE; }