#include "core/system/director.hpp" #include #include #include #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/input/key_config.hpp" #include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "core/scene/scene.hpp" #include "core/system/event_buffer.hpp" #include "game/debug_config.hpp" #include "game/options.hpp" #include "game/scenes/game_scene.hpp" #include "game/scenes/intro_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" #include "game/ui/ui_palette.hpp" #include "utils/defaults.hpp" Director::Director() { if (!SDL_Init(0)) { std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); } // Rutes dels recursos relatives a l'executable, no al CWD. SDL_GetBasePath // retorna la carpeta del binari al desplegament portable i Contents/ // Resources/ dins d'un .app a macOS. Ha de fer-se abans de qualsevol // loader que passi per Resource::Helper::resolve (Locale, KeyConfig, // Resource::Cache, PaletteLoader, etc.). if (const char* base = SDL_GetBasePath()) { Resource::Helper::setDataRoot(base); } // Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega // les opcions; si no existeix config.yaml el crea amb els defaults. createSystemFolder(); Options::setConfigFile(system_folder_ + "config.yaml"); if (!Options::loadFromFile()) { std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n'; } // Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix // es crea amb presets per defecte (off, crt, ntsc). Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE); if (!Options::loadPostFXFromFile()) { std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n"; } // Presets del shader CRT-Pi (shaders/crtpi.yaml). Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE); if (!Options::loadCrtPiFromFile()) { std::cerr << "Director: no s'ha pogut carregar ni desar crtpi.yaml.\n"; } // Screen es configura ara (subsystem + finestra + GPU/renderer); la seua // destrucció la fa aquest Director (veure ~Director). Res de static dtor. Screen::get().configure(Options::buildScreenConfig()); Screen::get().setActiveShader(Options::buildActiveShader()); Screen::get().applyPostFXParams(Options::buildPostFXParams()); Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); (void)Resource::Cache::get(); // Traduccions per a tots els textos visibles. Si el fitxer no existeix // o hi ha un error de parse, Locale::text() caurà a retornar la clau. Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml"); // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. KeyConfig::get().load("data/input/keys.yaml"); // Recursos compartits d'UI (paleta + font). Es carreguen abans que els // overlays perquè tots els reaprofiten sense duplicar la lògica. Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif"); Resource::Cache::get().getSurface("8bithud.gif")->setPalette(Resource::Cache::get().getPalette("ui")); // Música de la piràmide 1 (primer i únic track de moment). Carreguem ací // perquè forma part del preload d'assets, com la font i la paleta. Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg"); // Audio: singleton manual propietari d'un SDL audio device. Es destrueix // explícitament a ~Director abans que Resource::Cache (Meyers) buide els // JA_Music_t que continga, així que no queden streams orfes. Audio::init(Options::buildAudioConfig()); // Font compartida pels overlays: construïm una sola instància de Text // i la injectem als consumidors (Notifier, RenderInfo) per a que cap // d'ells hagi de conèixer els noms dels recursos concrets. auto hud_font_surface = Resource::Cache::get().getSurface("8bithud.gif"); auto hud_text = std::make_shared(hud_font_surface, "data/font/8bithud.fnt"); const auto& ui_palette = Resource::Cache::get().getPalette("ui"); RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text); Notifier::get().init(hud_text, ui_palette); // Flux d'escenes LOGO → TITLE → INTRO → GAME. Cada placeholder rep una // factory que construeix la següent, construïda bottom-up des de GAME. // // Release: sempre s'arranca a LOGO (el flux complet, el que veu el // jugador final). Debug: s'obri `debug.yaml`; si no existeix, el creem // amb `start_scene: GAME` perquè el dia a dia de gameplay no es mengi // els 15 s de placeholders. L'usuari pot editar-lo a mà per escollir // un punt d'entrada intermedi. #ifdef _DEBUG const auto DEBUG_YAML = system_folder_ + "debug.yaml"; DebugConfig::createDefaultIfMissing(DEBUG_YAML); const auto START = DebugConfig::resolveStartScene(DEBUG_YAML); #else const auto START = DebugConfig::StartScene::LOGO; #endif std::cout << "Director: start_scene=" << DebugConfig::toString(START) << '\n'; auto make_game = []() -> std::unique_ptr { return std::make_unique(); }; auto make_intro = [hud_text, make_game]() -> std::unique_ptr { return std::make_unique(hud_text, make_game); }; auto make_title = [hud_text, make_intro]() -> std::unique_ptr { return std::make_unique(hud_text, make_intro); }; switch (START) { case DebugConfig::StartScene::LOGO: current_scene_ = std::make_unique(hud_text, make_title); break; case DebugConfig::StartScene::TITLE: current_scene_ = make_title(); break; case DebugConfig::StartScene::INTRO: current_scene_ = make_intro(); break; case DebugConfig::StartScene::GAME: current_scene_ = make_game(); break; } last_ticks_ms_ = SDL_GetTicks(); } Director::~Director() { // Ordre imprescindible: primer l'escena (pot tenir recursos); després // alliberem els recursos d'àudio del Cache mentre SDL encara està viu // (en Meyers, el Cache es destrueix després de SDL_Quit i els streams // dels JA_Music_t quedarien invalidats); després Audio (tanca l'audio // device), Screen (GPU + finestra) i finalment SDL_Quit. Fer-ho així -- // NO via atexit -- evita crashes a vkDestroyInstance quan el runtime // desfà les llibreries abans que corren els destructors estàtics. current_scene_.reset(); Resource::Cache::get().clearAudio(); Audio::destroy(); Screen::get().shutdown(); SDL_Quit(); } void Director::createSystemFolder() { // Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte // ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME // (.local/share), que és el lloc per a dades, no per a configuració. const char* xdg = std::getenv("XDG_CONFIG_HOME"); std::string base; if (xdg != nullptr && *xdg != '\0') { base = xdg; } else { const char* home = std::getenv("HOME"); base = (home != nullptr) ? std::string(home) + "/.config" : "."; } system_folder_ = base + "/jailgames/aee_2026/"; std::error_code error; std::filesystem::create_directories(system_folder_, error); // Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix. std::filesystem::create_directories(system_folder_ + "shaders/", error); if (error) { std::cerr << "Director: no s'ha pogut crear " << system_folder_ << ": " << error.message() << '\n'; } } void Director::processEvents() { const bool QUIT_REQUESTED = std::ranges::any_of( EventBuffer::events, [](const SDL_Event& event) { return event.type == SDL_EVENT_QUIT; }); if (QUIT_REQUESTED) { current_scene_.reset(); } } void Director::switchSceneIfRequested() { if (!current_scene_) { return; } if (auto next = current_scene_->nextScene()) { // TODO: migrar a logger unificat (INC-03 a docs/audit-core-rendering.md). std::cout << "Director: scene transition\n"; current_scene_ = std::move(next); } } auto Director::iterate() -> SDL_AppResult { processEvents(); if (!current_scene_) { EventBuffer::events.clear(); return SDL_APP_SUCCESS; } const std::uint64_t NOW_MS = SDL_GetTicks(); int delta_ms = static_cast(NOW_MS - last_ticks_ms_); delta_ms = std::max(delta_ms, 0); last_ticks_ms_ = NOW_MS; Audio::update(); current_scene_->update(delta_ms); current_scene_->render(); if (current_scene_->wantsQuit()) { EventBuffer::events.clear(); return SDL_APP_SUCCESS; } switchSceneIfRequested(); EventBuffer::events.clear(); return SDL_APP_CONTINUE; }