443 lines
15 KiB
C++
443 lines
15 KiB
C++
#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;
|
||
}
|