diff --git a/.gitignore b/.gitignore index 02c0bed..f0411ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,57 @@ +# --- Build outputs --- +build/ +dist/ aee aee.exe -.DS_Store -trick.ini -.vscode/ +*.o +*.obj +*.exe +*.app + +# --- Generated assets --- +resource.pack data.jrf -build/ -dist/ \ No newline at end of file + +# --- Runtime / debug junk --- +trick.ini +*.log +*.dmp + +# --- Editor / IDE --- +.vscode/ +.idea/ +*.swp +*.swo +*~ +.cache/ +compile_commands.json + +# --- macOS --- +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes +.fseventsd +.DocumentRevisions-V100 +.TemporaryItems +.VolumeIcon.icns +Icon? + +# --- Windows --- +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +Desktop.ini +desktop.ini +$RECYCLE.BIN/ +*.lnk + +# --- Linux --- +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* diff --git a/CLAUDE.md b/CLAUDE.md index a396f0f..ce84138 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,9 +113,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve ### Input Layer (`source/core/input/`) -- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer) +- **KeyConfig** (`key_config.hpp/cpp`) — **Font única de veritat per a les tecles d'UI/sistema**. Carrega `data/input/keys.yaml` al boot (12 entrades: F1-F10 GlobalInputs + F11 pausa + F12 menú de servei) i opcionalment aplica overrides des de `~/.config/jailgames/aee/keys.yaml`. Exposa `KeyConfig::scancode("id")`, `scancodePtr("id")` (per a Menu KeyBind), `setScancode(...)`, `isGuiKey(sc)` (filtre del Director per a no propagar tecles d'UI a `JI_AnyKey`). `saveOverrides()` només persistix les entrades que difereixen del default. Les tecles de moviment del jugador NO viuen ací — es queden a `Options::keys_game` +- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps function keys (via `KeyConfig::scancode("dec_zoom")`, etc.) to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer) - **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity -- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database +- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug + overlay notification with controller name. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement. Mapeig: SOUTH/EAST/WEST/NORTH (4 botons frontals) → Enter sintètic per avançar escenes; al menú EAST=accept, SOUTH=cancel/back. SELECT → menu_toggle (servei), START → pause_toggle (via `KeyConfig::scancode(...)`). Loads extra mappings from `gamecontrollerdb.txt` at init via `SDL_AddGamepadMappingsFromFile` - **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp` ### Locale Layer (`source/core/locale/`) @@ -127,8 +128,8 @@ Flat C-style APIs (no classes), prefixed by subsystem. Being progressively conve Follows the pattern from `jaildoctors_dilemma`, persists to YAML: - **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT` -- **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game` -- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset` +- **defaults.hpp** — Default values: `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`. (Les tecles d'UI viuen a `data/input/keys.yaml` via `KeyConfig`) +- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset` ### Utilities (`source/utils/`) @@ -155,7 +156,7 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML: | Backspace | Go up one menu level / close menu if at root | | ↑↓←→ / Enter | Menu navigation | -All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working. +UI/system key bindings are loaded from [data/input/keys.yaml](data/input/keys.yaml) via `KeyConfig`. Overrides fets des del menú es persistixen a `~/.config/jailgames/aee/keys.yaml` (només les que difereixen del default). Game movement keys (`Options::keys_game.up/down/left/right`) viuen separadament a `config.yaml` (secció `controls:`) i es remapejen via la CONTROLS submenu — el `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working. ### Execution Model (Single-threaded, Scene-based) @@ -224,7 +225,8 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text | File | Content | |------|---------| -| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) | +| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (només moviment del jugador) | +| `~/.config/jailgames/aee/keys.yaml` | UI key overrides (només entrades que difereixen del default de [data/input/keys.yaml](data/input/keys.yaml)). Generat per `KeyConfig::saveOverrides()` | | `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) | | `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) | diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fbad88..8729744 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ set(APP_SOURCES # Core - Input (nova capa) source/core/input/gamepad.cpp source/core/input/global_inputs.cpp + source/core/input/key_config.cpp source/core/input/key_remap.cpp source/core/input/mouse.cpp @@ -241,6 +242,27 @@ if(NOT EMSCRIPTEN) ) target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_compile_options(pack_resources PRIVATE -Wall) + + # --- Regeneració automàtica de resource.pack --- + # Cada `cmake --build build` torna a empaquetar `data/` si algun fitxer ha + # canviat. Evita debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake + # a re-globbar a la pròxima invocació (recull fitxers nous afegits a data/). + file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*") + set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resource.pack") + + add_custom_command( + OUTPUT ${RESOURCE_PACK} + COMMAND $ + "${CMAKE_SOURCE_DIR}/data" + "${RESOURCE_PACK}" + DEPENDS pack_resources ${DATA_FILES} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Empaquetant data/ → resource.pack" + VERBATIM + ) + + add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK}) + add_dependencies(${PROJECT_NAME} resource_pack) endif() # --- CLANG-FORMAT TARGETS --- diff --git a/data/input/keys.yaml b/data/input/keys.yaml new file mode 100644 index 0000000..ac201d5 --- /dev/null +++ b/data/input/keys.yaml @@ -0,0 +1,50 @@ +# Aventures En Egipte — Configuració de tecles d'UI +# +# Font única de veritat per a les tecles de funció / sistema. +# Les tecles de moviment del jugador viuen separades a config.yaml (secció `controls:`). +# +# Si l'usuari remapeja alguna tecla des del menú de servei, la diferència respecte +# aquests valors per defecte es persistix a ~/.config/jailgames/aee/keys.yaml. +# +# Camps: +# id - Identificador usat des del codi via KeyConfig::scancode("id") +# code - Nom SDL del scancode (per SDL_GetScancodeFromName), p.ex. "F1", "Escape" +# desc - Descripció curta (per a HELP / overlays futurs) + +keys: + - id: dec_zoom + code: "F1" + desc: "Redueix el zoom de la finestra" + - id: inc_zoom + code: "F2" + desc: "Augmenta el zoom de la finestra" + - id: fullscreen + code: "F3" + desc: "Pantalla completa" + - id: toggle_shader + code: "F4" + desc: "Activa/desactiva shaders" + - id: toggle_aspect_ratio + code: "F5" + desc: "Aspecte 4:3 / pixels quadrats" + - id: toggle_supersampling + code: "F6" + desc: "Activa/desactiva supersampling" + - id: next_shader + code: "F7" + desc: "Tipus de shader (PostFX / CRT-Pi)" + - id: next_shader_preset + code: "F8" + desc: "Pròxim preset del shader" + - id: toggle_stretch_filter + code: "F9" + desc: "Filtre 4:3 (nearest / linear)" + - id: toggle_render_info + code: "F10" + desc: "Mostra info de renderitzat" + - id: pause_toggle + code: "F11" + desc: "Pausa el joc" + - id: menu_toggle + code: "F12" + desc: "Menú de servei" diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 3567bc8..99d92fb 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -4,77 +4,78 @@ menu: titles: - root: "OPCIONS" - video: "VIDEO" - audio: "AUDIO" - controls: "CONTROLS" - game: "JOC" + root: "Opcions" + video: "Vídeo" + audio: "Àudio" + controls: "Controls" + game: "Joc" items: - video: "VIDEO" - audio: "AUDIO" - controls: "CONTROLS" - game: "JOC" - use_new_logo: "LOGO NOU" - show_title_credits: "CREDITS DEL PORT" - zoom: "ZOOM" - screen: "PANTALLA" - shader: "SHADER" - aspect_4_3: "ASPECTE 4:3" - supersampling: "SUPERSAMPLING" - vsync: "VSYNC" - integer_scale: "ESCALA ENTERA" - shader_type: "TIPUS SHADER" - preset: "PRESET" - stretch_filter: "FILTRE 4:3" - render_info: "RENDER INFO" - uptime: "TEMPS DE JOC" - master_enable: "AUDIO" - master_volume: "MASTER" - music: "MUSICA" - music_volume: "VOL MUSICA" - sounds: "SONS" - sounds_volume: "VOL SONS" - move_up: "MOU AMUNT" - move_down: "MOU AVALL" - move_left: "MOU ESQUERRA" - move_right: "MOU DRETA" - menu_key: "TECLA MENU" + video: "Vídeo" + audio: "Àudio" + controls: "Controls" + game: "Joc" + use_new_logo: "Logo nou" + show_title_credits: "Crèdits del port" + zoom: "Zoom" + screen: "Pantalla" + shader: "Shader" + aspect_4_3: "Aspecte 4:3" + supersampling: "Supersampling" + vsync: "Vsync" + integer_scale: "Escala entera" + shader_type: "Tipus shader" + preset: "Preset" + stretch_filter: "Filtre 4:3" + render_info: "Render info" + uptime: "Temps de joc" + master_enable: "Àudio" + master_volume: "Màster" + music: "Música" + music_volume: "Vol música" + sounds: "Sons" + sounds_volume: "Vol sons" + move_up: "Mou amunt" + move_down: "Mou avall" + move_left: "Mou esquerra" + move_right: "Mou dreta" + menu_key: "Tecla menú" values: - "yes": "SI" - "no": "NO" - "on": "ON" - "off": "OFF" - fullscreen: "COMPLETA" - windowed: "FINESTRA" - linear: "LINEAR" - nearest: "NEAREST" - top: "TOP" - bottom: "BOTTOM" - press_key: "" - empty: "(BUIT)" + "yes": "Sí" + "no": "No" + "on": "On" + "off": "Off" + fullscreen: "Completa" + windowed: "Finestra" + linear: "Linear" + nearest: "Nearest" + top: "Top" + bottom: "Bottom" + press_key: "" + empty: "(Buit)" unknown: "---" window: title: "© 2000 Aventures en Egipte — JailDesigner" notifications: - exit_double_esc: "TORNA A PULSAR ESC PER EIXIR" - zoom_fmt: "ZOOM %dX" - fullscreen: "PANTALLA COMPLETA" - windowed: "FINESTRA" - shader_on: "SHADER ON" - shader_off: "SHADER OFF" + exit_double_esc: "Torna a pulsar ESC per a eixir" + zoom_fmt: "Zoom %dX" + fullscreen: "Pantalla completa" + windowed: "Finestra" + shader_on: "Shader on" + shader_off: "Shader off" aspect_43: "4:3 CRT" - aspect_square: "PIXELS QUADRATS" - ss_on: "SUPERSAMPLING ON" - ss_off: "SUPERSAMPLING OFF" - preset_fmt: "PRESET: %s" - filter_linear: "FILTRE: LINEAR" - filter_nearest: "FILTRE: NEAREST" - pause: "PAUSA" - resume: "REPRES" + aspect_square: "Píxels quadrats" + ss_on: "Supersampling on" + ss_off: "Supersampling off" + preset_fmt: "Preset: %s" + filter_linear: "Filtre: linear" + filter_nearest: "Filtre: nearest" + pause: "Pausa" + gamepad_connected: "connectat" + gamepad_disconnected: "desconnectat" credits: port_role: "Conversio a C++ i SDL3" diff --git a/resource.pack b/resource.pack deleted file mode 100644 index 989554b..0000000 Binary files a/resource.pack and /dev/null differ diff --git a/source/core/input/gamepad.cpp b/source/core/input/gamepad.cpp index 7304284..b5ab68a 100644 --- a/source/core/input/gamepad.cpp +++ b/source/core/input/gamepad.cpp @@ -1,10 +1,13 @@ #include "core/input/gamepad.hpp" #include +#include +#include "core/input/key_config.hpp" #include "core/jail/jinput.hpp" +#include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" -#include "game/options.hpp" +#include "core/rendering/overlay.hpp" namespace Gamepad { @@ -19,11 +22,23 @@ namespace Gamepad { static bool prev_down_ = false; static bool prev_left_ = false; static bool prev_right_ = false; - static bool prev_a_ = false; - static bool prev_b_ = false; + static bool prev_south_ = false; + static bool prev_east_ = false; + static bool prev_west_ = false; + static bool prev_north_ = false; static bool prev_start_ = false; static bool prev_back_ = false; + static void notify(const char* name, const char* status_key) { + std::string msg = (name && *name) ? name : "Gamepad"; + msg += ' '; + msg += Locale::get(status_key); + Overlay::showNotification(msg.c_str(), 2.5F); + } + + static void notifyConnected(const char* name) { notify(name, "notifications.gamepad_connected"); } + static void notifyDisconnected(const char* name) { notify(name, "notifications.gamepad_disconnected"); } + static void openFirstGamepad() { int count = 0; SDL_JoystickID* ids = SDL_GetGamepads(&count); @@ -70,12 +85,16 @@ namespace Gamepad { pad_ = SDL_OpenGamepad(event.gdevice.which); if (pad_) { pad_id_ = event.gdevice.which; - SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_)); + const char* name = SDL_GetGamepadName(pad_); + SDL_Log("Gamepad connectat: %s", name ? name : ""); + notifyConnected(name); } } } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) { if (pad_ && event.gdevice.which == pad_id_) { - SDL_Log("Gamepad desconnectat"); + const char* name = SDL_GetGamepadName(pad_); + std::string saved_name = name ? name : ""; + SDL_Log("Gamepad desconnectat: %s", saved_name.c_str()); SDL_CloseGamepad(pad_); pad_ = nullptr; pad_id_ = 0; @@ -84,6 +103,7 @@ namespace Gamepad { JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false); + notifyDisconnected(saved_name.c_str()); } } } @@ -125,16 +145,18 @@ namespace Gamepad { bool lt = dlt || slt; bool rt = drt || srt; - // Botons - bool a = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); // A/Cross - bool b = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); // B/Circle + // Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle) + bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); + bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); + bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST); + bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH); bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START); bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK); - // Start → obre/tanca menú (flanc) - if (start && !prev_start_) pushKey(Options::keys_gui.menu_toggle); - // Back → ESC (flanc) - if (back && !prev_back_) pushKey(SDL_SCANCODE_ESCAPE); + // Select (Back) → obre/tanca menú de servei (flanc) + if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle")); + // Start → pausa (flanc) + if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle")); if (Menu::isOpen()) { // Navegació del menú per flanc @@ -142,8 +164,9 @@ namespace Gamepad { if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN); if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT); if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT); - if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN); - if (b && !prev_b_) pushKey(SDL_SCANCODE_BACKSPACE); + // EAST accepta, SOUTH cancela / endarrere + if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN); + if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE); // Assegura que el joc no rep tecles de moviment mentre el menú està obert JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false); @@ -156,16 +179,21 @@ namespace Gamepad { JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn); JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt); JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt); - // Botó A al joc: emet Enter per avançar seqüències (JI_AnyKey) - if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN); + // Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic) + if ((south && !prev_south_) || (east && !prev_east_) || + (west && !prev_west_) || (north && !prev_north_)) { + pushKey(SDL_SCANCODE_RETURN); + } } prev_up_ = up; prev_down_ = dn; prev_left_ = lt; prev_right_ = rt; - prev_a_ = a; - prev_b_ = b; + prev_south_ = south; + prev_east_ = east; + prev_west_ = west; + prev_north_ = north; prev_start_ = start; prev_back_ = back; } diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index ca556dc..9bcadbf 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -3,6 +3,7 @@ #include #include +#include "core/input/key_config.hpp" #include "core/jail/jinput.hpp" #include "core/locale/locale.hpp" #include "core/rendering/overlay.hpp" @@ -26,7 +27,7 @@ namespace GlobalInputs { bool consumed = false; // F1 — Reduir zoom - bool dec_zoom = JI_KeyPressed(Options::keys_gui.dec_zoom); + bool dec_zoom = JI_KeyPressed(KeyConfig::scancode("dec_zoom")); if (dec_zoom && !dec_zoom_prev) { Screen::get()->decZoom(); char msg[32]; @@ -37,7 +38,7 @@ namespace GlobalInputs { dec_zoom_prev = dec_zoom; // F2 — Augmentar zoom - bool inc_zoom = JI_KeyPressed(Options::keys_gui.inc_zoom); + bool inc_zoom = JI_KeyPressed(KeyConfig::scancode("inc_zoom")); if (inc_zoom && !inc_zoom_prev) { Screen::get()->incZoom(); char msg[32]; @@ -48,7 +49,7 @@ namespace GlobalInputs { inc_zoom_prev = inc_zoom; // F3 — Toggle pantalla completa - bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen); + bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen")); if (fullscreen && !fullscreen_prev) { Screen::get()->toggleFullscreen(); Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed")); @@ -57,7 +58,7 @@ namespace GlobalInputs { fullscreen_prev = fullscreen; // F4 — Toggle shaders - bool shader = JI_KeyPressed(Options::keys_gui.toggle_shader); + bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader")); if (shader && !shader_prev) { Screen::get()->toggleShaders(); Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off")); @@ -66,7 +67,7 @@ namespace GlobalInputs { shader_prev = shader; // F5 — Toggle aspect ratio 4:3 - bool aspect = JI_KeyPressed(Options::keys_gui.toggle_aspect_ratio); + bool aspect = JI_KeyPressed(KeyConfig::scancode("toggle_aspect_ratio")); if (aspect && !aspect_prev) { Screen::get()->toggleAspectRatio(); Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square")); @@ -75,7 +76,7 @@ namespace GlobalInputs { aspect_prev = aspect; // F6 — Toggle supersampling - bool ss = JI_KeyPressed(Options::keys_gui.toggle_supersampling); + bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling")); if (ss && !ss_prev) { Screen::get()->toggleSupersampling(); Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off")); @@ -84,7 +85,7 @@ namespace GlobalInputs { ss_prev = ss; // F7 — Canviar tipus de shader (PostFX ↔ CrtPi) - bool next_shader = JI_KeyPressed(Options::keys_gui.next_shader); + bool next_shader = JI_KeyPressed(KeyConfig::scancode("next_shader")); if (next_shader && !next_shader_prev) { Screen::get()->nextShaderType(); char msg[64]; @@ -95,7 +96,7 @@ namespace GlobalInputs { next_shader_prev = next_shader; // F8 — Pròxim preset del shader actiu - bool next_preset = JI_KeyPressed(Options::keys_gui.next_shader_preset); + bool next_preset = JI_KeyPressed(KeyConfig::scancode("next_shader_preset")); if (next_preset && !next_preset_prev) { Screen::get()->nextPreset(); char msg[64]; @@ -106,7 +107,7 @@ namespace GlobalInputs { next_preset_prev = next_preset; // F9 — Toggle filtre d'estirament 4:3 (NEAREST ↔ LINEAR) - bool stretch_filter = JI_KeyPressed(Options::keys_gui.toggle_stretch_filter); + bool stretch_filter = JI_KeyPressed(KeyConfig::scancode("toggle_stretch_filter")); if (stretch_filter && !stretch_filter_prev) { Screen::get()->toggleStretchFilter(); Overlay::showNotification(Options::video.stretch_filter_linear ? Locale::get("notifications.filter_linear") : Locale::get("notifications.filter_nearest")); @@ -115,7 +116,7 @@ namespace GlobalInputs { stretch_filter_prev = stretch_filter; // F10 — Toggle render info (FPS, driver, shader) - bool render_info = JI_KeyPressed(Options::keys_gui.toggle_render_info); + bool render_info = JI_KeyPressed(KeyConfig::scancode("toggle_render_info")); if (render_info && !render_info_prev) { Overlay::toggleRenderInfo(); } diff --git a/source/core/input/key_config.cpp b/source/core/input/key_config.cpp new file mode 100644 index 0000000..db9dae8 --- /dev/null +++ b/source/core/input/key_config.cpp @@ -0,0 +1,182 @@ +#include "core/input/key_config.hpp" + +#include +#include +#include + +#include "core/resources/resource_helper.hpp" +#include "external/fkyaml_node.hpp" + +namespace KeyConfig { + + namespace { + std::vector entries_; + std::unordered_map index_; + std::string overrides_path_; + + auto findIndex(const std::string& id) -> size_t { + auto it = index_.find(id); + if (it == index_.end()) return SIZE_MAX; + return it->second; + } + + void loadDefaults(const std::string& defaults_resource_path) { + auto buf = ResourceHelper::loadFile(defaults_resource_path); + if (buf.empty()) { + std::cerr << "KeyConfig: no s'ha pogut llegir " << defaults_resource_path << '\n'; + return; + } + + std::string content(buf.begin(), buf.end()); + try { + auto yaml = fkyaml::node::deserialize(content); + if (!yaml.contains("keys")) return; + + for (const auto& node : yaml["keys"]) { + KeyEntry entry; + entry.id = node["id"].get_value(); + entry.code = node["code"].get_value(); + if (node.contains("desc")) { + entry.desc = node["desc"].get_value(); + } + SDL_Scancode sc = SDL_GetScancodeFromName(entry.code.c_str()); + if (sc == SDL_SCANCODE_UNKNOWN) { + std::cerr << "KeyConfig: scancode desconegut '" << entry.code + << "' per '" << entry.id << "'\n"; + } + entry.scancode = sc; + entry.default_scancode = sc; + + index_[entry.id] = entries_.size(); + entries_.push_back(std::move(entry)); + } + std::cout << "KeyConfig: " << entries_.size() << " tecles carregades de " + << defaults_resource_path << '\n'; + } catch (const fkyaml::exception& e) { + std::cerr << "KeyConfig: error parsejant YAML: " << e.what() << '\n'; + } + } + + void applyOverrides(const std::string& disk_path) { + std::ifstream file(disk_path); + if (!file.good()) return; + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + try { + auto yaml = fkyaml::node::deserialize(content); + if (!yaml.contains("overrides")) return; + + int applied = 0; + for (const auto& kv : yaml["overrides"].as_map()) { + auto id = kv.first.get_value(); + auto code = kv.second.get_value(); + auto idx = findIndex(id); + if (idx == SIZE_MAX) { + std::cerr << "KeyConfig: override per id desconegut '" << id << "'\n"; + continue; + } + SDL_Scancode sc = SDL_GetScancodeFromName(code.c_str()); + if (sc == SDL_SCANCODE_UNKNOWN) { + std::cerr << "KeyConfig: override amb scancode invàlid '" << code + << "' per '" << id << "'\n"; + continue; + } + entries_[idx].scancode = sc; + entries_[idx].code = code; + applied++; + } + if (applied > 0) { + std::cout << "KeyConfig: aplicats " << applied + << " overrides de " << disk_path << '\n'; + } + } catch (const fkyaml::exception& e) { + std::cerr << "KeyConfig: error parsejant overrides: " << e.what() << '\n'; + } + } + } // namespace + + void init(const std::string& defaults_resource_path, + const std::string& user_overrides_disk_path) { + entries_.clear(); + index_.clear(); + overrides_path_ = user_overrides_disk_path; + + loadDefaults(defaults_resource_path); + if (!overrides_path_.empty()) { + applyOverrides(overrides_path_); + } + } + + void destroy() { + entries_.clear(); + index_.clear(); + overrides_path_.clear(); + } + + auto scancode(const std::string& id) -> SDL_Scancode { + auto idx = findIndex(id); + if (idx == SIZE_MAX) return SDL_SCANCODE_UNKNOWN; + return entries_[idx].scancode; + } + + auto scancodePtr(const std::string& id) -> SDL_Scancode* { + auto idx = findIndex(id); + if (idx == SIZE_MAX) return nullptr; + return &entries_[idx].scancode; + } + + void setScancode(const std::string& id, SDL_Scancode sc) { + auto idx = findIndex(id); + if (idx == SIZE_MAX) return; + entries_[idx].scancode = sc; + const char* name = SDL_GetScancodeName(sc); + entries_[idx].code = (name != nullptr) ? name : ""; + } + + auto isGuiKey(SDL_Scancode sc) -> bool { + if (sc == SDL_SCANCODE_UNKNOWN) return false; + for (const auto& e : entries_) { + if (e.scancode == sc) return true; + } + return false; + } + + auto entries() -> const std::vector& { + return entries_; + } + + auto saveOverrides() -> bool { + if (overrides_path_.empty()) return false; + + // Recull només les entrades remapeades. + std::vector changed; + for (const auto& e : entries_) { + if (e.scancode != e.default_scancode) changed.push_back(&e); + } + + std::ofstream file(overrides_path_); + if (!file.is_open()) { + std::cerr << "KeyConfig: no es pot escriure " << overrides_path_ << '\n'; + return false; + } + + file << "# AEE - Overrides de tecles d'UI\n"; + file << "# Auto-generat. Només llista les tecles modificades respecte\n"; + file << "# els valors per defecte de data/input/keys.yaml.\n"; + file << "\n"; + + if (changed.empty()) { + file << "overrides: {}\n"; + } else { + file << "overrides:\n"; + for (const auto* e : changed) { + file << " " << e->id << ": \"" << e->code << "\"\n"; + } + } + return true; + } + +} // namespace KeyConfig diff --git a/source/core/input/key_config.hpp b/source/core/input/key_config.hpp new file mode 100644 index 0000000..206f78e --- /dev/null +++ b/source/core/input/key_config.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include +#include +#include + +// KeyConfig: font única de veritat per a les tecles d'UI/sistema. +// +// Llegeix els valors per defecte des de `data/input/keys.yaml` (recurs read-only) +// i opcionalment aplica overrides des d'un fitxer de l'usuari (per a remapejos +// fets des del menú de servei). Els callers consulten per `id` (ex. "menu_toggle"). +// +// Les tecles de moviment del jugador NO viuen ací — es queden a Options::keys_game. + +struct KeyEntry { + std::string id; + std::string code; // nom SDL del scancode tal com apareix al YAML + std::string desc; + SDL_Scancode scancode{SDL_SCANCODE_UNKNOWN}; + SDL_Scancode default_scancode{SDL_SCANCODE_UNKNOWN}; +}; + +namespace KeyConfig { + // Inicialitza KeyConfig llegint defaults des d'un recurs (via ResourceHelper) + // i opcionalment sobreposant overrides des d'un fitxer de disc. + void init(const std::string& defaults_resource_path, + const std::string& user_overrides_disk_path); + void destroy(); + + // Consulta el scancode actual associat a un id. Torna SDL_SCANCODE_UNKNOWN si no existix. + [[nodiscard]] auto scancode(const std::string& id) -> SDL_Scancode; + + // Punter estable al scancode d'un id — útil per a Menu::ItemKind::KeyBind. + // Torna nullptr si l'id no existix. + [[nodiscard]] auto scancodePtr(const std::string& id) -> SDL_Scancode*; + + // Estableix el scancode d'un id. No persistix per si sol — cal cridar saveOverrides(). + void setScancode(const std::string& id, SDL_Scancode sc); + + // True si el scancode coincidix amb alguna tecla d'UI registrada. + // Usat pel Director per a evitar que tecles d'UI activen `key_pressed_` al joc. + [[nodiscard]] auto isGuiKey(SDL_Scancode sc) -> bool; + + // Llistat complet de les entrades (per a HELP / debug / iteració). + [[nodiscard]] auto entries() -> const std::vector&; + + // Persistix al fitxer d'overrides les entrades que difereixen del default. + // Si no s'ha proporcionat user_overrides_disk_path al init, és no-op. + auto saveOverrides() -> bool; +} // namespace KeyConfig diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index 5dcc524..662e905 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -6,6 +6,7 @@ #include #include +#include "core/input/key_config.hpp" #include "core/locale/locale.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" @@ -181,7 +182,7 @@ namespace Menu { p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down}); p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left}); p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right}); - p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_gui.menu_toggle}); + p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")}); return p; } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 3e76cb6..9673e6a 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -5,6 +5,7 @@ #include "core/input/gamepad.hpp" #include "core/input/global_inputs.hpp" +#include "core/input/key_config.hpp" #include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" #include "core/jail/jail_audio.hpp" @@ -281,17 +282,18 @@ void Director::handleEvent(const SDL_Event& event) { menu_keys_held_[event.key.scancode] = true; return; } - // Pausa: F11 (o tecla configurada) pausa/reprén la simulació + // Pausa: F11 (o tecla configurada) pausa/reprén la simulació. + // No mostrem notificació — l'indicador persistent "Pausa" a la cantonada + // superior dreta (pintat per Overlay) ja comunica l'estat. if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && - event.key.scancode == Options::keys_gui.pause_toggle) { + event.key.scancode == KeyConfig::scancode("pause_toggle")) { togglePause(); - Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume")); menu_keys_held_[event.key.scancode] = true; return; } // Menú: F12 (o tecla configurada) obre/tanca el menú flotant if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && - event.key.scancode == Options::keys_gui.menu_toggle) { + event.key.scancode == KeyConfig::scancode("menu_toggle")) { Menu::toggle(); JI_SetInputBlocked(Menu::isOpen()); menu_keys_held_[event.key.scancode] = true; @@ -346,19 +348,12 @@ void Director::handleEvent(const SDL_Event& event) { // quan l'overlay faça timeout return; } else { - // Comprova si és una tecla GUI (no passa al joc) + // Comprova si és una tecla d'UI registrada (no passa al joc). + // KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada, + // incloent pause_toggle i menu_toggle (defensa en profunditat: + // aquestes ja s'haurien hagut de menjar al swallow d'amunt). const auto sc = event.key.scancode; - const bool is_gui_key = (sc == Options::keys_gui.dec_zoom || - sc == Options::keys_gui.inc_zoom || - sc == Options::keys_gui.fullscreen || - sc == Options::keys_gui.toggle_shader || - sc == Options::keys_gui.toggle_aspect_ratio || - sc == Options::keys_gui.toggle_supersampling || - sc == Options::keys_gui.next_shader || - sc == Options::keys_gui.next_shader_preset || - sc == Options::keys_gui.toggle_stretch_filter || - sc == Options::keys_gui.toggle_render_info); - if (!is_gui_key) { + if (!KeyConfig::isGuiKey(sc)) { key_pressed_ = true; JI_moveCheats(sc); } diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index 4edbbc5..0312a05 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -2,21 +2,7 @@ #include -// Tecles GUI (capa de presentació — finestra, zoom, shaders, etc.) -namespace Defaults::KeysGUI { - constexpr SDL_Scancode DEC_ZOOM = SDL_SCANCODE_F1; - constexpr SDL_Scancode INC_ZOOM = SDL_SCANCODE_F2; - constexpr SDL_Scancode FULLSCREEN = SDL_SCANCODE_F3; - constexpr SDL_Scancode TOGGLE_SHADER = SDL_SCANCODE_F4; - constexpr SDL_Scancode TOGGLE_ASPECT_RATIO = SDL_SCANCODE_F5; - constexpr SDL_Scancode TOGGLE_SUPERSAMPLING = SDL_SCANCODE_F6; - constexpr SDL_Scancode NEXT_SHADER = SDL_SCANCODE_F7; - constexpr SDL_Scancode NEXT_SHADER_PRESET = SDL_SCANCODE_F8; - constexpr SDL_Scancode TOGGLE_STRETCH_FILTER = SDL_SCANCODE_F9; - constexpr SDL_Scancode TOGGLE_RENDER_INFO = SDL_SCANCODE_F10; - constexpr SDL_Scancode PAUSE_TOGGLE = SDL_SCANCODE_F11; - constexpr SDL_Scancode MENU_TOGGLE = SDL_SCANCODE_F12; -} // namespace Defaults::KeysGUI +// Tecles GUI: viuen a data/input/keys.yaml (font única — KeyConfig). // Tecles de joc (moviment del personatge, accions) namespace Defaults::KeysGame { diff --git a/source/game/modulegame.cpp b/source/game/modulegame.cpp index d2eaed2..c8608ca 100644 --- a/source/game/modulegame.cpp +++ b/source/game/modulegame.cpp @@ -58,7 +58,7 @@ void ModuleGame::onEnter() { // Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de // 32) per cada tick; durant aquesta fase el gameplay no corre, // només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`. - fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif")); + fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif")); phase_ = Phase::FadingIn; } diff --git a/source/game/options.cpp b/source/game/options.cpp index b8fff33..e8eff68 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -189,8 +189,6 @@ namespace Options { loadScancodeField(node, "down", keys_game.down); loadScancodeField(node, "left", keys_game.left); loadScancodeField(node, "right", keys_game.right); - loadScancodeField(node, "menu_toggle", keys_gui.menu_toggle); - loadScancodeField(node, "pause_toggle", keys_gui.pause_toggle); } } @@ -333,15 +331,14 @@ namespace Options { file << " show_title_credits: " << (game.show_title_credits ? "true" : "false") << "\n"; file << "\n"; - // CONTROLS - file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", \"F12\", etc.)\n"; + // CONTROLS — només moviment del jugador. Les tecles d'UI viuen a + // data/input/keys.yaml (defaults) + ~/.config/jailgames/aee/keys.yaml (overrides). + file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", etc.)\n"; file << "controls:\n"; file << " up: \"" << SDL_GetScancodeName(keys_game.up) << "\"\n"; file << " down: \"" << SDL_GetScancodeName(keys_game.down) << "\"\n"; file << " left: \"" << SDL_GetScancodeName(keys_game.left) << "\"\n"; file << " right: \"" << SDL_GetScancodeName(keys_game.right) << "\"\n"; - file << " menu_toggle: \"" << SDL_GetScancodeName(keys_gui.menu_toggle) << "\"\n"; - file << " pause_toggle: \"" << SDL_GetScancodeName(keys_gui.pause_toggle) << "\"\n"; file.close(); diff --git a/source/game/options.hpp b/source/game/options.hpp index eb217f4..a9319fb 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -8,23 +8,8 @@ namespace Options { - // Tecles GUI (finestra, zoom, shaders) - struct KeysGUI { - SDL_Scancode dec_zoom{Defaults::KeysGUI::DEC_ZOOM}; - SDL_Scancode inc_zoom{Defaults::KeysGUI::INC_ZOOM}; - SDL_Scancode fullscreen{Defaults::KeysGUI::FULLSCREEN}; - SDL_Scancode toggle_shader{Defaults::KeysGUI::TOGGLE_SHADER}; - SDL_Scancode toggle_aspect_ratio{Defaults::KeysGUI::TOGGLE_ASPECT_RATIO}; - SDL_Scancode toggle_supersampling{Defaults::KeysGUI::TOGGLE_SUPERSAMPLING}; - SDL_Scancode next_shader{Defaults::KeysGUI::NEXT_SHADER}; - SDL_Scancode next_shader_preset{Defaults::KeysGUI::NEXT_SHADER_PRESET}; - SDL_Scancode toggle_stretch_filter{Defaults::KeysGUI::TOGGLE_STRETCH_FILTER}; - SDL_Scancode toggle_render_info{Defaults::KeysGUI::TOGGLE_RENDER_INFO}; - SDL_Scancode pause_toggle{Defaults::KeysGUI::PAUSE_TOGGLE}; - SDL_Scancode menu_toggle{Defaults::KeysGUI::MENU_TOGGLE}; - }; - - // Tecles de joc (moviment, accions) + // Tecles de joc (moviment, accions). Les tecles d'UI viuen ara a KeyConfig + // (carregades de data/input/keys.yaml). struct KeysGame { SDL_Scancode up{Defaults::KeysGame::UP}; SDL_Scancode down{Defaults::KeysGame::DOWN}; @@ -123,7 +108,6 @@ namespace Options { // --- Variables globals --- inline std::string version{}; - inline KeysGUI keys_gui{}; inline KeysGame keys_game{}; inline Video video{}; inline RenderInfo render_info{}; diff --git a/source/main.cpp b/source/main.cpp index 58ffbae..94f0aef 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -11,6 +11,7 @@ #include #include +#include "core/input/key_config.hpp" #include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jfile.hpp" @@ -57,6 +58,10 @@ SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) { Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml"); Options::loadFromFile(); + // KeyConfig: defaults des de data/input/keys.yaml + overrides de l'usuari + KeyConfig::init("input/keys.yaml", + std::string(file_getconfigfolder()) + "keys.yaml"); + #ifndef NDEBUG // debug.yaml: estat inicial de gameplay per a tests ràpids, // només en builds de debug. @@ -119,11 +124,13 @@ void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) { Director::get()->teardown(); Options::saveToFile(); + KeyConfig::saveOverrides(); #ifndef NDEBUG Options::saveDebugToFile(); #endif Director::destroy(); + KeyConfig::destroy(); Menu::destroy(); Overlay::destroy(); JA_Quit();