#include "core/system/director.h" #include #include // for errno, EEXIST, EACCES, ENAMETOO... #include // for printf, perror #include // for strcmp #ifndef __EMSCRIPTEN__ #include // for mkdir, stat, S_IRWXU #include // for getuid #endif #include // for exit, EXIT_FAILURE, srand #include #include // for basic_ostream, operator<<, basi... #include // for cout #include #include // for basic_string, operator+, char_t... #include // for vector #include "core/audio/jail_audio.hpp" // for JA_Init #include "core/input/input.h" // for Input, inputs_e, INPUT_USE_GAME... #include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda... #include "core/locale/lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK #include "core/rendering/screen.h" // for Screen #include "core/rendering/texture.h" // for Texture #include "core/resources/asset.h" // for Asset, assetType #include "core/resources/resource.h" #include "core/resources/resource_helper.h" #include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H... #include "game/game.h" // for Game #include "game/options.hpp" // for Options::init, loadFromFile... #include "game/scenes/intro.h" // for Intro #include "game/scenes/logo.h" // for Logo #include "game/scenes/title.h" // for Title #include "utils/utils.h" // for input_t, boolToString #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) #include #endif // Constructor Director::Director(int argc, const char *argv[]) { std::cout << "Game start" << std::endl; // Inicializa variables section = new section_t(); section->name = SECTION_PROG_LOGO; // Inicializa las opciones del programa (defaults + dispositivos d'entrada) Options::init(); // Comprueba los parametros del programa (pot activar console) checkProgramArguments(argc, argv); // Crea la carpeta del sistema donde guardar datos createSystemFolder("jailgames"); #ifndef DEBUG createSystemFolder("jailgames/coffee_crisis"); #else createSystemFolder("jailgames/coffee_crisis_debug"); #endif // Estableix el fitxer de configuració i carrega les opcions (o crea el // YAML amb defaults si no existeix). Options::setConfigFile(systemFolder + "/config.yaml"); Options::loadFromFile(); // Inicializa el sistema de recursos (pack + fallback). // En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file // de emscripten ya empaqueta data/ — no hay resources.pack. { #if defined(__EMSCRIPTEN__) const bool enable_fallback = true; #elif defined(RELEASE_BUILD) const bool enable_fallback = false; #else const bool enable_fallback = true; #endif if (!ResourceHelper::initializeResourceSystem("resources.pack", enable_fallback)) { std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << std::endl; exit(EXIT_FAILURE); } } // Crea el objeto que controla los ficheros de recursos asset = new Asset(executablePath); asset->setVerbose(Options::settings.console); // Si falta algún fichero no inicia el programa if (!setFileList()) { exit(EXIT_FAILURE); } // Inicializa SDL initSDL(); // Inicializa JailAudio initJailAudio(); // Establece el modo de escalado de texturas Texture::setGlobalScaleMode(Options::video.scale_mode); // Crea los objetos lang = new Lang(asset); lang->setLang(Options::settings.language); #ifdef __EMSCRIPTEN__ input = new Input("/gamecontrollerdb.txt"); #else { const std::string binDir = std::filesystem::path(executablePath).parent_path().string(); #ifdef MACOS_BUNDLE input = new Input(binDir + "/../Resources/gamecontrollerdb.txt"); #else input = new Input(binDir + "/gamecontrollerdb.txt"); #endif } #endif initInput(); // Orden importante: Screen + initShaders ANTES de Resource::init. // Si `Resource::init` se ejecuta primero, carga ~100 texturas vía // `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un // estado que hace crashear al driver Vulkan cuando después `initShaders` // intenta reclamar la ventana para el dispositivo SDL3 GPU. // // Por eso el constructor de Screen NO carga notificationText desde // Resource; se enlaza después vía `screen->initNotifications()`. screen = new Screen(window, renderer, asset); #ifndef NO_SHADERS if (Options::video.gpu.acceleration) { screen->initShaders(); } #endif // Ahora sí, precarga todos los recursos en memoria (texturas, sonidos, // música, ...). Vivirán durante toda la vida de la app. Resource::init(renderer, asset, input); // Completa el enlazado de Screen con recursos que necesitan Resource // inicializado (actualmente sólo el Text de las notificaciones). screen->initNotifications(); activeSection = ActiveSection::None; } Director::~Director() { Options::saveToFile(); // Libera las secciones primero: sus destructores tocan audio/render SDL // (p.ej. Intro::~Intro llama a JA_DeleteMusic) y deben ejecutarse antes // de SDL_Quit(). logo.reset(); intro.reset(); title.reset(); game.reset(); // Screen puede tener referencias a Text propiedad de Resource: destruir // Screen antes que Resource. delete screen; // Libera todos los recursos precargados antes de cerrar SDL. Resource::destroy(); delete asset; delete input; delete lang; delete section; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); ResourceHelper::shutdownResourceSystem(); std::cout << "\nBye!" << std::endl; } // Inicializa el objeto input void Director::initInput() { // Establece si ha de mostrar mensajes input->setVerbose(Options::settings.console); // Busca si hay un mando conectado input->discoverGameController(); // Teclado - Movimiento del jugador input->bindKey(input_up, SDL_SCANCODE_UP); input->bindKey(input_down, SDL_SCANCODE_DOWN); input->bindKey(input_left, SDL_SCANCODE_LEFT); input->bindKey(input_right, SDL_SCANCODE_RIGHT); input->bindKey(input_fire_left, SDL_SCANCODE_Q); input->bindKey(input_fire_center, SDL_SCANCODE_W); input->bindKey(input_fire_right, SDL_SCANCODE_E); // Teclado - Otros input->bindKey(input_accept, SDL_SCANCODE_RETURN); input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE); input->bindKey(input_pause, SDL_SCANCODE_ESCAPE); input->bindKey(input_exit, SDL_SCANCODE_ESCAPE); input->bindKey(input_window_dec_size, SDL_SCANCODE_F1); input->bindKey(input_window_inc_size, SDL_SCANCODE_F2); input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3); input->bindKey(input_toggle_gpu, SDL_SCANCODE_F9); input->bindKey(input_toggle_shader, SDL_SCANCODE_F10); input->bindKey(input_toggle_shader_type, SDL_SCANCODE_F11); // Mando - Movimiento del jugador input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP); input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN); input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT); input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST); input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH); input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST); // Mando - Otros // SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK. input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST); #ifdef GAME_CONSOLE input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK); input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START); #else input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START); input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK); #endif } // Inicializa JailAudio void Director::initJailAudio() { JA_Init(48000, SDL_AUDIO_S16, 2); } // Arranca SDL y crea la ventana bool Director::initSDL() { // Indicador de éxito bool success = true; // Inicializa SDL if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) { if (Options::settings.console) { std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl; } success = false; } else { // Inicia el generador de numeros aleatorios std::srand(static_cast(SDL_GetTicks())); // Crea la ventana window = SDL_CreateWindow( Options::window.caption.c_str(), GAMECANVAS_WIDTH * Options::window.zoom, GAMECANVAS_HEIGHT * Options::window.zoom, 0); if (window == nullptr) { if (Options::settings.console) { std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl; } success = false; } else { SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); // Crea un renderizador para la ventana renderer = SDL_CreateRenderer(window, NULL); if (renderer == nullptr) { if (Options::settings.console) { std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl; } success = false; } else { // Modo de blending por defecto (consistente con CCAE): // permite alpha blending para fades y notificaciones. SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Activa vsync si es necesario if (Options::video.vsync) { SDL_SetRenderVSync(renderer, 1); } // Inicializa el color de renderizado SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF); // Establece el tamaño del buffer de renderizado SDL_SetRenderLogicalPresentation(renderer, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX); // Establece el modo de mezcla SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); } } } if (Options::settings.console) { std::cout << std::endl; } return success; } // Crea el indice de ficheros bool Director::setFileList() { #ifdef MACOS_BUNDLE const std::string prefix = "/../Resources"; #else const std::string prefix = ""; #endif // Ficheros de configuración asset->add(systemFolder + "/score.bin", t_data, false, true); asset->add(prefix + "/data/demo/demo.bin", t_data); // Musicas asset->add(prefix + "/data/music/intro.ogg", t_music); asset->add(prefix + "/data/music/playing.ogg", t_music); asset->add(prefix + "/data/music/title.ogg", t_music); // Sonidos asset->add(prefix + "/data/sound/balloon.wav", t_sound); asset->add(prefix + "/data/sound/bubble1.wav", t_sound); asset->add(prefix + "/data/sound/bubble2.wav", t_sound); asset->add(prefix + "/data/sound/bubble3.wav", t_sound); asset->add(prefix + "/data/sound/bubble4.wav", t_sound); asset->add(prefix + "/data/sound/bullet.wav", t_sound); asset->add(prefix + "/data/sound/coffeeout.wav", t_sound); asset->add(prefix + "/data/sound/hiscore.wav", t_sound); asset->add(prefix + "/data/sound/itemdrop.wav", t_sound); asset->add(prefix + "/data/sound/itempickup.wav", t_sound); asset->add(prefix + "/data/sound/menu_move.wav", t_sound); asset->add(prefix + "/data/sound/menu_select.wav", t_sound); asset->add(prefix + "/data/sound/player_collision.wav", t_sound); asset->add(prefix + "/data/sound/stage_change.wav", t_sound); asset->add(prefix + "/data/sound/title.wav", t_sound); asset->add(prefix + "/data/sound/clock.wav", t_sound); asset->add(prefix + "/data/sound/powerball.wav", t_sound); // Texturas asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap); asset->add(prefix + "/data/gfx/balloon1.ani", t_data); asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap); asset->add(prefix + "/data/gfx/balloon2.ani", t_data); asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap); asset->add(prefix + "/data/gfx/balloon3.ani", t_data); asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap); asset->add(prefix + "/data/gfx/balloon4.ani", t_data); asset->add(prefix + "/data/gfx/bullet.png", t_bitmap); asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap); asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap); asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap); asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap); asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap); asset->add(prefix + "/data/gfx/game_text.png", t_bitmap); asset->add(prefix + "/data/gfx/intro.png", t_bitmap); asset->add(prefix + "/data/gfx/logo.png", t_bitmap); asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap); asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap); asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap); asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data); asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap); asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data); asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap); asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data); asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap); asset->add(prefix + "/data/gfx/item_clock.ani", t_data); asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap); asset->add(prefix + "/data/gfx/item_coffee.ani", t_data); asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap); asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data); asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap); asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap); asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap); asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap); asset->add(prefix + "/data/gfx/title_dust.ani", t_data); asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap); asset->add(prefix + "/data/gfx/player_head.ani", t_data); asset->add(prefix + "/data/gfx/player_body.ani", t_data); asset->add(prefix + "/data/gfx/player_legs.ani", t_data); asset->add(prefix + "/data/gfx/player_death.ani", t_data); asset->add(prefix + "/data/gfx/player_fire.ani", t_data); asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap); asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap); asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap); asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap); asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap); asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap); asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap); asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap); asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap); asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap); // Fuentes asset->add(prefix + "/data/font/8bithud.png", t_font); asset->add(prefix + "/data/font/8bithud.txt", t_font); asset->add(prefix + "/data/font/nokia.png", t_font); asset->add(prefix + "/data/font/nokia_big2.png", t_font); asset->add(prefix + "/data/font/nokia.txt", t_font); asset->add(prefix + "/data/font/nokia2.png", t_font); asset->add(prefix + "/data/font/nokia2.txt", t_font); asset->add(prefix + "/data/font/nokia_big2.txt", t_font); asset->add(prefix + "/data/font/smb2_big.png", t_font); asset->add(prefix + "/data/font/smb2_big.txt", t_font); asset->add(prefix + "/data/font/smb2.png", t_font); asset->add(prefix + "/data/font/smb2.txt", t_font); // Textos asset->add(prefix + "/data/lang/es_ES.txt", t_lang); asset->add(prefix + "/data/lang/en_UK.txt", t_lang); asset->add(prefix + "/data/lang/ba_BA.txt", t_lang); // Menus asset->add(prefix + "/data/menu/title.men", t_data); asset->add(prefix + "/data/menu/title_gc.men", t_data); asset->add(prefix + "/data/menu/options.men", t_data); asset->add(prefix + "/data/menu/options_gc.men", t_data); asset->add(prefix + "/data/menu/pause.men", t_data); asset->add(prefix + "/data/menu/gameover.men", t_data); asset->add(prefix + "/data/menu/player_select.men", t_data); return asset->check(); } // Comprueba los parametros del programa void Director::checkProgramArguments(int argc, const char *argv[]) { // Establece la ruta del programa executablePath = argv[0]; // Comprueba el resto de parametros for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--console") == 0) { Options::settings.console = true; } } } // Crea la carpeta del sistema donde guardar datos void Director::createSystemFolder(const std::string &folder) { #ifdef __EMSCRIPTEN__ // En Emscripten usamos una carpeta en MEMFS (no persistente) systemFolder = "/config/" + folder; #elif _WIN32 systemFolder = std::string(getenv("APPDATA")) + "/" + folder; #elif __APPLE__ struct passwd *pw = getpwuid(getuid()); const char *homedir = pw->pw_dir; systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder; #elif __linux__ struct passwd *pw = getpwuid(getuid()); const char *homedir = pw->pw_dir; systemFolder = 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 #ifdef __EMSCRIPTEN__ // En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente) (void)folder; #else struct stat st = {0}; if (stat(systemFolder.c_str(), &st) == -1) { errno = 0; #ifdef _WIN32 int ret = mkdir(systemFolder.c_str()); #else int ret = mkdir(systemFolder.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 } // Gestiona las transiciones entre secciones void Director::handleSectionTransition() { // Determina qué sección debería estar activa ActiveSection targetSection = ActiveSection::None; switch (section->name) { case SECTION_PROG_LOGO: targetSection = ActiveSection::Logo; break; case SECTION_PROG_INTRO: targetSection = ActiveSection::Intro; break; case SECTION_PROG_TITLE: targetSection = ActiveSection::Title; break; case SECTION_PROG_GAME: targetSection = ActiveSection::Game; break; } // Si no ha cambiado, no hay nada que hacer if (targetSection == activeSection) return; // Destruye la sección anterior logo.reset(); intro.reset(); title.reset(); game.reset(); // Crea la nueva sección activeSection = targetSection; switch (activeSection) { case ActiveSection::Logo: logo = std::make_unique(renderer, screen, asset, input, section); break; case ActiveSection::Intro: intro = std::make_unique(renderer, screen, asset, input, lang, section); break; case ActiveSection::Title: title = std::make_unique(renderer, screen, input, asset, lang, section); break; case ActiveSection::Game: { const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2; game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, section); break; } case ActiveSection::None: break; } } // Ejecuta un frame del juego SDL_AppResult Director::iterate() { #ifdef __EMSCRIPTEN__ // En WASM no se puede salir: reinicia al logo if (section->name == SECTION_PROG_QUIT) { section->name = SECTION_PROG_LOGO; } #else if (section->name == SECTION_PROG_QUIT) { return SDL_APP_SUCCESS; } #endif // Actualiza la visibilidad del cursor del ratón Mouse::updateCursorVisibility(Options::video.fullscreen); // Gestiona las transiciones entre secciones handleSectionTransition(); // Ejecuta un frame de la sección activa switch (activeSection) { case ActiveSection::Logo: logo->iterate(); break; case ActiveSection::Intro: intro->iterate(); break; case ActiveSection::Title: title->iterate(); break; case ActiveSection::Game: game->iterate(); break; case ActiveSection::None: break; } return SDL_APP_CONTINUE; } // Procesa un evento SDL_AppResult Director::handleEvent(SDL_Event *event) { #ifndef __EMSCRIPTEN__ // Evento de salida de la aplicación if (event->type == SDL_EVENT_QUIT) { section->name = SECTION_PROG_QUIT; return SDL_APP_SUCCESS; } #endif // Hot-plug de mandos if (event->type == SDL_EVENT_GAMEPAD_ADDED) { std::string name; if (input->handleGamepadAdded(event->gdevice.which, name)) { screen->notify(name + " " + lang->getText(94), color_t{0x40, 0xFF, 0x40}, color_t{0, 0, 0}, 2500); } } else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) { std::string name; if (input->handleGamepadRemoved(event->gdevice.which, name)) { screen->notify(name + " " + lang->getText(95), color_t{0xFF, 0x50, 0x50}, color_t{0, 0, 0}, 2500); } } // Gestiona la visibilidad del cursor según el movimiento del ratón Mouse::handleEvent(*event, Options::video.fullscreen); // Reenvía el evento a la sección activa switch (activeSection) { case ActiveSection::Logo: logo->handleEvent(event); break; case ActiveSection::Intro: intro->handleEvent(event); break; case ActiveSection::Title: title->handleEvent(event); break; case ActiveSection::Game: game->handleEvent(event); break; case ActiveSection::None: break; } return SDL_APP_CONTINUE; }