Files
orni-attack/source/core/system/director.cpp
T

443 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 "director.hpp"
#include <SDL3/SDL.h>
#include <sys/stat.h>
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <iostream>
#include <memory>
#include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp"
#include "core/defaults/window.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.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 <pwd.h>
#include <unistd.h>
#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<std::string> i comprovar-los
std::vector<std::string> 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. El gamecontrollerdb.txt viu al costat del
// binari (no dins de resources.pack, perquè SDL_AddGamepadMappingsFromFile
// necessita una ruta real de filesystem). resource_base ja apunta al directori
// de l'executable (o a Contents/Resources en bundles de macOS).
Input::init(resource_base + "/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
// arrancades.
{
auto& p1 = cfg_->player1;
auto& p2 = cfg_->player2;
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
if (BOTH_EMPTY) {
const auto& pads = Input::get()->getGamepads();
bool changed = false;
if (!pads.empty() && pads[0]) {
p1.gamepad_name = pads[0]->name;
p1.gamepad_path = pads[0]->path;
changed = true;
}
if (pads.size() > 1 && pads[1]) {
p2.gamepad_name = pads[1]->name;
p2.gamepad_path = pads[1]->path;
changed = true;
}
if (changed) {
ConfigYaml::saveToFile();
}
}
}
// 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<int>(std::round(
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * cfg_->window.zoom_factor));
sdl_ = std::make_unique<SDLManager>(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 = cfg_->audio.enabled,
.volume = cfg_->audio.volume,
.music_enabled = cfg_->audio.music_enabled,
.music_volume = cfg_->audio.music_volume,
.sound_enabled = cfg_->audio.sound_enabled,
.sound_volume = cfg_->audio.sound_volume,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG);
// Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
// de decodificació en transicions (TITLE → GAME, primera explosió, etc.).
// Mateix patró que aee_arcade: iterem `listResources` i forcem la càrrega
// al cache de cada subsistema.
for (const auto& path : Resource::Helper::listResources("music/")) {
AudioResource::getMusic(path.substr(std::string_view{"music/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("sounds/")) {
AudioResource::getSound(path.substr(std::string_view{"sounds/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("shapes/")) {
Graphics::ShapeLoader::load(path.substr(std::string_view{"shapes/"}.size()));
}
if (cfg_->console) {
std::cout << "Recursos precachejats (música, sons, shapes)\n";
}
context_ = std::make_unique<SceneContext>();
#ifdef _DEBUG
context_->setNextScene(SceneType::TITLE);
#else
context_->setNextScene(SceneType::LOGO);
#endif
debug_overlay_ = std::make_unique<System::DebugOverlay>(
sdl_->getRenderer(),
cfg_->rendering);
System::Notifier::init(sdl_->getRenderer());
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
System::DefineInputs::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::DefineInputs::destroy();
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<std::string> 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<Scene> {
switch (type) {
case SceneType::LOGO:
return std::make_unique<LogoScene>(sdl, context);
case SceneType::TITLE:
return std::make_unique<TitleScene>(sdl, context);
case SceneType::GAME:
return std::make_unique<GameScene>(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<float>(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);
}
if (auto* di = System::DefineInputs::get(); di != nullptr) {
di->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
}
// Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
// pero no es pinta per no confondre's visualment amb el modal).
const auto* di = System::DefineInputs::get();
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
menu->draw(); // service menu: per damunt fins i tot dels toasts
}
if (di != nullptr) {
di->draw(); // overlay de rebind: per damunt de tot
}
sdl_->present();
return SDL_APP_CONTINUE;
}