Files
aee-arcade/source/core/system/director.cpp
T

333 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "core/system/director.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include "core/audio/audio.hpp"
#include "core/input/gamepad_manager.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/service_menu.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 + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen a Defaults::UI::Font (un únic lloc per canviar-los).
Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal");
const std::string HUD_FONT_GIF_KEY = std::string(Defaults::UI::Font::HUD_NAME) + ".gif";
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, Defaults::UI::Font::HUD_GIF);
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
const std::string PLACEHOLDER_FONT_GIF_KEY = std::string(Defaults::UI::Font::PLACEHOLDER_NAME) + ".gif";
Resource::Cache::get().loadSurface(PLACEHOLDER_FONT_GIF_KEY, Defaults::UI::Font::PLACEHOLDER_GIF);
Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY)->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());
// Gamepad: descobreix mandos presents silenciosament. A partir d'ara
// els events SDL_EVENT_GAMEPAD_ADDED/REMOVED arribaran via
// `processGamepadEvents()` i dispararan notificacions.
GamepadManager::get().init();
// 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(HUD_FONT_GIF_KEY);
auto hud_text = std::make_shared<Text>(hud_font_surface, Defaults::UI::Font::HUD_FNT);
hud_text->setDefaultKerning(Defaults::UI::Font::HUD_KERNING);
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);
// Font dels placeholders (LOGO / TITLE / INTRO). És una font distinta a
// la de HUD perquè els placeholders desapareixeran amb les seues escenes
// finals; mentrestant tenen estètica pròpia.
auto placeholder_font_surface = Resource::Cache::get().getSurface(PLACEHOLDER_FONT_GIF_KEY);
auto placeholder_text = std::make_shared<Text>(placeholder_font_surface, Defaults::UI::Font::PLACEHOLDER_FNT);
placeholder_text->setDefaultKerning(Defaults::UI::Font::PLACEHOLDER_KERNING);
// Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més
// files). Els dos noms i kernings viuen a `Defaults::UI::Font`.
auto load_menu_font = [&](const char* name, const char* gif, const char* fnt, int kerning) {
const std::string KEY = std::string(name) + ".gif";
Resource::Cache::get().loadSurface(KEY, gif);
Resource::Cache::get().getSurface(KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
auto text = std::make_shared<Text>(Resource::Cache::get().getSurface(KEY), fnt);
text->setDefaultKerning(kerning);
return text;
};
auto menu_title_text = load_menu_font(
Defaults::UI::Font::MENU_TITLE_NAME,
Defaults::UI::Font::MENU_TITLE_GIF,
Defaults::UI::Font::MENU_TITLE_FNT,
Defaults::UI::Font::MENU_TITLE_KERNING);
auto menu_items_text = load_menu_font(
Defaults::UI::Font::MENU_ITEMS_NAME,
Defaults::UI::Font::MENU_ITEMS_GIF,
Defaults::UI::Font::MENU_ITEMS_FNT,
Defaults::UI::Font::MENU_ITEMS_KERNING);
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
// Overlay hook: Screen::present() cridarà aquesta lambda al principi,
// ABANS de pujar els píxels al backend GPU. Així el menú queda per
// damunt de qualsevol escena sense que Screen hagi de conèixer game/ui.
Screen::get().setOverlayRenderer([]() { ServiceMenu::get().render(); });
// 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<Scene> { return std::make_unique<GameScene>(); };
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
return std::make_unique<IntroScene>(placeholder_text, make_game);
};
auto make_title = [placeholder_text, make_intro]() -> std::unique_ptr<Scene> {
return std::make_unique<TitleScene>(placeholder_text, make_intro);
};
switch (START) {
case DebugConfig::StartScene::LOGO:
current_scene_ = std::make_unique<LogoScene>(placeholder_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();
GamepadManager::get().shutdown();
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::processGamepadEvents() {
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
// tractar el botó BACK com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
// passa pel pipeline del `ServiceMenu::handleEvent()` via
// `processMenuInput()` i allà es converteix en `close()`.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
bool consumed = false;
if (it->type == SDL_EVENT_GAMEPAD_ADDED || it->type == SDL_EVENT_GAMEPAD_REMOVED) {
GamepadManager::get().handleEvent(*it);
consumed = true;
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
!ServiceMenu::get().isOpen()) {
ServiceMenu::get().toggle();
consumed = true;
}
if (consumed) {
it = EventBuffer::events.erase(it);
} else {
++it;
}
}
}
void Director::processMenuInput() {
// Intercepta input abans que l'escena vegi els events. Si el menú està
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8,
// F11, F12) ni el jugador els veuen. F9 obre el menú quan està tancat.
// Erase invalida iteradors, per això actualitzem `it` a cada branca.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
if (ServiceMenu::get().isOpen() && ServiceMenu::get().handleEvent(*it)) {
it = EventBuffer::events.erase(it);
} else if (it->type == SDL_EVENT_KEY_DOWN &&
it->key.scancode == KeyConfig::get().scancode("menu_toggle")) {
ServiceMenu::get().toggle();
it = EventBuffer::events.erase(it);
} else {
++it;
}
}
}
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 {
// Ordre important: primer els events de gamepad (hot-plug + BACK que
// obre el menú quan està tancat), després el menú (pot consumir tecles
// i INJECTAR un SDL_EVENT_QUIT des de l'acció "Eixir del joc"), i
// finalment processEvents perquè vegi el QUIT al mateix frame — altrament
// el QUIT injectat es perdria quan es netegi EventBuffer al final.
processGamepadEvents();
processMenuInput();
processEvents();
if (!current_scene_) {
EventBuffer::events.clear();
return SDL_APP_SUCCESS;
}
const std::uint64_t NOW_MS = SDL_GetTicks();
int delta_ms = static_cast<int>(NOW_MS - last_ticks_ms_);
delta_ms = std::max(delta_ms, 0);
last_ticks_ms_ = NOW_MS;
Audio::update();
ServiceMenu::get().update(delta_ms);
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;
}