diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bcfb99..b02b8cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ set(APP_SOURCES # Game - UI source/game/ui/console.cpp + source/game/ui/console_commands.cpp source/game/ui/notifier.cpp # Utils diff --git a/config/assets.yaml b/config/assets.yaml index 332d74c..169c096 100644 --- a/config/assets.yaml +++ b/config/assets.yaml @@ -117,6 +117,11 @@ assets: required: false absolute: true + # CONSOLE + console: + - type: DATA + path: ${PREFIX}/data/console/commands.yaml + # ROOMS rooms: - type: ROOM diff --git a/data/console/commands.yaml b/data/console/commands.yaml new file mode 100644 index 0000000..93bfb53 --- /dev/null +++ b/data/console/commands.yaml @@ -0,0 +1,229 @@ +# JailDoctor's Dilemma - Console Commands +# Metadata for the in-game console command system. +# Execution logic stays in C++; this file defines metadata only. +# +# Fields: +# keyword - Command name (uppercase) +# handler - C++ handler function identifier +# description - Short description for help output +# usage - Full usage string for terminal help +# instant - (optional) Skip typewriter effect (default: false) +# hidden - (optional) Hide from TAB completion (default: false) +# debug_only - (optional) Only available in debug builds (default: false) +# help_hidden - (optional) Don't show in help output (default: false) +# dynamic_completions - (optional) Completions generated at runtime (default: false) +# completions - (optional) Static TAB completion tree +# debug_extras - (optional) Overrides applied in debug builds + +categories: + - name: VIDEO + commands: + - keyword: SS + handler: cmd_ss + description: Supersampling + usage: "SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]" + completions: + SS: [ON, OFF, SIZE, UPSCALE, DOWNSCALE] + SS UPSCALE: [NEAREST, LINEAR] + SS DOWNSCALE: [BILINEAR, LANCZOS2, LANCZOS3] + + - keyword: SHADER + handler: cmd_shader + description: "Toggle/select shader (F4)" + usage: "SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI]" + completions: + SHADER: [ON, OFF, NEXT, POSTFX, CRTPI] + SHADER NEXT: [PRESET] + + - keyword: BORDER + handler: cmd_border + description: "Decorative border (B)" + usage: "BORDER [ON|OFF]" + completions: + BORDER: [ON, OFF] + + - keyword: FULLSCREEN + handler: cmd_fullscreen + description: "Fullscreen mode (F3)" + usage: "FULLSCREEN [ON|OFF]" + completions: + FULLSCREEN: [ON, OFF] + + - keyword: ZOOM + handler: cmd_zoom + description: "Window zoom (F1/F2)" + usage: "ZOOM [UP|DOWN|<1-N>]" + completions: + ZOOM: [UP, DOWN] + + - keyword: INTSCALE + handler: cmd_intscale + description: "Integer scaling (F7)" + usage: "INTSCALE [ON|OFF]" + completions: + INTSCALE: [ON, OFF] + + - keyword: VSYNC + handler: cmd_vsync + description: "Vertical sync" + usage: "VSYNC [ON|OFF]" + completions: + VSYNC: [ON, OFF] + + - keyword: DRIVER + handler: cmd_driver + description: "GPU driver (restart to apply)" + usage: "DRIVER [LIST|AUTO|NONE|]" + completions: + DRIVER: [LIST, AUTO, NONE] + + - keyword: PALETTE + handler: cmd_palette + description: "Color palette (F5/F6)" + usage: "PALETTE [NEXT|PREV|]" + dynamic_completions: true + + - name: AUDIO + commands: + - keyword: AUDIO + handler: cmd_audio + description: Audio master + usage: "AUDIO [ON|OFF|VOL <0-100>]" + completions: + AUDIO: [ON, OFF, VOL] + + - keyword: MUSIC + handler: cmd_music + description: Music volume + usage: "MUSIC [ON|OFF|VOL <0-100>]" + completions: + MUSIC: [ON, OFF, VOL] + + - keyword: SOUND + handler: cmd_sound + description: Sound volume + usage: "SOUND [ON|OFF|VOL <0-100>]" + completions: + SOUND: [ON, OFF, VOL] + + - name: GAME + commands: + - keyword: SET + handler: cmd_set + description: "Change player skin" + usage: "SET PLAYER SKIN <1|2>" + completions: + SET: [PLAYER] + SET PLAYER: [SKIN] + debug_extras: + description: "Set player/debug options" + usage: "SET PLAYER SKIN <1|2> | SET INITIAL [ROOM|POS|SCENE] | SET ITEMS <0-200>" + completions: + SET: [PLAYER, INITIAL, ITEMS] + SET PLAYER: [SKIN] + SET INITIAL: [ROOM, POS, SCENE] + SET INITIAL SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2] + + - keyword: RESTART + handler: cmd_restart + description: Restart from the beginning + usage: RESTART + instant: true + + - keyword: KIOSK + handler: cmd_kiosk + description: Enable kiosk mode + usage: "KIOSK [ON]" + completions: + KIOSK: [ON] + + - keyword: EXIT + handler: cmd_exit + description: Quit application + usage: EXIT + instant: true + + - keyword: QUIT + handler: cmd_quit + description: Quit application + usage: QUIT + instant: true + help_hidden: true + + - name: INFO + commands: + - keyword: SHOW + handler: cmd_show + description: Show info overlay + usage: "SHOW [INFO]" + completions: + SHOW: [INFO] + debug_extras: + description: "Show overlay/test notification" + usage: "SHOW [INFO|NOTIFICATION|CHEEVO]" + completions: + SHOW: [INFO, NOTIFICATION, CHEEVO] + + - keyword: HIDE + handler: cmd_hide + description: Hide info overlay + usage: "HIDE [INFO]" + completions: + HIDE: [INFO] + + - keyword: SIZE + handler: cmd_size + description: Window size in pixels + usage: SIZE + + - keyword: HELP + handler: cmd_help + description: "Show this help" + usage: "HELP / ?" + + - keyword: "?" + handler: cmd_help + help_hidden: true + + - name: DEBUG + debug_only: true + commands: + - keyword: DEBUG + handler: cmd_debug + description: "Toggle debug overlay (F12)" + usage: "DEBUG [ON|OFF]" + completions: + DEBUG: [ON, OFF] + + - keyword: ROOM + handler: cmd_room + description: "Change to room number (GAME only)" + usage: "ROOM <1-60>|NEXT|PREV" + completions: + ROOM: [NEXT, PREV] + + - keyword: SCENE + handler: cmd_scene + description: Change scene + usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]" + completions: + SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2, RESTART] + + - name: CHEATS + commands: + - keyword: CHEAT + handler: cmd_cheat + description: "Game cheats (GAME only)" + usage: "CHEAT [INFINITE LIVES|INVINCIBILITY|OPEN THE JAIL|CLOSE THE JAIL]" + hidden: true + completions: + CHEAT: [INFINITE, INVINCIBILITY, OPEN, CLOSE] + CHEAT INFINITE: [LIVES] + CHEAT INFINITE LIVES: [ON, OFF] + CHEAT INVINCIBILITY: [ON, OFF] + CHEAT OPEN: [THE] + CHEAT OPEN THE: [JAIL] + CHEAT CLOSE: [THE] + CHEAT CLOSE THE: [JAIL] + debug_extras: + hidden: false diff --git a/source/game/ui/console.cpp b/source/game/ui/console.cpp index d4a1d2e..d00199f 100644 --- a/source/game/ui/console.cpp +++ b/source/game/ui/console.cpp @@ -2,43 +2,21 @@ #include +#include // Para ranges::transform #include // Para toupper -#include // Para function -#include // Para std::cout #include // Para std::istringstream #include // Para string #include // Para vector -#include "core/audio/audio.hpp" // Para Audio -#include "core/locale/locale.hpp" // Para Locale -#include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/sprite/sprite.hpp" // Para Sprite #include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/text.hpp" // Para Text #include "core/resources/resource_cache.hpp" // Para Resource -#include "game/game_control.hpp" // Para GameControl (refresh_player_color) #include "game/options.hpp" // Para Options -#include "game/scene_manager.hpp" // Para SceneManager #include "game/ui/notifier.hpp" // Para Notifier -#ifdef _DEBUG -#include "core/system/debug.hpp" // Para Debug -#endif - -// ── Sistema de comandos ──────────────────────────────────────────────────────── - -// Mapa de completions: {ruta_completa_en_mayúsculas, {opciones}} -// Ej: {"CHEAT OPEN THE", {"JAIL"}} -using CompletionMap = std::vector>>; - -struct ConsoleCommand { - std::string_view keyword; - std::function& args)> execute; - bool instant{false}; // Si true, muestra la respuesta sin efecto typewriter - bool hidden{false}; // Si true, no aparece en el autocompletado (TAB) - CompletionMap completions{}; // Árbol de sub-argumentos para TAB; cargado en el constructor de Console -}; +// ── Helpers de texto ────────────────────────────────────────────────────────── // Convierte la entrada a uppercase y la divide en tokens por espacios static auto parseTokens(const std::string& input) -> std::vector { @@ -60,854 +38,6 @@ static auto parseTokens(const std::string& input) -> std::vector { return tokens; } -// Macro para comando de toggle booleano (evita repetición en ON/OFF) -#define BOOL_TOGGLE_CMD(label, getter, toggle_fn) \ - [](const std::vector& args) -> std::string { \ - if (args.empty()) { \ - (toggle_fn); \ - return label " " + std::string((getter) ? "ON" : "OFF"); \ - } \ - if (args[0] == "ON") { \ - if (getter) { return label " already ON"; } \ - (toggle_fn); \ - return label " ON"; \ - } \ - if (args[0] == "OFF") { \ - if (!(getter)) { return label " already OFF"; } \ - (toggle_fn); \ - return label " OFF"; \ - } \ - return "usage: " label " [on|off]"; \ - } - -// Texto de ayuda común para HELP y ? -static void printHelp() { - std::cout << "=== JDD CONSOLE COMMANDS ===" << '\n'; - - std::cout << '\n'; - std::cout << "[VIDEO]" << '\n'; - std::cout << " SS [ON|OFF|SIZE] Supersampling" << '\n'; - std::cout << " SS UPSCALE [NEAREST|LINEAR] SS upscale filter" << '\n'; - std::cout << " SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] SS downscale algorithm" << '\n'; - std::cout << " SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] Toggle/select shader (F4)" << '\n'; - std::cout << " BORDER [ON|OFF] Decorative border (B)" << '\n'; - std::cout << " FULLSCREEN [ON|OFF] Fullscreen mode (F3)" << '\n'; - std::cout << " ZOOM [UP|DOWN] Window zoom (F1/F2)" << '\n'; - std::cout << " INTSCALE [ON|OFF] Integer scaling (F7)" << '\n'; - std::cout << " VSYNC [ON|OFF] Vertical sync" << '\n'; - std::cout << " DRIVER [LIST|AUTO|NONE|] GPU driver (restart to apply)" << '\n'; - std::cout << " PALETTE [NEXT|PREV] Color palette (F5/F6)" << '\n'; - - std::cout << '\n'; - std::cout << "[AUDIO]" << '\n'; - std::cout << " AUDIO [ON|OFF|VOL <0-100>] Audio master" << '\n'; - std::cout << " MUSIC [ON|OFF|VOL <0-100>] Music volume" << '\n'; - std::cout << " SOUND [ON|OFF|VOL <0-100>] Sound volume" << '\n'; - - std::cout << '\n'; - std::cout << "[GAME]" << '\n'; - std::cout << " SET PLAYER SKIN <1|2> Change player skin (GAME only)" << '\n'; - std::cout << " RESTART Restart from the beginning" << '\n'; - std::cout << " KIOSK [ON] Enable kiosk mode" << '\n'; - std::cout << " EXIT / QUIT Quit application" << '\n'; - - std::cout << '\n'; - std::cout << "[INFO]" << '\n'; - std::cout << " SHOW [INFO] Show info overlay" << '\n'; - std::cout << " HIDE [INFO] Hide info overlay" << '\n'; - std::cout << " SIZE Window size in pixels" << '\n'; - std::cout << " HELP / ? Show this help in terminal" << '\n'; - -#ifdef _DEBUG - std::cout << '\n'; - std::cout << "[DEBUG]" << '\n'; - std::cout << " DEBUG Toggle debug overlay (F12)" << '\n'; - std::cout << " ROOM <1-60>|NEXT|PREV Change to room number (GAME only)" << '\n'; - std::cout << " SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]" << '\n'; - std::cout << " SET INITIAL [ROOM|POS] Set initial room/position from current state (GAME only)" << '\n'; - std::cout << " SET INITIAL SCENE [] Set initial debug scene (GAME|LOGO|TITLE|LOADING|CREDITS|ENDING|ENDING2)" << '\n'; - std::cout << " SET ITEMS <0-200> Set collected items count (GAME only)" << '\n'; - std::cout << " CHEAT INFINITE LIVES [ON|OFF] Infinite lives (GAME only)" << '\n'; - std::cout << " CHEAT INVINCIBILITY [ON|OFF] Invincibility (GAME only)" << '\n'; - std::cout << " CHEAT OPEN THE JAIL Open the jail (GAME only)" << '\n'; - std::cout << " CHEAT CLOSE THE JAIL Close the jail (GAME only)" << '\n'; - std::cout << " SHOW NOTIFICATION Test notification popup" << '\n'; - std::cout << " SHOW CHEEVO Test achievement notification" << '\n'; -#endif -} - -// En Release, los comandos de truco (CHEAT) son ocultos en el autocompletado -#ifdef _DEBUG -static constexpr bool CHEAT_HIDDEN = false; -#else -static constexpr bool CHEAT_HIDDEN = true; -#endif - -// Tabla de comandos disponibles -static const std::vector COMMANDS = { - // SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] — Supersampling - {.keyword = "SS", .execute = [](const std::vector& args) -> std::string { - if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } - static const std::array DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"}; - if (!args.empty() && args[0] == "SIZE") { - if (!Options::video.supersampling.enabled) { return "Supersampling is OFF: no texture"; } - const auto [w, h] = Screen::get()->getSsTextureSize(); - if (w == 0) { return "SS texture: not active"; } - return "SS texture: " + std::to_string(w) + "x" + std::to_string(h); - } - if (!args.empty() && args[0] == "UPSCALE") { - if (args.size() == 1) { - Screen::get()->setLinearUpscale(!Options::video.supersampling.linear_upscale); - return std::string("Upscale: ") + (Options::video.supersampling.linear_upscale ? "Linear" : "Nearest"); - } - if (args[1] == "NEAREST") { - if (!Options::video.supersampling.linear_upscale) { return "Upscale already Nearest"; } - Screen::get()->setLinearUpscale(false); - return "Upscale: Nearest"; - } - if (args[1] == "LINEAR") { - if (Options::video.supersampling.linear_upscale) { return "Upscale already Linear"; } - Screen::get()->setLinearUpscale(true); - return "Upscale: Linear"; - } - return "usage: ss upscale [nearest|linear]"; - } - if (!args.empty() && args[0] == "DOWNSCALE") { - if (args.size() == 1) { - return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(Options::video.supersampling.downscale_algo)]); - } - int algo = -1; - if (args[1] == "BILINEAR") { algo = 0; } - if (args[1] == "LANCZOS2") { algo = 1; } - if (args[1] == "LANCZOS3") { algo = 2; } - if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; } - if (Options::video.supersampling.downscale_algo == algo) { - return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); - } - Screen::get()->setDownscaleAlgo(algo); - return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); - } - if (args.empty()) { - Screen::get()->toggleSupersampling(); - return std::string("PostFX Supersampling ") + (Options::video.supersampling.enabled ? "ON" : "OFF"); - } - if (args[0] == "ON") { - if (Options::video.supersampling.enabled) { return "Supersampling already ON"; } - Screen::get()->toggleSupersampling(); - return "PostFX Supersampling ON"; - } - if (args[0] == "OFF") { - if (!Options::video.supersampling.enabled) { return "Supersampling already OFF"; } - Screen::get()->toggleSupersampling(); - return "PostFX Supersampling OFF"; - } - return "usage: ss [on|off|size|upscale [nearest|linear]|downscale [bilinear|lanczos2|lanczos3]]"; - }, - .completions = { - {"SS", {"ON", "OFF", "SIZE", "UPSCALE", "DOWNSCALE"}}, - {"SS UPSCALE", {"NEAREST", "LINEAR"}}, - {"SS DOWNSCALE", {"BILINEAR", "LANCZOS2", "LANCZOS3"}}, - }}, - - // SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] — Toggle/cicla/selecciona shader (F4 / Shift+F4) - {.keyword = "SHADER", .execute = [](const std::vector& args) -> std::string { - if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } - if (args.empty()) { - Screen::get()->toggleShaders(); - return std::string("Shader ") + (Options::video.shader.enabled ? "ON" : "OFF"); - } - if (args[0] == "ON") { - if (Options::video.shader.enabled) { return "Shader already ON"; } - Screen::get()->toggleShaders(); - return "Shader ON"; - } - if (args[0] == "OFF") { - if (!Options::video.shader.enabled) { return "Shader already OFF"; } - Screen::get()->toggleShaders(); - return "Shader OFF"; - } - if (args[0] == "POSTFX") { - Screen::get()->setActiveShader(Rendering::ShaderType::POSTFX); - return "Shader: PostFX"; - } - if (args[0] == "CRTPI") { - Screen::get()->setActiveShader(Rendering::ShaderType::CRTPI); - return "Shader: CrtPi"; - } - if (args[0] == "NEXT") { - // SHADER NEXT PRESET → cicla presets del shader activo - if (args.size() >= 2 && args[1] == "PRESET") { - if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) { - if (Options::crtpi_presets.empty()) { return "No CrtPi presets available"; } - Options::video.shader.current_crtpi_preset = - (Options::video.shader.current_crtpi_preset + 1) % - static_cast(Options::crtpi_presets.size()); - Screen::get()->reloadCrtPi(); - return "CrtPi preset: " + - Options::crtpi_presets[static_cast(Options::video.shader.current_crtpi_preset)].name; - } - if (Options::postfx_presets.empty()) { return "No PostFX presets available"; } - Options::video.shader.current_postfx_preset = - (Options::video.shader.current_postfx_preset + 1) % - static_cast(Options::postfx_presets.size()); - Screen::get()->reloadPostFX(); - return "PostFX preset: " + - Options::postfx_presets[static_cast(Options::video.shader.current_postfx_preset)].name; - } - // SHADER NEXT → cicla entre tipos de shader (PostFX ↔ CrtPi) - Screen::get()->nextShader(); - return std::string("Shader: ") + - (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CrtPi" : "PostFX"); - } - return "usage: shader [on|off|next [preset]|postfx|crtpi]"; - }, - .completions = { - {"SHADER", {"ON", "OFF", "NEXT", "POSTFX", "CRTPI"}}, - {"SHADER NEXT", {"PRESET"}}, - }}, - - // BORDER [ON|OFF] — Borde decorativo (B) - {.keyword = "BORDER", .execute = BOOL_TOGGLE_CMD("Border", Options::video.border.enabled, Screen::get()->toggleBorder()), .completions = {{"BORDER", {"ON", "OFF"}}}}, - - // FULLSCREEN [ON|OFF [PLEASE]] — Pantalla completa (F3); OFF bloqueado en kiosk sin PLEASE - {.keyword = "FULLSCREEN", .execute = [](const std::vector& args) -> std::string { - const bool EXPLICIT_ON = !args.empty() && args[0] == "ON"; - const bool EXPLICIT_OFF = !args.empty() && args[0] == "OFF"; - const bool WITH_PLEASE = !args.empty() && args.back() == "PLEASE"; - const bool IS_TOGGLE = args.empty(); - const bool TURNING_OFF = EXPLICIT_OFF || (IS_TOGGLE && Options::video.fullscreen); - - if (TURNING_OFF && Options::kiosk.enabled && !WITH_PLEASE) { - return "Not allowed in kiosk mode"; - } - if (EXPLICIT_ON) { - if (Options::video.fullscreen) { return "Fullscreen already ON"; } - Screen::get()->toggleVideoMode(); - return "Fullscreen ON"; - } - if (EXPLICIT_OFF) { - if (!Options::video.fullscreen) { return "Fullscreen already OFF"; } - Screen::get()->toggleVideoMode(); - return "Fullscreen OFF"; - } - if (IS_TOGGLE) { - Screen::get()->toggleVideoMode(); - return std::string("Fullscreen ") + (Options::video.fullscreen ? "ON" : "OFF"); - } - return "usage: fullscreen [on|off]"; - }, - .completions = {{"FULLSCREEN", {"ON", "OFF"}}}}, - - // ZOOM UP/DOWN — Zoom de ventana (F1/F2) - {.keyword = "ZOOM", .execute = [](const std::vector& args) -> std::string { - if (args.empty()) { return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]"; } - if (args[0] == "UP") { - if (!Screen::get()->incWindowZoom()) { return "Max zoom reached"; } - return "Zoom " + std::to_string(Options::window.zoom); - } - if (args[0] == "DOWN") { - if (!Screen::get()->decWindowZoom()) { return "Min zoom reached"; } - return "Zoom " + std::to_string(Options::window.zoom); - } - // Zoom numérico directo - try { - const int N = std::stoi(args[0]); - const int MAX = Screen::get()->getMaxZoom(); - if (N < 1 || N > MAX) { - return "Zoom must be between 1 and " + std::to_string(MAX); - } - if (N == Options::window.zoom) { return "Zoom already " + std::to_string(N); } - Screen::get()->setWindowZoom(N); - return "Zoom " + std::to_string(Options::window.zoom); - } catch (...) {} - return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]"; - }, - .completions = {{"ZOOM", {"UP", "DOWN"}}}}, - - // INTSCALE [ON|OFF] — Escalado entero (F7) - {.keyword = "INTSCALE", .execute = [](const std::vector& args) -> std::string { - const bool ON = args.empty() ? !Options::video.integer_scale - : (args[0] == "ON"); - if (!args.empty() && args[0] != "ON" && args[0] != "OFF") { - return "usage: intscale [on|off]"; - } - if (ON == Options::video.integer_scale) { - return std::string("IntScale already ") + (ON ? "ON" : "OFF"); - } - Screen::get()->toggleIntegerScale(); - Screen::get()->setVideoMode(Options::video.fullscreen); - return std::string("IntScale ") + (Options::video.integer_scale ? "ON" : "OFF"); - }, - .completions = {{"INTSCALE", {"ON", "OFF"}}}}, - - // VSYNC [ON|OFF] — Sincronización vertical - {.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync()), .completions = {{"VSYNC", {"ON", "OFF"}}}}, - - // DRIVER [LIST|AUTO|] — Driver GPU (aplica en el próximo arranque) - {.keyword = "DRIVER", .execute = [](const std::vector& args) -> std::string { - // Sin argumentos: muestra el driver activo (permitido en kiosk) - if (args.empty()) { - const auto& driver = Screen::get()->getGPUDriver(); - return "GPU: " + (driver.empty() ? std::string("sdl") : driver); - } - // LIST: lista drivers disponibles marcando el activo con * (permitido en kiosk) - if (args[0] == "LIST") { - const int COUNT = SDL_GetNumGPUDrivers(); - if (COUNT <= 0) { return "No GPU drivers found"; } - const std::string& active = Screen::get()->getGPUDriver(); - std::string result = "Drivers:"; - for (int i = 0; i < COUNT; ++i) { - const char* name = SDL_GetGPUDriver(i); - if (name != nullptr) { - result += ' '; - result += name; - if (active == name) { result += '*'; } - } - } - SDL_Log("SDL GPU drivers: %s", result.c_str()); - return result; - } - // Cambiar driver: bloqueado en kiosk salvo PLEASE - const bool HAS_PLEASE = std::ranges::find(args, std::string("PLEASE")) != args.end(); - if (Options::kiosk.enabled && !HAS_PLEASE) { - return "Not allowed in kiosk mode"; - } - if (args[0] == "AUTO") { - Options::video.gpu.preferred_driver.clear(); - Options::saveToFile(); - return "Driver: auto (restart)"; - } - if (args[0] == "NONE") { - Options::video.gpu.preferred_driver = "none"; - Options::saveToFile(); - return "Driver: none (SDL fallback, restart)"; - } - std::string driver_lower = args[0]; - std::ranges::transform(driver_lower, driver_lower.begin(), ::tolower); - // Validar que el nombre existe en la lista de drivers SDL - const int COUNT = SDL_GetNumGPUDrivers(); - bool found = false; - for (int i = 0; i < COUNT && !found; ++i) { - const char* name = SDL_GetGPUDriver(i); - if (name != nullptr && driver_lower == name) { found = true; } - } - if (!found) { - return "Unknown driver: " + driver_lower + ". Use DRIVER LIST or NONE"; - } - Options::video.gpu.preferred_driver = driver_lower; - Options::saveToFile(); - return "Driver: " + driver_lower + " (restart)"; - }, - .completions = {{"DRIVER", {"LIST", "AUTO", "NONE"}}}}, - - // PALETTE NEXT/PREV/ — Paleta de colores (F5/F6 o por nombre) - {.keyword = "PALETTE", .execute = [](const std::vector& args) -> std::string { - const auto palName = []() -> std::string { - return Screen::get()->getPalettePrettyName(); - }; - if (args.empty()) { return "usage: palette [next|prev|]"; } - if (args[0] == "NEXT") { - Screen::get()->nextPalette(); - return "Palette: " + palName(); - } - if (args[0] == "PREV") { - Screen::get()->previousPalette(); - return "Palette: " + palName(); - } - if (!Screen::get()->setPaletteByName(args[0])) { - std::string arg_lower = args[0]; - std::ranges::transform(arg_lower, arg_lower.begin(), ::tolower); - return "Unknown palette: " + arg_lower; - } - return "Palette: " + palName(); - }}, - -#ifdef _DEBUG - // DEBUG [ON|OFF] — Activa/desactiva el modo debug del juego (tecla 0); solo en escena GAME - {.keyword = "DEBUG", .execute = [](const std::vector& args) -> std::string { - if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } - if (!GameControl::toggle_debug_mode) { return "Game not initialized"; } - const bool ENABLED = Debug::get()->isEnabled(); - if (!args.empty() && args[0] == "ON") { - if (ENABLED) { return "Debug mode already ON"; } - GameControl::toggle_debug_mode(); - return "Debug mode ON"; - } - if (!args.empty() && args[0] == "OFF") { - if (!ENABLED) { return "Debug mode already OFF"; } - GameControl::toggle_debug_mode(); - return "Debug mode OFF"; - } - if (!args.empty()) { return "usage: debug [on|off]"; } - GameControl::toggle_debug_mode(); - return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF"); - }, - .completions = {{"DEBUG", {"ON", "OFF"}}}}, - - // ROOM |NEXT|PREV — Cambia a la habitación indicada (1-60); solo en escena GAME - {.keyword = "ROOM", .execute = [](const std::vector& args) -> std::string { - if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } - if (args.empty()) { return "usage: room <1-60>|next|prev"; } - int num = 0; - if (args[0] == "NEXT" || args[0] == "PREV") { - if (!GameControl::get_current_room) { return "Game not initialized"; } - const std::string current = GameControl::get_current_room(); - try { - num = std::stoi(current.substr(0, current.find('.'))); - } catch (...) { return "Cannot determine current room"; } - num += (args[0] == "NEXT") ? 1 : -1; - } else { - try { - num = std::stoi(args[0]); - } catch (...) { return "usage: room <1-60>|next|prev"; } - } - if (num < 1 || num > 60) { return "Room must be between 1 and 60"; } - char buf[16]; - std::snprintf(buf, sizeof(buf), "%02d.yaml", num); - if (GameControl::change_room && GameControl::change_room(buf)) { - return std::string("Room: ") + buf; - } - return std::string("Room not found: ") + buf; - }, - .completions = {{"ROOM", {"NEXT", "PREV"}}}}, - -#endif - - // SHOW INFO — disponible en Release; SHOW NOTIFICATION / SHOW CHEEVO — solo en Debug - {.keyword = "SHOW", .execute = [](const std::vector& args) -> std::string { -#ifdef _DEBUG - if (!args.empty() && args[0] == "NOTIFICATION") { - Notifier::get()->show({"NOTIFICATION"}); - return "Notification shown"; - } - if (!args.empty() && args[0] == "CHEEVO") { - Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c1")}, Notifier::Style::CHEEVO, -1, false); // NOLINT(readability-static-accessed-through-instance) - return "Cheevo notification shown"; - } - if (args.empty() || args[0] != "INFO") { return "usage: show [info|notification|cheevo]"; } -#else - if (args.empty() || args[0] != "INFO") { return "usage: show [info]"; } -#endif - if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; } - RenderInfo::get()->toggle(); - return "Info overlay ON"; - }, - .completions = { -#ifdef _DEBUG - {"SHOW", {"INFO", "NOTIFICATION", "CHEEVO"}}, -#else - {"SHOW", {"INFO"}}, -#endif - }}, - - // HIDE INFO — disponible en Release - {.keyword = "HIDE", .execute = [](const std::vector& args) -> std::string { - if (args.empty() || args[0] != "INFO") { return "usage: hide [info]"; } - if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; } - RenderInfo::get()->toggle(); - return "Info overlay OFF"; - }, - .completions = {{"HIDE", {"INFO"}}}}, - - // CHEAT — Trucos de juego; solo en escena GAME; no aparece en ayuda en builds Release - {.keyword = "CHEAT", .execute = [](const std::vector& args) -> std::string { - if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } - if (args.empty()) { return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; } - - // CHEAT INFINITE LIVES [ON|OFF] - if (args[0] == "INFINITE") { - if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; } - auto& cheat = Options::cheats.infinite_lives; - using State = Options::Cheat::State; - const std::vector REST(args.begin() + 2, args.end()); - if (REST.empty()) { - cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; - } else if (REST[0] == "ON") { - if (cheat == State::ENABLED) { return "Infinite lives already ON"; } - cheat = State::ENABLED; - } else if (REST[0] == "OFF") { - if (cheat == State::DISABLED) { return "Infinite lives already OFF"; } - cheat = State::DISABLED; - } else { - return "usage: cheat infinite lives [on|off]"; - } - if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } - return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF"); - } - - // CHEAT INVINCIBILITY [ON|OFF] - if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") { - auto& cheat = Options::cheats.invincible; - using State = Options::Cheat::State; - if (args.size() == 1) { - cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; - } else if (args[1] == "ON") { - if (cheat == State::ENABLED) { return "Invincibility already ON"; } - cheat = State::ENABLED; - } else if (args[1] == "OFF") { - if (cheat == State::DISABLED) { return "Invincibility already OFF"; } - cheat = State::DISABLED; - } else { - return "usage: cheat invincibility [on|off]"; - } - if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } - return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF"); - } - - // CHEAT OPEN THE JAIL - if (args[0] == "OPEN") { - if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat open the jail"; } - if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; } - Options::cheats.jail_is_open = Options::Cheat::State::ENABLED; - return "Jail opened"; - } - - // CHEAT CLOSE THE JAIL - if (args[0] == "CLOSE") { - if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat close the jail"; } - if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; } - Options::cheats.jail_is_open = Options::Cheat::State::DISABLED; - return "Jail closed"; - } - - return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; - }, - .hidden = CHEAT_HIDDEN, - .completions = { - {"CHEAT", {"INFINITE", "INVINCIBILITY", "OPEN", "CLOSE"}}, - {"CHEAT INFINITE", {"LIVES"}}, - {"CHEAT INFINITE LIVES", {"ON", "OFF"}}, - {"CHEAT INVINCIBILITY", {"ON", "OFF"}}, - {"CHEAT OPEN", {"THE"}}, - {"CHEAT OPEN THE", {"JAIL"}}, - {"CHEAT CLOSE", {"THE"}}, - {"CHEAT CLOSE THE", {"JAIL"}}, - }}, - - // SET PLAYER SKIN <1|2> — Cambia la skin del jugador (disponible en todos los builds, GAME) - // SET INITIAL [ROOM|POS] — Guarda habitación/posición actual como inicio (solo _DEBUG, GAME) - // SET INITIAL SCENE [] — Guarda escena como escena inicial de debug (solo _DEBUG) - // SET ITEMS <0-200> — Fija el contador de items recogidos (solo _DEBUG, GAME) - {.keyword = "SET", .execute = [](const std::vector& args) -> std::string { - if (args.size() >= 3 && args[0] == "PLAYER" && args[1] == "SKIN") { - if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } - int num = 0; - try { - num = std::stoi(args[2]); - } catch (...) {} - if (num < 1 || num > 2) { return "usage: set player skin <1|2>"; } - if (!GameControl::change_player_skin) { return "Game not initialized"; } - GameControl::change_player_skin(num); - return "Player skin: " + std::to_string(num); - } -#ifdef _DEBUG - // SET INITIAL SCENE [] — disponible desde cualquier escena - if (args.size() >= 2 && args[0] == "INITIAL" && args[1] == "SCENE") { - SceneManager::Scene target = SceneManager::current; - std::string name = "current"; - if (args.size() >= 3) { - if (args[2] == "GAME") { - target = SceneManager::Scene::GAME; - name = "game"; - } else if (args[2] == "LOGO") { - target = SceneManager::Scene::LOGO; - name = "logo"; - } else if (args[2] == "LOADING") { - target = SceneManager::Scene::LOADING_SCREEN; - name = "loading"; - } else if (args[2] == "TITLE") { - target = SceneManager::Scene::TITLE; - name = "title"; - } else if (args[2] == "CREDITS") { - target = SceneManager::Scene::CREDITS; - name = "credits"; - } else if (args[2] == "ENDING") { - target = SceneManager::Scene::ENDING; - name = "ending"; - } else if (args[2] == "ENDING2") { - target = SceneManager::Scene::ENDING2; - name = "ending2"; - } else { - std::string scene_lower = args[2]; - std::ranges::transform(scene_lower, scene_lower.begin(), ::tolower); - return "Unknown scene: " + scene_lower; - } - } - Debug::get()->setInitialScene(target); - Debug::get()->saveToFile(); - return "Initial scene set to: " + name; - } - - if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } - - // SET ITEMS <0-200> — Fija el contador de items recogidos - if (args[0] == "ITEMS") { - if (args.size() < 2) { return "usage: set items <0-200>"; } - int count = 0; - try { - count = std::stoi(args[1]); - } catch (...) { return "usage: set items <0-200>"; } - if (count < 0 || count > 200) { return "Items must be between 0 and 200"; } - if (!GameControl::set_items) { return "Game not initialized"; } - GameControl::set_items(count); - return "Items: " + std::to_string(count); - } - - if (args.empty() || args[0] != "INITIAL") { return "usage: set initial [room|pos|scene] | set items <0-200> | set player skin <1|2>"; } - - const bool DO_ROOM = args.size() == 1 || (args.size() >= 2 && args[1] == "ROOM"); - const bool DO_POS = args.size() == 1 || (args.size() >= 2 && args[1] == "POS"); - - if (!DO_ROOM && !DO_POS) { return "usage: set initial [room|pos|scene]"; } - if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; } - - std::string result; - if (DO_ROOM) { result = GameControl::set_initial_room(); } - if (DO_POS) { - if (!result.empty()) { result += ", "; } - result += GameControl::set_initial_pos(); - } - return result; -#else - return "usage: set player skin <1|2>"; -#endif - }, - .completions = { -#ifdef _DEBUG - {"SET", {"PLAYER", "INITIAL", "ITEMS"}}, - {"SET PLAYER", {"SKIN"}}, - {"SET INITIAL", {"ROOM", "POS", "SCENE"}}, - {"SET INITIAL SCENE", {"LOGO", "LOADING", "TITLE", "CREDITS", "GAME", "ENDING", "ENDING2"}}, -#else - {"SET", {"PLAYER"}}, - {"SET PLAYER", {"SKIN"}}, -#endif - }}, - -#ifdef _DEBUG - // SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena; solo en Debug - {.keyword = "SCENE", .execute = [](const std::vector& args) -> std::string { - if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; } - if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|ending|ending2|restart]"; } - - // RESTART: reinicia la escena actual (funciona desde cualquier escena) - if (args[0] == "RESTART") { - SceneManager::scene_before_restart = SceneManager::current; - SceneManager::current = SceneManager::Scene::RESTART_CURRENT; - return "Restarting..."; - } - - // Para el resto: si pedimos la escena que ya está activa → también reiniciar - const auto GO_TO = [](SceneManager::Scene target, const std::string& label) -> std::string { - if (SceneManager::current == target) { - SceneManager::scene_before_restart = target; - SceneManager::current = SceneManager::Scene::RESTART_CURRENT; - } else { - SceneManager::current = target; - } - return "Scene: " + label; - }; - - if (args[0] == "LOGO") { return GO_TO(SceneManager::Scene::LOGO, "Logo"); } - if (args[0] == "LOADING") { return GO_TO(SceneManager::Scene::LOADING_SCREEN, "Loading"); } - if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); } - if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); } - if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); } - if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); } - if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); } - return "Unknown scene: " + args[0]; - }, - .completions = {{"SCENE", {"LOGO", "LOADING", "TITLE", "CREDITS", "GAME", "ENDING", "ENDING2", "RESTART"}}}}, -#endif - - // RESTART — Reiniciar desde el principio (equivale a SCENE LOGO) - {.keyword = "RESTART", .execute = [](const std::vector&) -> std::string { - SceneManager::current = SceneManager::Scene::LOGO; - Audio::get()->stopMusic(); - return "Restarting..."; - }, - .instant = true}, - - // KIOSK [ON|OFF PLEASE|PLEASE] — Modo kiosko - {.keyword = "KIOSK", .execute = [](const std::vector& args) -> std::string { - const bool DISABLE = (!args.empty() && args[0] == "PLEASE") || - (args.size() >= 2 && args[0] == "OFF" && args[1] == "PLEASE"); - if (DISABLE) { - Options::kiosk.enabled = false; - return "Kiosk mode OFF"; - } - if (!args.empty() && args[0] == "OFF") { - return "Not allowed in kiosk mode"; - } - if (args.empty() || args[0] == "ON") { - if (Options::kiosk.enabled) { return "Kiosk mode already ON"; } - Options::kiosk.enabled = true; - if (!Options::video.fullscreen) { Screen::get()->toggleVideoMode(); } - return "Kiosk mode ON"; - } - return "usage: kiosk [on]"; - }, - .completions = {{"KIOSK", {"ON"}}}}, - - // AUDIO [ON|OFF|VOL <0-100>] — Audio maestro (estado + volumen) - {.keyword = "AUDIO", .execute = [](const std::vector& args) -> std::string { - if (args.empty()) { - const int VOL = static_cast(Options::audio.volume * 100.0F); - return std::string("Audio ") + (Options::audio.enabled ? "ON" : "OFF") + - " vol:" + std::to_string(VOL); - } - if (args[0] == "ON") { - if (Options::audio.enabled) { return "Audio already ON"; } - Options::audio.enabled = true; - Audio::get()->enable(true); - return "Audio ON"; - } - if (args[0] == "OFF") { - if (!Options::audio.enabled) { return "Audio already OFF"; } - Options::audio.enabled = false; - Audio::get()->enable(false); - return "Audio OFF"; - } - if (args[0] == "VOL" && args.size() >= 2) { - try { - const int VAL = std::stoi(args[1]); - if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } - Options::audio.volume = static_cast(VAL) / 100.0F; - Audio::get()->enable(Options::audio.enabled); - return "Audio vol:" + std::to_string(VAL); - } catch (...) { return "usage: audio vol <0-100>"; } - } - return "usage: audio [on|off|vol n]"; - }, - .completions = {{"AUDIO", {"ON", "OFF", "VOL"}}}}, - - // MUSIC [ON|OFF|VOL <0-100>] — Volumen e interruptor de música - {.keyword = "MUSIC", .execute = [](const std::vector& args) -> std::string { - if (args.empty()) { - const int VOL = static_cast(Options::audio.music.volume * 100.0F); - return std::string("Music ") + (Options::audio.music.enabled ? "ON" : "OFF") + - " vol:" + std::to_string(VOL); - } - if (args[0] == "ON") { - if (Options::audio.music.enabled) { return "Music already ON"; } - Options::audio.music.enabled = true; - Audio::get()->enableMusic(true); - Audio::get()->setMusicVolume(Options::audio.music.volume); - return "Music ON"; - } - if (args[0] == "OFF") { - if (!Options::audio.music.enabled) { return "Music already OFF"; } - Audio::get()->setMusicVolume(0.0F); - Audio::get()->enableMusic(false); - Options::audio.music.enabled = false; - return "Music OFF"; - } - if (args[0] == "VOL" && args.size() >= 2) { - try { - const int VAL = std::stoi(args[1]); - if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } - Options::audio.music.volume = static_cast(VAL) / 100.0F; - if (Options::audio.music.enabled) { - Audio::get()->setMusicVolume(Options::audio.music.volume); - } - return "Music vol:" + std::to_string(VAL); - } catch (...) { return "usage: music vol <0-100>"; } - } - return "usage: music [on|off|vol n]"; - }, - .completions = {{"MUSIC", {"ON", "OFF", "VOL"}}}}, - - // SOUND [ON|OFF|VOL <0-100>] — Volumen e interruptor de efectos de sonido - {.keyword = "SOUND", .execute = [](const std::vector& args) -> std::string { - if (args.empty()) { - const int VOL = static_cast(Options::audio.sound.volume * 100.0F); - return std::string("Sound ") + (Options::audio.sound.enabled ? "ON" : "OFF") + - " vol:" + std::to_string(VOL); - } - if (args[0] == "ON") { - if (Options::audio.sound.enabled) { return "Sound already ON"; } - Options::audio.sound.enabled = true; - Audio::get()->enableSound(true); - Audio::get()->setSoundVolume(Options::audio.sound.volume); - return "Sound ON"; - } - if (args[0] == "OFF") { - if (!Options::audio.sound.enabled) { return "Sound already OFF"; } - Audio::get()->setSoundVolume(0.0F); - Audio::get()->enableSound(false); - Options::audio.sound.enabled = false; - return "Sound OFF"; - } - if (args[0] == "VOL" && args.size() >= 2) { - try { - const int VAL = std::stoi(args[1]); - if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } - Options::audio.sound.volume = static_cast(VAL) / 100.0F; - if (Options::audio.sound.enabled) { - Audio::get()->setSoundVolume(Options::audio.sound.volume); - } - return "Sound vol:" + std::to_string(VAL); - } catch (...) { return "usage: sound vol <0-100>"; } - } - return "usage: sound [on|off|vol n]"; - }, - .completions = {{"SOUND", {"ON", "OFF", "VOL"}}}}, - - // EXIT / QUIT — Cerrar la aplicacion (bloqueado en kiosk) - {.keyword = "EXIT", .execute = [](const std::vector& args) -> std::string { - if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { - return "Not allowed in kiosk mode"; - } - SceneManager::current = SceneManager::Scene::QUIT; - return "Quitting..."; - }, - .instant = true}, - {.keyword = "QUIT", .execute = [](const std::vector& args) -> std::string { - if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { - return "Not allowed in kiosk mode"; - } - SceneManager::current = SceneManager::Scene::QUIT; - return "Quitting..."; - }, - .instant = true}, - - // SIZE — Devuelve el tamaño actual de la ventana en píxeles - {.keyword = "SIZE", .execute = [](const std::vector&) -> std::string { - int w = 0; - int h = 0; - SDL_GetWindowSize(SDL_GetRenderWindow(Screen::get()->getRenderer()), &w, &h); - return std::to_string(w) + "x" + std::to_string(h); - }}, - - // HELP / ? — Muestra ayuda en la terminal del sistema y lista de comandos en consola - {.keyword = "HELP", .execute = [](const std::vector&) -> std::string { - printHelp(); - std::string result = - "Commands:\n" - "fullscreen, zoom, intscale, vsync, driver, palette, audio, music, sound, set, restart, kiosk, exit, quit, show, hide, size, help\n"; -#ifdef _DEBUG - result += - "\nDebug commands:\n" - "debug, room, scene, cheat\n"; -#endif - result += "-- more info on the terminal"; - return result; - }}, - {.keyword = "?", .execute = [](const std::vector&) -> std::string { - printHelp(); - std::string result = - "Commands:\n" - "fullscreen, zoom, intscale, vsync, driver, palette, audio, music, sound, set, restart, kiosk, exit, quit, show, hide, size, help\n"; -#ifdef _DEBUG - result += - "\nDebug commands:\n" - "debug, room, scene, cheat\n"; -#endif - result += "-- more info on the terminal"; - return result; - }}, -}; - // ── Helpers ─────────────────────────────────────────────────────────────────── // Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input) @@ -979,13 +109,8 @@ Console::Console(const std::string& font_name) target_height_ = height_; y_ = -height_; - // Construir mapa de autocompletado a partir de COMMANDS - for (const auto& cmd : COMMANDS) { - for (const auto& [path, opts] : cmd.completions) { - auto& vec = tab_completions_[std::string(path)]; - for (const auto& opt : opts) { vec.emplace_back(opt); } - } - } + // Cargar comandos desde YAML + registry_.load("data/console/commands.yaml"); buildSurface(); } @@ -1225,17 +350,16 @@ void Console::handleEvent(const SDL_Event& event) { const size_t space_pos = upper.rfind(' '); if (space_pos == std::string::npos) { - // Modo comando: ciclar keywords que empiecen por el prefijo - for (const auto& cmd : COMMANDS) { - if (cmd.hidden) { continue; } - if (upper.empty() || cmd.keyword.starts_with(upper)) { - tab_matches_.emplace_back(cmd.keyword); + // Modo comando: ciclar keywords visibles que empiecen por el prefijo + for (const auto& kw : registry_.getVisibleKeywords()) { + if (upper.empty() || kw.starts_with(upper)) { + tab_matches_.emplace_back(kw); } } } else { const std::string base_cmd = upper.substr(0, space_pos); const std::string sub_prefix = upper.substr(space_pos + 1); - if (base_cmd == "PALETTE" && Screen::get() != nullptr) { + if (registry_.hasDynamicCompletions(base_cmd) && base_cmd == "PALETTE" && Screen::get() != nullptr) { // NEXT/PREV primero, luego todos los nombres de paleta disponibles for (const auto* sv : {"NEXT", "PREV"}) { if (sub_prefix.empty() || std::string_view{sv}.starts_with(sub_prefix)) { @@ -1248,9 +372,9 @@ void Console::handleEvent(const SDL_Event& event) { } } } else { - const auto it = tab_completions_.find(base_cmd); - if (it != tab_completions_.end()) { - for (const auto& arg : it->second) { + const auto* opts = registry_.getCompletions(base_cmd); + if (opts != nullptr) { + for (const auto& arg : *opts) { if (sub_prefix.empty() || std::string_view{arg}.starts_with(sub_prefix)) { tab_matches_.emplace_back(base_cmd + " " + arg); } @@ -1289,17 +413,13 @@ void Console::processCommand() { const std::string& cmd = TOKENS[0]; const std::vector ARGS(TOKENS.begin() + 1, TOKENS.end()); std::string result; - bool found = false; bool instant = false; - for (const auto& command : COMMANDS) { - if (command.keyword == cmd) { - result = command.execute(ARGS); - instant = command.instant; - found = true; - break; - } - } - if (!found) { + + const auto* def = registry_.findCommand(cmd); + if (def != nullptr) { + result = registry_.execute(cmd, ARGS); + instant = def->instant; + } else { std::string cmd_lower = cmd; std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower); result = "Unknown: " + cmd_lower; diff --git a/source/game/ui/console.hpp b/source/game/ui/console.hpp index 6594719..ba7f88e 100644 --- a/source/game/ui/console.hpp +++ b/source/game/ui/console.hpp @@ -6,9 +6,10 @@ #include // Para function #include // Para shared_ptr #include // Para string -#include // Para unordered_map (tab_completions_) #include // Para vector +#include "game/ui/console_commands.hpp" // Para CommandRegistry + class Surface; class Sprite; class Text; @@ -100,7 +101,9 @@ class Console { std::string saved_input_; // guarda input_line_ al empezar a navegar // Estado de autocompletado (TAB) - std::vector tab_matches_; // Comandos que coinciden con el prefijo actual - int tab_index_{-1}; // Índice actual en tab_matches_ - std::unordered_map> tab_completions_; // Mapa pre-calculado en constructor + std::vector tab_matches_; // Comandos que coinciden con el prefijo actual + int tab_index_{-1}; // Índice actual en tab_matches_ + + // Registro de comandos (metadatos YAML + handlers C++) + CommandRegistry registry_; }; diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp new file mode 100644 index 0000000..6b1e68e --- /dev/null +++ b/source/game/ui/console_commands.cpp @@ -0,0 +1,936 @@ +#include "game/ui/console_commands.hpp" + +#include + +#include // Para ranges::transform, ranges::find +#include // Para array +#include // Para cout +#include // Para ostringstream +#include // Para string +#include // Para vector + +#include "core/audio/audio.hpp" // Para Audio +#include "core/locale/locale.hpp" // Para Locale +#include "core/rendering/render_info.hpp" // Para RenderInfo +#include "core/rendering/screen.hpp" // Para Screen +#include "core/resources/resource_helper.hpp" // Para Resource::Helper +#include "external/fkyaml_node.hpp" // Para fkyaml::node +#include "game/game_control.hpp" // Para GameControl +#include "game/options.hpp" // Para Options +#include "game/scene_manager.hpp" // Para SceneManager +#include "game/ui/notifier.hpp" // Para Notifier + +#ifdef _DEBUG +#include "core/system/debug.hpp" // Para Debug +#endif + +// ── Helpers ────────────────────────────────────────────────────────────────── + +// Toggle genérico para comandos booleanos ON/OFF (reemplaza macro BOOL_TOGGLE_CMD) +static auto boolToggle( + const std::string& label, + bool& option, + const std::function& toggle_fn, + const std::vector& args) -> std::string { + if (args.empty()) { + toggle_fn(); + return label + " " + (option ? "ON" : "OFF"); + } + if (args[0] == "ON") { + if (option) { return label + " already ON"; } + toggle_fn(); + return label + " ON"; + } + if (args[0] == "OFF") { + if (!option) { return label + " already OFF"; } + toggle_fn(); + return label + " OFF"; + } + return "usage: " + label + " [on|off]"; +} + +// ── Command handlers ───────────────────────────────────────────────────────── + +// SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] +static auto cmd_ss(const std::vector& args) -> std::string { + if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } + static const std::array DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"}; + if (!args.empty() && args[0] == "SIZE") { + if (!Options::video.supersampling.enabled) { return "Supersampling is OFF: no texture"; } + const auto [w, h] = Screen::get()->getSsTextureSize(); + if (w == 0) { return "SS texture: not active"; } + return "SS texture: " + std::to_string(w) + "x" + std::to_string(h); + } + if (!args.empty() && args[0] == "UPSCALE") { + if (args.size() == 1) { + Screen::get()->setLinearUpscale(!Options::video.supersampling.linear_upscale); + return std::string("Upscale: ") + (Options::video.supersampling.linear_upscale ? "Linear" : "Nearest"); + } + if (args[1] == "NEAREST") { + if (!Options::video.supersampling.linear_upscale) { return "Upscale already Nearest"; } + Screen::get()->setLinearUpscale(false); + return "Upscale: Nearest"; + } + if (args[1] == "LINEAR") { + if (Options::video.supersampling.linear_upscale) { return "Upscale already Linear"; } + Screen::get()->setLinearUpscale(true); + return "Upscale: Linear"; + } + return "usage: ss upscale [nearest|linear]"; + } + if (!args.empty() && args[0] == "DOWNSCALE") { + if (args.size() == 1) { + return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(Options::video.supersampling.downscale_algo)]); + } + int algo = -1; + if (args[1] == "BILINEAR") { algo = 0; } + if (args[1] == "LANCZOS2") { algo = 1; } + if (args[1] == "LANCZOS3") { algo = 2; } + if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; } + if (Options::video.supersampling.downscale_algo == algo) { + return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); + } + Screen::get()->setDownscaleAlgo(algo); + return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast(algo)]); + } + if (args.empty()) { + Screen::get()->toggleSupersampling(); + return std::string("PostFX Supersampling ") + (Options::video.supersampling.enabled ? "ON" : "OFF"); + } + if (args[0] == "ON") { + if (Options::video.supersampling.enabled) { return "Supersampling already ON"; } + Screen::get()->toggleSupersampling(); + return "PostFX Supersampling ON"; + } + if (args[0] == "OFF") { + if (!Options::video.supersampling.enabled) { return "Supersampling already OFF"; } + Screen::get()->toggleSupersampling(); + return "PostFX Supersampling OFF"; + } + return "usage: ss [on|off|size|upscale [nearest|linear]|downscale [bilinear|lanczos2|lanczos3]]"; +} + +// SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] +static auto cmd_shader(const std::vector& args) -> std::string { + if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; } + if (args.empty()) { + Screen::get()->toggleShaders(); + return std::string("Shader ") + (Options::video.shader.enabled ? "ON" : "OFF"); + } + if (args[0] == "ON") { + if (Options::video.shader.enabled) { return "Shader already ON"; } + Screen::get()->toggleShaders(); + return "Shader ON"; + } + if (args[0] == "OFF") { + if (!Options::video.shader.enabled) { return "Shader already OFF"; } + Screen::get()->toggleShaders(); + return "Shader OFF"; + } + if (args[0] == "POSTFX") { + Screen::get()->setActiveShader(Rendering::ShaderType::POSTFX); + return "Shader: PostFX"; + } + if (args[0] == "CRTPI") { + Screen::get()->setActiveShader(Rendering::ShaderType::CRTPI); + return "Shader: CrtPi"; + } + if (args[0] == "NEXT") { + if (args.size() >= 2 && args[1] == "PRESET") { + if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) { + if (Options::crtpi_presets.empty()) { return "No CrtPi presets available"; } + Options::video.shader.current_crtpi_preset = + (Options::video.shader.current_crtpi_preset + 1) % + static_cast(Options::crtpi_presets.size()); + Screen::get()->reloadCrtPi(); + return "CrtPi preset: " + + Options::crtpi_presets[static_cast(Options::video.shader.current_crtpi_preset)].name; + } + if (Options::postfx_presets.empty()) { return "No PostFX presets available"; } + Options::video.shader.current_postfx_preset = + (Options::video.shader.current_postfx_preset + 1) % + static_cast(Options::postfx_presets.size()); + Screen::get()->reloadPostFX(); + return "PostFX preset: " + + Options::postfx_presets[static_cast(Options::video.shader.current_postfx_preset)].name; + } + Screen::get()->nextShader(); + return std::string("Shader: ") + + (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CrtPi" : "PostFX"); + } + return "usage: shader [on|off|next [preset]|postfx|crtpi]"; +} + +// BORDER [ON|OFF] +static auto cmd_border(const std::vector& args) -> std::string { + return boolToggle("Border", Options::video.border.enabled, [] { Screen::get()->toggleBorder(); }, args); +} + +// FULLSCREEN [ON|OFF [PLEASE]] +static auto cmd_fullscreen(const std::vector& args) -> std::string { + const bool EXPLICIT_ON = !args.empty() && args[0] == "ON"; + const bool EXPLICIT_OFF = !args.empty() && args[0] == "OFF"; + const bool WITH_PLEASE = !args.empty() && args.back() == "PLEASE"; + const bool IS_TOGGLE = args.empty(); + const bool TURNING_OFF = EXPLICIT_OFF || (IS_TOGGLE && Options::video.fullscreen); + + if (TURNING_OFF && Options::kiosk.enabled && !WITH_PLEASE) { + return "Not allowed in kiosk mode"; + } + if (EXPLICIT_ON) { + if (Options::video.fullscreen) { return "Fullscreen already ON"; } + Screen::get()->toggleVideoMode(); + return "Fullscreen ON"; + } + if (EXPLICIT_OFF) { + if (!Options::video.fullscreen) { return "Fullscreen already OFF"; } + Screen::get()->toggleVideoMode(); + return "Fullscreen OFF"; + } + if (IS_TOGGLE) { + Screen::get()->toggleVideoMode(); + return std::string("Fullscreen ") + (Options::video.fullscreen ? "ON" : "OFF"); + } + return "usage: fullscreen [on|off]"; +} + +// ZOOM UP/DOWN/ +static auto cmd_zoom(const std::vector& args) -> std::string { + if (args.empty()) { return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]"; } + if (args[0] == "UP") { + if (!Screen::get()->incWindowZoom()) { return "Max zoom reached"; } + return "Zoom " + std::to_string(Options::window.zoom); + } + if (args[0] == "DOWN") { + if (!Screen::get()->decWindowZoom()) { return "Min zoom reached"; } + return "Zoom " + std::to_string(Options::window.zoom); + } + try { + const int N = std::stoi(args[0]); + const int MAX = Screen::get()->getMaxZoom(); + if (N < 1 || N > MAX) { + return "Zoom must be between 1 and " + std::to_string(MAX); + } + if (N == Options::window.zoom) { return "Zoom already " + std::to_string(N); } + Screen::get()->setWindowZoom(N); + return "Zoom " + std::to_string(Options::window.zoom); + } catch (...) {} + return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]"; +} + +// INTSCALE [ON|OFF] +static auto cmd_intscale(const std::vector& args) -> std::string { + const bool ON = args.empty() ? !Options::video.integer_scale + : (args[0] == "ON"); + if (!args.empty() && args[0] != "ON" && args[0] != "OFF") { + return "usage: intscale [on|off]"; + } + if (ON == Options::video.integer_scale) { + return std::string("IntScale already ") + (ON ? "ON" : "OFF"); + } + Screen::get()->toggleIntegerScale(); + Screen::get()->setVideoMode(Options::video.fullscreen); + return std::string("IntScale ") + (Options::video.integer_scale ? "ON" : "OFF"); +} + +// VSYNC [ON|OFF] +static auto cmd_vsync(const std::vector& args) -> std::string { + return boolToggle("VSync", Options::video.vertical_sync, [] { Screen::get()->toggleVSync(); }, args); +} + +// DRIVER [LIST|AUTO|NONE|] +static auto cmd_driver(const std::vector& args) -> std::string { + if (args.empty()) { + const auto& driver = Screen::get()->getGPUDriver(); + return "GPU: " + (driver.empty() ? std::string("sdl") : driver); + } + if (args[0] == "LIST") { + const int COUNT = SDL_GetNumGPUDrivers(); + if (COUNT <= 0) { return "No GPU drivers found"; } + const std::string& active = Screen::get()->getGPUDriver(); + std::string result = "Drivers:"; + for (int i = 0; i < COUNT; ++i) { + const char* name = SDL_GetGPUDriver(i); + if (name != nullptr) { + result += ' '; + result += name; + if (active == name) { result += '*'; } + } + } + SDL_Log("SDL GPU drivers: %s", result.c_str()); + return result; + } + const bool HAS_PLEASE = std::ranges::find(args, std::string("PLEASE")) != args.end(); + if (Options::kiosk.enabled && !HAS_PLEASE) { + return "Not allowed in kiosk mode"; + } + if (args[0] == "AUTO") { + Options::video.gpu.preferred_driver.clear(); + Options::saveToFile(); + return "Driver: auto (restart)"; + } + if (args[0] == "NONE") { + Options::video.gpu.preferred_driver = "none"; + Options::saveToFile(); + return "Driver: none (SDL fallback, restart)"; + } + std::string driver_lower = args[0]; + std::ranges::transform(driver_lower, driver_lower.begin(), ::tolower); + const int COUNT = SDL_GetNumGPUDrivers(); + bool found = false; + for (int i = 0; i < COUNT && !found; ++i) { + const char* name = SDL_GetGPUDriver(i); + if (name != nullptr && driver_lower == name) { found = true; } + } + if (!found) { + return "Unknown driver: " + driver_lower + ". Use DRIVER LIST or NONE"; + } + Options::video.gpu.preferred_driver = driver_lower; + Options::saveToFile(); + return "Driver: " + driver_lower + " (restart)"; +} + +// PALETTE NEXT/PREV/ +static auto cmd_palette(const std::vector& args) -> std::string { + const auto palName = []() -> std::string { + return Screen::get()->getPalettePrettyName(); + }; + if (args.empty()) { return "usage: palette [next|prev|]"; } + if (args[0] == "NEXT") { + Screen::get()->nextPalette(); + return "Palette: " + palName(); + } + if (args[0] == "PREV") { + Screen::get()->previousPalette(); + return "Palette: " + palName(); + } + if (!Screen::get()->setPaletteByName(args[0])) { + std::string arg_lower = args[0]; + std::ranges::transform(arg_lower, arg_lower.begin(), ::tolower); + return "Unknown palette: " + arg_lower; + } + return "Palette: " + palName(); +} + +// AUDIO [ON|OFF|VOL <0-100>] +static auto cmd_audio(const std::vector& args) -> std::string { + if (args.empty()) { + const int VOL = static_cast(Options::audio.volume * 100.0F); + return std::string("Audio ") + (Options::audio.enabled ? "ON" : "OFF") + + " vol:" + std::to_string(VOL); + } + if (args[0] == "ON") { + if (Options::audio.enabled) { return "Audio already ON"; } + Options::audio.enabled = true; + Audio::get()->enable(true); + return "Audio ON"; + } + if (args[0] == "OFF") { + if (!Options::audio.enabled) { return "Audio already OFF"; } + Options::audio.enabled = false; + Audio::get()->enable(false); + return "Audio OFF"; + } + if (args[0] == "VOL" && args.size() >= 2) { + try { + const int VAL = std::stoi(args[1]); + if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } + Options::audio.volume = static_cast(VAL) / 100.0F; + Audio::get()->enable(Options::audio.enabled); + return "Audio vol:" + std::to_string(VAL); + } catch (...) { return "usage: audio vol <0-100>"; } + } + return "usage: audio [on|off|vol n]"; +} + +// MUSIC [ON|OFF|VOL <0-100>] +static auto cmd_music(const std::vector& args) -> std::string { + if (args.empty()) { + const int VOL = static_cast(Options::audio.music.volume * 100.0F); + return std::string("Music ") + (Options::audio.music.enabled ? "ON" : "OFF") + + " vol:" + std::to_string(VOL); + } + if (args[0] == "ON") { + if (Options::audio.music.enabled) { return "Music already ON"; } + Options::audio.music.enabled = true; + Audio::get()->enableMusic(true); + Audio::get()->setMusicVolume(Options::audio.music.volume); + return "Music ON"; + } + if (args[0] == "OFF") { + if (!Options::audio.music.enabled) { return "Music already OFF"; } + Audio::get()->setMusicVolume(0.0F); + Audio::get()->enableMusic(false); + Options::audio.music.enabled = false; + return "Music OFF"; + } + if (args[0] == "VOL" && args.size() >= 2) { + try { + const int VAL = std::stoi(args[1]); + if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } + Options::audio.music.volume = static_cast(VAL) / 100.0F; + if (Options::audio.music.enabled) { + Audio::get()->setMusicVolume(Options::audio.music.volume); + } + return "Music vol:" + std::to_string(VAL); + } catch (...) { return "usage: music vol <0-100>"; } + } + return "usage: music [on|off|vol n]"; +} + +// SOUND [ON|OFF|VOL <0-100>] +static auto cmd_sound(const std::vector& args) -> std::string { + if (args.empty()) { + const int VOL = static_cast(Options::audio.sound.volume * 100.0F); + return std::string("Sound ") + (Options::audio.sound.enabled ? "ON" : "OFF") + + " vol:" + std::to_string(VOL); + } + if (args[0] == "ON") { + if (Options::audio.sound.enabled) { return "Sound already ON"; } + Options::audio.sound.enabled = true; + Audio::get()->enableSound(true); + Audio::get()->setSoundVolume(Options::audio.sound.volume); + return "Sound ON"; + } + if (args[0] == "OFF") { + if (!Options::audio.sound.enabled) { return "Sound already OFF"; } + Audio::get()->setSoundVolume(0.0F); + Audio::get()->enableSound(false); + Options::audio.sound.enabled = false; + return "Sound OFF"; + } + if (args[0] == "VOL" && args.size() >= 2) { + try { + const int VAL = std::stoi(args[1]); + if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } + Options::audio.sound.volume = static_cast(VAL) / 100.0F; + if (Options::audio.sound.enabled) { + Audio::get()->setSoundVolume(Options::audio.sound.volume); + } + return "Sound vol:" + std::to_string(VAL); + } catch (...) { return "usage: sound vol <0-100>"; } + } + return "usage: sound [on|off|vol n]"; +} + +#ifdef _DEBUG +// DEBUG [ON|OFF] +static auto cmd_debug(const std::vector& args) -> std::string { + if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } + if (!GameControl::toggle_debug_mode) { return "Game not initialized"; } + const bool ENABLED = Debug::get()->isEnabled(); + if (!args.empty() && args[0] == "ON") { + if (ENABLED) { return "Debug mode already ON"; } + GameControl::toggle_debug_mode(); + return "Debug mode ON"; + } + if (!args.empty() && args[0] == "OFF") { + if (!ENABLED) { return "Debug mode already OFF"; } + GameControl::toggle_debug_mode(); + return "Debug mode OFF"; + } + if (!args.empty()) { return "usage: debug [on|off]"; } + GameControl::toggle_debug_mode(); + return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF"); +} + +// ROOM |NEXT|PREV +static auto cmd_room(const std::vector& args) -> std::string { + if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } + if (args.empty()) { return "usage: room <1-60>|next|prev"; } + int num = 0; + if (args[0] == "NEXT" || args[0] == "PREV") { + if (!GameControl::get_current_room) { return "Game not initialized"; } + const std::string current = GameControl::get_current_room(); + try { + num = std::stoi(current.substr(0, current.find('.'))); + } catch (...) { return "Cannot determine current room"; } + num += (args[0] == "NEXT") ? 1 : -1; + } else { + try { + num = std::stoi(args[0]); + } catch (...) { return "usage: room <1-60>|next|prev"; } + } + if (num < 1 || num > 60) { return "Room must be between 1 and 60"; } + char buf[16]; + std::snprintf(buf, sizeof(buf), "%02d.yaml", num); + if (GameControl::change_room && GameControl::change_room(buf)) { + return std::string("Room: ") + buf; + } + return std::string("Room not found: ") + buf; +} + +// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] +static auto cmd_scene(const std::vector& args) -> std::string { + if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; } + if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|ending|ending2|restart]"; } + + if (args[0] == "RESTART") { + SceneManager::scene_before_restart = SceneManager::current; + SceneManager::current = SceneManager::Scene::RESTART_CURRENT; + return "Restarting..."; + } + + const auto GO_TO = [](SceneManager::Scene target, const std::string& label) -> std::string { + if (SceneManager::current == target) { + SceneManager::scene_before_restart = target; + SceneManager::current = SceneManager::Scene::RESTART_CURRENT; + } else { + SceneManager::current = target; + } + return "Scene: " + label; + }; + + if (args[0] == "LOGO") { return GO_TO(SceneManager::Scene::LOGO, "Logo"); } + if (args[0] == "LOADING") { return GO_TO(SceneManager::Scene::LOADING_SCREEN, "Loading"); } + if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); } + if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); } + if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); } + if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); } + if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); } + return "Unknown scene: " + args[0]; +} +#endif + +// SHOW [INFO|NOTIFICATION|CHEEVO] +static auto cmd_show(const std::vector& args) -> std::string { +#ifdef _DEBUG + if (!args.empty() && args[0] == "NOTIFICATION") { + Notifier::get()->show({"NOTIFICATION"}); + return "Notification shown"; + } + if (!args.empty() && args[0] == "CHEEVO") { + Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c1")}, Notifier::Style::CHEEVO, -1, false); // NOLINT(readability-static-accessed-through-instance) + return "Cheevo notification shown"; + } + if (args.empty() || args[0] != "INFO") { return "usage: show [info|notification|cheevo]"; } +#else + if (args.empty() || args[0] != "INFO") { return "usage: show [info]"; } +#endif + if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; } + RenderInfo::get()->toggle(); + return "Info overlay ON"; +} + +// HIDE [INFO] +static auto cmd_hide(const std::vector& args) -> std::string { + if (args.empty() || args[0] != "INFO") { return "usage: hide [info]"; } + if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; } + RenderInfo::get()->toggle(); + return "Info overlay OFF"; +} + +// CHEAT [subcomando] +static auto cmd_cheat(const std::vector& args) -> std::string { + if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } + if (args.empty()) { return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; } + + // CHEAT INFINITE LIVES [ON|OFF] + if (args[0] == "INFINITE") { + if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; } + auto& cheat = Options::cheats.infinite_lives; + using State = Options::Cheat::State; + const std::vector REST(args.begin() + 2, args.end()); + if (REST.empty()) { + cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; + } else if (REST[0] == "ON") { + if (cheat == State::ENABLED) { return "Infinite lives already ON"; } + cheat = State::ENABLED; + } else if (REST[0] == "OFF") { + if (cheat == State::DISABLED) { return "Infinite lives already OFF"; } + cheat = State::DISABLED; + } else { + return "usage: cheat infinite lives [on|off]"; + } + if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } + return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF"); + } + + // CHEAT INVINCIBILITY [ON|OFF] + if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") { + auto& cheat = Options::cheats.invincible; + using State = Options::Cheat::State; + if (args.size() == 1) { + cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; + } else if (args[1] == "ON") { + if (cheat == State::ENABLED) { return "Invincibility already ON"; } + cheat = State::ENABLED; + } else if (args[1] == "OFF") { + if (cheat == State::DISABLED) { return "Invincibility already OFF"; } + cheat = State::DISABLED; + } else { + return "usage: cheat invincibility [on|off]"; + } + if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } + return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF"); + } + + // CHEAT OPEN THE JAIL + if (args[0] == "OPEN") { + if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat open the jail"; } + if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; } + Options::cheats.jail_is_open = Options::Cheat::State::ENABLED; + return "Jail opened"; + } + + // CHEAT CLOSE THE JAIL + if (args[0] == "CLOSE") { + if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat close the jail"; } + if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; } + Options::cheats.jail_is_open = Options::Cheat::State::DISABLED; + return "Jail closed"; + } + + return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; +} + +// SET PLAYER SKIN / SET INITIAL / SET ITEMS +static auto cmd_set(const std::vector& args) -> std::string { + if (args.size() >= 3 && args[0] == "PLAYER" && args[1] == "SKIN") { + if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } + int num = 0; + try { + num = std::stoi(args[2]); + } catch (...) {} + if (num < 1 || num > 2) { return "usage: set player skin <1|2>"; } + if (!GameControl::change_player_skin) { return "Game not initialized"; } + GameControl::change_player_skin(num); + return "Player skin: " + std::to_string(num); + } +#ifdef _DEBUG + // SET INITIAL SCENE [] + if (args.size() >= 2 && args[0] == "INITIAL" && args[1] == "SCENE") { + SceneManager::Scene target = SceneManager::current; + std::string name = "current"; + if (args.size() >= 3) { + if (args[2] == "GAME") { + target = SceneManager::Scene::GAME; + name = "game"; + } else if (args[2] == "LOGO") { + target = SceneManager::Scene::LOGO; + name = "logo"; + } else if (args[2] == "LOADING") { + target = SceneManager::Scene::LOADING_SCREEN; + name = "loading"; + } else if (args[2] == "TITLE") { + target = SceneManager::Scene::TITLE; + name = "title"; + } else if (args[2] == "CREDITS") { + target = SceneManager::Scene::CREDITS; + name = "credits"; + } else if (args[2] == "ENDING") { + target = SceneManager::Scene::ENDING; + name = "ending"; + } else if (args[2] == "ENDING2") { + target = SceneManager::Scene::ENDING2; + name = "ending2"; + } else { + std::string scene_lower = args[2]; + std::ranges::transform(scene_lower, scene_lower.begin(), ::tolower); + return "Unknown scene: " + scene_lower; + } + } + Debug::get()->setInitialScene(target); + Debug::get()->saveToFile(); + return "Initial scene set to: " + name; + } + + if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } + + // SET ITEMS <0-200> + if (!args.empty() && args[0] == "ITEMS") { + if (args.size() < 2) { return "usage: set items <0-200>"; } + int count = 0; + try { + count = std::stoi(args[1]); + } catch (...) { return "usage: set items <0-200>"; } + if (count < 0 || count > 200) { return "Items must be between 0 and 200"; } + if (!GameControl::set_items) { return "Game not initialized"; } + GameControl::set_items(count); + return "Items: " + std::to_string(count); + } + + if (args.empty() || args[0] != "INITIAL") { return "usage: set initial [room|pos|scene] | set items <0-200> | set player skin <1|2>"; } + + const bool DO_ROOM = args.size() == 1 || (args.size() >= 2 && args[1] == "ROOM"); + const bool DO_POS = args.size() == 1 || (args.size() >= 2 && args[1] == "POS"); + + if (!DO_ROOM && !DO_POS) { return "usage: set initial [room|pos|scene]"; } + if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; } + + std::string result; + if (DO_ROOM) { result = GameControl::set_initial_room(); } + if (DO_POS) { + if (!result.empty()) { result += ", "; } + result += GameControl::set_initial_pos(); + } + return result; +#else + return "usage: set player skin <1|2>"; +#endif +} + +// RESTART +static auto cmd_restart(const std::vector&) -> std::string { + SceneManager::current = SceneManager::Scene::LOGO; + Audio::get()->stopMusic(); + return "Restarting..."; +} + +// KIOSK [ON|OFF PLEASE|PLEASE] +static auto cmd_kiosk(const std::vector& args) -> std::string { + const bool DISABLE = (!args.empty() && args[0] == "PLEASE") || + (args.size() >= 2 && args[0] == "OFF" && args[1] == "PLEASE"); + if (DISABLE) { + Options::kiosk.enabled = false; + return "Kiosk mode OFF"; + } + if (!args.empty() && args[0] == "OFF") { + return "Not allowed in kiosk mode"; + } + if (args.empty() || args[0] == "ON") { + if (Options::kiosk.enabled) { return "Kiosk mode already ON"; } + Options::kiosk.enabled = true; + if (!Options::video.fullscreen) { Screen::get()->toggleVideoMode(); } + return "Kiosk mode ON"; + } + return "usage: kiosk [on]"; +} + +// EXIT / QUIT +static auto cmd_exit(const std::vector& args) -> std::string { + if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { + return "Not allowed in kiosk mode"; + } + SceneManager::current = SceneManager::Scene::QUIT; + return "Quitting..."; +} + +// SIZE +static auto cmd_size(const std::vector&) -> std::string { + int w = 0; + int h = 0; + SDL_GetWindowSize(SDL_GetRenderWindow(Screen::get()->getRenderer()), &w, &h); + return std::to_string(w) + "x" + std::to_string(h); +} + +// ── CommandRegistry ────────────────────────────────────────────────────────── + +void CommandRegistry::registerHandlers() { + handlers_["cmd_ss"] = cmd_ss; + handlers_["cmd_shader"] = cmd_shader; + handlers_["cmd_border"] = cmd_border; + handlers_["cmd_fullscreen"] = cmd_fullscreen; + handlers_["cmd_zoom"] = cmd_zoom; + handlers_["cmd_intscale"] = cmd_intscale; + handlers_["cmd_vsync"] = cmd_vsync; + handlers_["cmd_driver"] = cmd_driver; + handlers_["cmd_palette"] = cmd_palette; + handlers_["cmd_audio"] = cmd_audio; + handlers_["cmd_music"] = cmd_music; + handlers_["cmd_sound"] = cmd_sound; + handlers_["cmd_show"] = cmd_show; + handlers_["cmd_hide"] = cmd_hide; + handlers_["cmd_cheat"] = cmd_cheat; + handlers_["cmd_set"] = cmd_set; + handlers_["cmd_restart"] = cmd_restart; + handlers_["cmd_kiosk"] = cmd_kiosk; + handlers_["cmd_exit"] = cmd_exit; + handlers_["cmd_quit"] = cmd_exit; // QUIT usa el mismo handler que EXIT + handlers_["cmd_size"] = cmd_size; +#ifdef _DEBUG + handlers_["cmd_debug"] = cmd_debug; + handlers_["cmd_room"] = cmd_room; + handlers_["cmd_scene"] = cmd_scene; +#endif + // HELP se registra en load() como lambda que captura this +} + +void CommandRegistry::load(const std::string& yaml_path) { + registerHandlers(); + + // Cargar y parsear el YAML + auto file_data = Resource::Helper::loadFile(yaml_path); + if (file_data.empty()) { + std::cerr << "CommandRegistry: Unable to load " << yaml_path << '\n'; + return; + } + + std::string yaml_content(file_data.begin(), file_data.end()); + fkyaml::node yaml; + try { + yaml = fkyaml::node::deserialize(yaml_content); + } catch (const fkyaml::exception& e) { + std::cerr << "CommandRegistry: YAML parse error: " << e.what() << '\n'; + return; + } + + if (!yaml.contains("categories")) { return; } + + for (const auto& cat_node : yaml["categories"]) { + const std::string category = cat_node["name"].get_value(); + const bool cat_debug_only = cat_node.contains("debug_only") && cat_node["debug_only"].get_value(); + + if (!cat_node.contains("commands")) { continue; } + + for (const auto& cmd_node : cat_node["commands"]) { + CommandDef def; + def.keyword = cmd_node["keyword"].get_value(); + def.handler_id = cmd_node["handler"].get_value(); + def.category = category; + def.description = cmd_node.contains("description") ? cmd_node["description"].get_value() : ""; + def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value() : def.keyword; + def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value(); + def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value(); + def.debug_only = cat_debug_only || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value()); + def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value(); + def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value(); + + // Completions estáticas + if (cmd_node.contains("completions")) { + auto completions_node = cmd_node["completions"]; + for (auto it = completions_node.begin(); it != completions_node.end(); ++it) { + std::string path = it.key().get_value(); + std::vector opts; + for (const auto& opt : *it) { + opts.push_back(opt.get_value()); + } + def.completions[path] = std::move(opts); + } + } + + // Aplicar debug_extras en debug builds +#ifdef _DEBUG + if (cmd_node.contains("debug_extras")) { + const auto& extras = cmd_node["debug_extras"]; + if (extras.contains("description")) { def.description = extras["description"].get_value(); } + if (extras.contains("usage")) { def.usage = extras["usage"].get_value(); } + if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value(); } + if (extras.contains("completions")) { + def.completions.clear(); + auto extras_completions = extras["completions"]; + for (auto it = extras_completions.begin(); it != extras_completions.end(); ++it) { + std::string path = it.key().get_value(); + std::vector opts; + for (const auto& opt : *it) { + opts.push_back(opt.get_value()); + } + def.completions[path] = std::move(opts); + } + } + } +#endif + + // En Release: saltar comandos debug_only +#ifndef _DEBUG + if (def.debug_only) { continue; } +#endif + + commands_.push_back(std::move(def)); + } + } + + // Registrar el handler de HELP (captura this) + handlers_["cmd_help"] = [this](const std::vector&) -> std::string { + std::cout << generateTerminalHelp(); + return generateConsoleHelp(); + }; + + // Aplanar completions en el mapa global + for (const auto& cmd : commands_) { + for (const auto& [path, opts] : cmd.completions) { + completions_map_[path] = opts; + } + } +} + +auto CommandRegistry::findCommand(const std::string& keyword) const -> const CommandDef* { + for (const auto& cmd : commands_) { + if (cmd.keyword == keyword) { return &cmd; } + } + return nullptr; +} + +auto CommandRegistry::execute(const std::string& keyword, const std::vector& args) const -> std::string { + const auto* def = findCommand(keyword); + if (def == nullptr) { return ""; } + const auto it = handlers_.find(def->handler_id); + if (it == handlers_.end()) { return "Handler not found: " + def->handler_id; } + return it->second(args); +} + +auto CommandRegistry::generateTerminalHelp() const -> std::string { + std::ostringstream out; + out << "=== JDD CONSOLE COMMANDS ===" << '\n'; + + std::string current_category; + for (const auto& cmd : commands_) { + if (cmd.help_hidden) { continue; } + + if (cmd.category != current_category) { + current_category = cmd.category; + out << '\n' << '[' << current_category << ']' << '\n'; + } + + // Formatear: usage alineado a la izquierda, descripción a la derecha + const std::string& usage = cmd.usage; + out << " " << usage; + if (!cmd.description.empty()) { + // Padding para alinear descripción (columna ~40) + const int PAD = std::max(1, 38 - static_cast(usage.size())); + out << std::string(static_cast(PAD), ' ') << cmd.description; + } + out << '\n'; + } + + return out.str(); +} + +auto CommandRegistry::generateConsoleHelp() const -> std::string { + std::string release_cmds; + std::string debug_cmds; + + for (const auto& cmd : commands_) { + if (cmd.help_hidden) { continue; } + + // Convertir keyword a minúsculas para la lista + std::string kw_lower = cmd.keyword; + std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower); + + if (cmd.debug_only) { + if (!debug_cmds.empty()) { debug_cmds += ", "; } + debug_cmds += kw_lower; + } else { + if (!release_cmds.empty()) { release_cmds += ", "; } + release_cmds += kw_lower; + } + } + + std::string result = "Commands:\n" + release_cmds + "\n"; + if (!debug_cmds.empty()) { + result += "\nDebug commands:\n" + debug_cmds + "\n"; + } + result += "-- more info on the terminal"; + return result; +} + +auto CommandRegistry::getCompletions(const std::string& path) const -> const std::vector* { + const auto it = completions_map_.find(path); + if (it != completions_map_.end()) { return &it->second; } + return nullptr; +} + +auto CommandRegistry::getVisibleKeywords() const -> std::vector { + std::vector result; + for (const auto& cmd : commands_) { + if (!cmd.hidden) { + result.push_back(cmd.keyword); + } + } + return result; +} + +auto CommandRegistry::hasDynamicCompletions(const std::string& keyword) const -> bool { + const auto* def = findCommand(keyword); + return def != nullptr && def->dynamic_completions; +} diff --git a/source/game/ui/console_commands.hpp b/source/game/ui/console_commands.hpp new file mode 100644 index 0000000..c77ef45 --- /dev/null +++ b/source/game/ui/console_commands.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include // Para function +#include // Para string +#include // Para unordered_map +#include // Para vector + +// Definición de un comando de consola (metadatos cargados desde YAML) +struct CommandDef { + std::string keyword; + std::string handler_id; + std::string category; + std::string description; + std::string usage; + bool instant{false}; + bool hidden{false}; + bool debug_only{false}; + bool help_hidden{false}; + bool dynamic_completions{false}; + std::unordered_map> completions; +}; + +// Tipo de función handler para comandos +using CommandHandler = std::function& args)>; + +// Registro de comandos: une metadatos YAML con handlers C++ +class CommandRegistry { + public: + // Carga los metadatos de comandos desde un archivo YAML y registra los handlers + void load(const std::string& yaml_path); + + // Búsqueda y ejecución + [[nodiscard]] auto findCommand(const std::string& keyword) const -> const CommandDef*; + auto execute(const std::string& keyword, const std::vector& args) const -> std::string; + + // Generación de ayuda (auto-generada desde los metadatos) + [[nodiscard]] auto generateTerminalHelp() const -> std::string; + [[nodiscard]] auto generateConsoleHelp() const -> std::string; + + // TAB completion + [[nodiscard]] auto getCompletions(const std::string& path) const -> const std::vector*; + [[nodiscard]] auto getVisibleKeywords() const -> std::vector; + [[nodiscard]] auto hasDynamicCompletions(const std::string& keyword) const -> bool; + + private: + std::vector commands_; + std::unordered_map handlers_; + std::unordered_map> completions_map_; + + void registerHandlers(); +};