Files
jaildoctors_dilemma/source/game/ui/console_commands.cpp
Sergio Valor fe520dd341 - Es pot posar shader preset directament per nom desde la consola
- shader preset i palette ja autocompleten amb la llista de noms
2026-04-01 20:08:55 +02:00

1001 lines
41 KiB
C++

#include "game/ui/console_commands.hpp"
#include <SDL3/SDL.h>
#include <algorithm> // Para ranges::transform, ranges::find
#include <array> // Para array
#include <iostream> // Para cout
#include <sstream> // Para ostringstream
#include <string> // Para string
#include <vector> // 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
#include "utils/utils.hpp" // Para toUpper, prettyName
#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<void()>& toggle_fn,
const std::vector<std::string>& 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<std::string>& args) -> std::string {
if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; }
static const std::array<std::string_view, 3> 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<size_t>(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<size_t>(algo)]);
}
Screen::get()->setDownscaleAlgo(algo);
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(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]]";
}
// Helper: aplica un preset por dirección (NEXT/PREV) o nombre; devuelve mensaje
static auto applyPreset(const std::vector<std::string>& args) -> std::string {
const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
const auto& presets_postfx = Options::postfx_presets;
const auto& presets_crtpi = Options::crtpi_presets;
const std::string shader_label = IS_CRTPI ? "CrtPi" : "PostFX";
auto& current_idx = IS_CRTPI ? Options::video.shader.current_crtpi_preset
: Options::video.shader.current_postfx_preset;
const int count = static_cast<int>(IS_CRTPI ? presets_crtpi.size() : presets_postfx.size());
if (count == 0) { return "No " + shader_label + " presets available"; }
const auto presetName = [&]() -> std::string {
const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(current_idx)].name
: presets_postfx[static_cast<size_t>(current_idx)].name;
return prettyName(name);
};
if (args.empty()) {
return shader_label + " preset: " + presetName();
}
if (args[0] == "NEXT") {
current_idx = (current_idx + 1) % count;
} else if (args[0] == "PREV") {
current_idx = (current_idx - 1 + count) % count;
} else {
// Buscar por nombre (case-insensitive, con guiones)
std::string search = args[0];
std::ranges::transform(search, search.begin(), ::toupper);
bool found = false;
for (int i = 0; i < count; ++i) {
const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(i)].name
: presets_postfx[static_cast<size_t>(i)].name;
if (toUpper(name) == search) {
current_idx = i;
found = true;
break;
}
}
if (!found) {
std::string name_lower = args[0];
std::ranges::transform(name_lower, name_lower.begin(), ::tolower);
return "Unknown preset: " + name_lower;
}
}
if (IS_CRTPI) { Screen::get()->reloadCrtPi(); } else { Screen::get()->reloadPostFX(); }
return shader_label + " preset: " + presetName();
}
// SHADER [ON|OFF|NEXT|POSTFX|CRTPI|PRESET [NEXT|PREV|<name>]]
static auto cmd_shader(const std::vector<std::string>& 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") {
Screen::get()->nextShader();
return std::string("Shader: ") +
(Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CrtPi" : "PostFX");
}
if (args[0] == "PRESET") {
const std::vector<std::string> rest(args.begin() + 1, args.end());
return applyPreset(rest);
}
return "usage: shader [on|off|next|postfx|crtpi|preset [next|prev|<name>]]";
}
// BORDER [ON|OFF]
static auto cmd_border(const std::vector<std::string>& 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<std::string>& 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/<num>
static auto cmd_zoom(const std::vector<std::string>& 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<std::string>& 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<std::string>& args) -> std::string {
return boolToggle("VSync", Options::video.vertical_sync, [] { Screen::get()->toggleVSync(); }, args);
}
// DRIVER [LIST|AUTO|NONE|<name>]
static auto cmd_driver(const std::vector<std::string>& 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/<name>
static auto cmd_palette(const std::vector<std::string>& args) -> std::string {
const auto palName = []() -> std::string {
return Screen::get()->getPalettePrettyName();
};
if (args.empty()) { return "usage: palette [next|prev|<name>]"; }
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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& 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 <num>|NEXT|PREV
static auto cmd_room(const std::vector<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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<std::string> 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<std::string>& 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 [<nombre>]
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>&) -> std::string {
SceneManager::current = SceneManager::Scene::LOGO;
Audio::get()->stopMusic();
return "Restarting...";
}
// KIOSK [ON|OFF PLEASE|PLEASE]
static auto cmd_kiosk(const std::vector<std::string>& 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<std::string>& 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>&) -> 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
// Proveedores de completions dinámicas
// PALETTE: NEXT, PREV + nombres de paletas disponibles (UPPERCASE)
dynamic_providers_["PALETTE"] = []() -> std::vector<std::string> {
std::vector<std::string> result = {"NEXT", "PREV"};
if (Screen::get() != nullptr) {
for (const auto& name : Screen::get()->getPaletteNames()) {
result.push_back(toUpper(name));
}
}
return result;
};
// SHADER PRESET: NEXT, PREV + nombres de presets del shader activo (UPPERCASE)
dynamic_providers_["SHADER PRESET"] = []() -> std::vector<std::string> {
std::vector<std::string> result = {"NEXT", "PREV"};
const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
if (IS_CRTPI) {
for (const auto& p : Options::crtpi_presets) { result.push_back(toUpper(p.name)); }
} else {
for (const auto& p : Options::postfx_presets) { result.push_back(toUpper(p.name)); }
}
return result;
};
}
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<std::string>();
const bool cat_debug_only = cat_node.contains("debug_only") && cat_node["debug_only"].get_value<bool>();
if (!cat_node.contains("commands")) { continue; }
for (const auto& cmd_node : cat_node["commands"]) {
CommandDef def;
def.keyword = cmd_node["keyword"].get_value<std::string>();
def.handler_id = cmd_node["handler"].get_value<std::string>();
def.category = category;
def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
def.debug_only = cat_debug_only || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();
// 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::string>();
std::vector<std::string> opts;
for (const auto& opt : *it) {
opts.push_back(opt.get_value<std::string>());
}
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<std::string>(); }
if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
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::string>();
std::vector<std::string> opts;
for (const auto& opt : *it) {
opts.push_back(opt.get_value<std::string>());
}
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::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<std::string>& 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<int>(usage.size()));
out << std::string(static_cast<size_t>(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 -> std::vector<std::string> {
// Primero: buscar proveedor dinámico (tiene prioridad si existe)
const auto dyn_it = dynamic_providers_.find(path);
if (dyn_it != dynamic_providers_.end()) {
return dyn_it->second();
}
// Fallback: completions estáticas del YAML
const auto it = completions_map_.find(path);
if (it != completions_map_.end()) { return it->second; }
return {};
}
auto CommandRegistry::getVisibleKeywords() const -> std::vector<std::string> {
std::vector<std::string> result;
for (const auto& cmd : commands_) {
if (!cmd.hidden) {
result.push_back(cmd.keyword);
}
}
return result;
}