build d'emscripten

This commit is contained in:
2026-04-17 10:00:37 +02:00
parent 6ea50cf35e
commit 5eb178b039
12 changed files with 316 additions and 44 deletions

View File

@@ -45,6 +45,11 @@ namespace GlobalInputs {
return;
}
#ifdef __EMSCRIPTEN__
// En la versión web no se puede salir del juego desde fuera de la escena GAME
// (el navegador gestiona la pestaña; Escape no cierra nada).
return;
#else
// Comportamiento normal fuera del modo kiosko
const std::string CODE = "PRESS AGAIN TO EXIT";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -52,6 +57,7 @@ namespace GlobalInputs {
} else {
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE);
}
#endif // __EMSCRIPTEN__
}
void handleSkipSection() {

View File

@@ -10,6 +10,40 @@
#include "game/options.hpp" // Para Options::controls
// Emscripten-only: SDL 3.4+ no casa el GUID de los mandos de Chrome Android
// con gamecontrollerdb (el gamepad.id de Android no lleva Vendor/Product, el
// parser extrae valores basura, el GUID resultante no está en la db y el
// gamepad queda abierto con un mapping incorrecto). Como la W3C Gamepad API
// garantiza el layout estándar cuando el navegador reporta mapping=="standard",
// inyectamos un mapping SDL con ese layout para el GUID del joystick antes
// de abrirlo como gamepad. Fuera de Emscripten es un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if ((name == nullptr) || (*name == 0)) { name = "Standard Gamepad"; }
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr, name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton
Input* Input::instance = nullptr;
@@ -397,6 +431,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string {
}
auto Input::addGamepad(int device_index) -> std::string {
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -407,7 +442,13 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
// Aplica los bindings de Options al nuevo gamepad. En hot-plug (incluido wasm,
// donde el navegador sólo expone los gamepads tras activación del usuario) el
// ctor ya llamó a applyGamepadBindingsFromOptions() pero gamepads_ estaba vacío.
applyGamepadBindingsFromOptions();
return name;
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
@@ -419,7 +460,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
return name;
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};

View File

@@ -51,10 +51,20 @@ class Input {
std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto pos = s.find_first_of("([");
if (pos != std::string::npos) { s.erase(pos); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador

View File

@@ -1,6 +1,10 @@
#include "core/rendering/screen.hpp"
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para max, min, transform
#include <cctype> // Para toupper
@@ -11,9 +15,11 @@
#include <iterator> // Para istreambuf_iterator, operator==
#include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#ifndef __EMSCRIPTEN__
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no soportado en WebGL2)
#endif
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
@@ -26,6 +32,38 @@
// [SINGLETON]
Screen* Screen::screen = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauración del canvas en wasm/Emscripten
// ============================================================================
// SDL3 + Emscripten no notifica de manera fiable los cambios de tamaño del
// canvas HTML (fullscreen exit con Esc, orientationchange). Registramos
// callbacks nativos de Emscripten que re-sincronizan SDL con el estado real
// del navegador delegando en Screen::handleCanvasResized() → setVideoMode().
// Se difiere con emscripten_async_call(0ms) porque cuando el event llega el
// canvas aún no está estable.
// Referencias:
// - https://github.com/libsdl-org/SDL/issues/13300
// - https://github.com/libsdl-org/SDL/issues/11389
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() {
Screen::screen = new Screen();
@@ -164,6 +202,16 @@ void Screen::toggleVideoMode() {
setVideoMode(Options::video.fullscreen);
}
// Re-sincroniza SDL con el estado real del canvas del navegador. Lo invocan los
// callbacks nativos de Emscripten cuando detectan un fullscreenchange u
// orientationchange. En desktop SDL emite SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED
// correctamente, así que fuera de emscripten es un no-op.
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
setVideoMode(Options::video.fullscreen);
#endif
}
// Reduce el tamaño de la ventana
auto Screen::decWindowZoom() -> bool {
if (static_cast<int>(Options::video.fullscreen) == 0) {
@@ -636,6 +684,10 @@ void Screen::nextShader() {
// El device GPU se crea siempre (independientemente de postfx) para evitar
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
void Screen::initShaders() {
#ifdef __EMSCRIPTEN__
// En WebGL2 no hay SDL3 GPU; el render va por SDL_Renderer sin shaders.
shader_backend_.reset();
#else
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
if (!shader_backend_) {
@@ -664,6 +716,7 @@ void Screen::initShaders() {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
applyCurrentCrtPiPreset();
}
#endif
}
// Obtiene información sobre la pantalla
@@ -767,10 +820,24 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
registerEmscriptenEventCallbacks();
std::cout << "Video system initialized successfully\n";
return true;
}
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
// NO registramos resize callback. En móvil, el scroll hace que el navegador
// oculte/muestre la barra de URL, disparando un resize del DOM por cada scroll,
// lo que nos llevaría a llamar setVideoMode innecesariamente.
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Crea el objeto de texto
void Screen::createText() {
// Carga la surface de la fuente directamente del archivo

View File

@@ -39,6 +39,7 @@ class Screen {
// Video y ventana
void setVideoMode(bool mode); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas cuando SDL3 no reporta el cambio (emscripten only: salida de fullscreen con Esc, rotación); no-op fuera de emscripten
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
@@ -144,8 +145,9 @@ class Screen {
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void createText(); // Crea el objeto de texto
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
void createText(); // Crea el objeto de texto
// Constructor y destructor
Screen();

View File

@@ -38,7 +38,7 @@
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif
#ifndef _WIN32
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
@@ -46,12 +46,17 @@
Director::Director() {
std::cout << "Game start" << '\n';
#ifdef __EMSCRIPTEN__
// En Emscripten los assets están en el root del filesystem virtual (/data, /config)
executable_path_ = "";
#else
// Obtiene la ruta del ejecutable
std::string base = SDL_GetBasePath();
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
executable_path_ = base;
#endif
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
@@ -81,7 +86,7 @@ Director::Director() {
// Preparar ruta al pack (en macOS bundle está en Contents/Resources/)
std::string pack_path = executable_path_ + PREFIX + "/resources.pack";
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// ============================================================
// RELEASE BUILD: Pack-first architecture
// ============================================================
@@ -139,6 +144,18 @@ Director::Director() {
Options::setConfigFile(Resource::List::get()->get("config.yaml"));
Options::loadFromFile();
#ifdef __EMSCRIPTEN__
// A la versión web el navegador gestiona la ventana: forzamos zoom x4
// para que la textura 256x192 no se vea minúscula en el canvas HTML,
// y activamos el borde para aprovechar al máximo el espacio del canvas.
Options::video.fullscreen = false;
Options::video.integer_scale = true;
Options::window.zoom = 4;
Options::video.border.enabled = true;
Options::video.border.height = 8;
Options::video.border.width = 8;
#endif
// Configura la ruta y carga los presets de PostFX
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml"));
Options::loadPostFXFromFile();
@@ -191,7 +208,7 @@ Director::Director() {
KeyConfig::init("data/input/keys.yaml");
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// In release, construct the path manually (not from Asset which has empty executable_path)
std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt";
Input::init(gamecontroller_db);
@@ -215,7 +232,7 @@ Director::Director() {
std::cout << "\n"; // Fin de inicialización de sistemas
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
{
// En release el locale está en el pack, no en el filesystem
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml");
@@ -228,7 +245,7 @@ Director::Director() {
#endif
// Special handling for cheevos.bin - also needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
std::string cheevos_path = system_folder_ + "/cheevos.bin";
Cheevos::init(cheevos_path);
#else
@@ -271,6 +288,12 @@ Director::~Director() {
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten utilizamos MEMFS (no persistente entre sesiones).
// No hace falta crear directorios: MEMFS los crea automáticamente al escribir.
system_folder_ = "/config/" + folder;
return;
#else
#ifdef _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
@@ -322,6 +345,7 @@ void Director::createSystemFolder(const std::string& folder) {
}
}
}
#endif // __EMSCRIPTEN__
}
// Carga la configuración de assets desde assets.yaml

View File

@@ -1,23 +1,45 @@
#include "core/system/global_events.hpp"
#include "core/input/input.hpp" // Para Input (gamepad add/remove)
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" // Para Locale
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event) {
// Evento de salida de la aplicación
#ifndef __EMSCRIPTEN__
// En la versión web no tenemos evento de quit del navegador
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
return;
}
#endif
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
// reLoadTextures();
}
// Conexión/desconexión de gamepads: hay que enrutarlos a Input para que
// añada el dispositivo a gamepads_. Sin esto, en wasm los gamepads
// nunca se detectan (la Gamepad API del navegador sólo los expone
// tras que el usuario los active, más tarde que el discoverGamepads
// inicial). En desktop también arregla la conexión en caliente.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (Input::get() != nullptr) {
std::string name = Input::get()->handleEvent(event);
if (!name.empty() && Notifier::get() != nullptr && Locale::get() != nullptr) {
const std::string KEY = (event.type == SDL_EVENT_GAMEPAD_ADDED)
? "ui.gamepad_connected"
: "ui.gamepad_disconnected";
Notifier::get()->show({name + " " + Locale::get()->get(KEY)});
}
}
}
// Enrutar eventos de texto a la consola cuando está activa
if (Console::get() != nullptr && Console::get()->isActive()) {
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {

View File

@@ -946,11 +946,16 @@ static auto cmdKiosk(const std::vector<std::string>& args) -> std::string {
// EXIT / QUIT
static auto cmdExit(const std::vector<std::string>& args) -> std::string {
#ifdef __EMSCRIPTEN__
(void)args;
return "Not allowed in web version";
#else
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode";
}
SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting...";
#endif
}
// SIZE