build d'emscripten
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user