refactor console_commands: applyCheatToggle, parsers YAML al namespace anonim, buildHelp i append_csv

This commit is contained in:
2026-05-17 21:56:47 +02:00
parent 87d3e70af3
commit 62bf99f174
2 changed files with 145 additions and 151 deletions
+144 -151
View File
@@ -426,7 +426,7 @@ static auto cmdSound(const std::vector<std::string>& args) -> std::string {
#ifdef _DEBUG
// DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]
static auto cmdDebug(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
static auto cmdDebug(const std::vector<std::string>& args) -> std::string {
// --- START subcommands (START SCENE works from any scene) ---
if (!args.empty() && args[0] == "START") {
// START SCENE [<name>] — works from any scene
@@ -525,7 +525,7 @@ static auto changeRoomWithEditor(const std::string& room_file) -> std::string {
return std::string("Room: ") + room_file;
}
static auto cmdRoom(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
static auto cmdRoom(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 <num>|next|prev|left|right|up|down"; }
@@ -613,7 +613,7 @@ static auto cmdScene(const std::vector<std::string>& args) -> std::string {
}
// EDIT [ON|OFF|REVERT]
static auto cmdEdit(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
static auto cmdEdit(const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
// Toggle: si está activo → off, si no → on
if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) {
@@ -814,47 +814,38 @@ static auto cmdHide(const std::vector<std::string>& args) -> std::string {
}
// CHEAT [subcomando]
static auto cmdCheat(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
// Apply ON/OFF/toggle a un cheat binari. mode="" → toggle; "ON"/"OFF" → estableix; altra cosa → cadena buida (usage error).
static auto applyCheatToggle(Options::Cheat::State& cheat, std::string_view mode, std::string_view label) -> std::string {
using State = Options::Cheat::State;
if (mode.empty()) {
cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
} else if (mode == "ON") {
if (cheat == State::ENABLED) { return std::string(label) + " already ON"; }
cheat = State::ENABLED;
} else if (mode == "OFF") {
if (cheat == State::DISABLED) { return std::string(label) + " already OFF"; }
cheat = State::DISABLED;
} else {
return {}; // sentinel: mode invàlid
}
return std::string(label) + " " + (cheat == State::ENABLED ? "ON" : "OFF");
}
static auto cmdCheat(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]"; }
// 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());
// cppcheck-suppress knownConditionTrueFalse -- cppcheck no infiere que REST puede estar vacío cuando args.size() == 2.
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]";
}
return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF");
const std::string_view MODE = (args.size() > 2) ? std::string_view(args[2]) : std::string_view();
const std::string RES = applyCheatToggle(Options::cheats.infinite_lives, MODE, "Infinite lives");
return RES.empty() ? "usage: cheat infinite lives [on|off]" : RES;
}
// 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]";
}
return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF");
const std::string_view MODE = (args.size() > 1) ? std::string_view(args[1]) : std::string_view();
const std::string RES = applyCheatToggle(Options::cheats.invincible, MODE, "Invincibility");
return RES.empty() ? "usage: cheat invincibility [on|off]" : RES;
}
return "usage: cheat [infinite lives|invincibility]";
@@ -910,7 +901,7 @@ static auto cmdSize(const std::vector<std::string>& /*unused*/) -> std::string {
}
// CONSOLE [TRANSPARENT [ON|OFF]|BG|MSG|PROMPT|COMMAND <0-255>]
static auto cmdConsole(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
static auto cmdConsole(const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
return std::string("Console ") + (Options::console.transparent ? "transparent" : "solid") + " bg:" + std::to_string(Options::console.bg_color) + " msg:" + std::to_string(Options::console.msg_color) + " prompt:" + std::to_string(Options::console.prompt_color) + " cmd:" + std::to_string(Options::console.command_color);
}
@@ -955,7 +946,7 @@ static auto cmdConsole(const std::vector<std::string>& args) -> std::string { /
// ── CommandRegistry ──────────────────────────────────────────────────────────
void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cognitive-complexity)
void CommandRegistry::registerHandlers() {
handlers_["cmd_shader"] = cmdShader;
handlers_["cmd_border"] = cmdBorder;
handlers_["cmd_fullscreen"] = cmdFullscreen;
@@ -1074,7 +1065,108 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni
#endif
}
void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readability-function-cognitive-complexity)
namespace {
// Parseja un node "scope" (string o sequence) a un vector. Buit si node és buit.
auto parseScopeNode(const fkyaml::node& scope_node) -> std::vector<std::string> {
std::vector<std::string> result;
if (scope_node.is_sequence()) {
std::ranges::transform(scope_node, std::back_inserter(result), [](const auto& s) { return s.template get_value<std::string>(); });
} else {
result.push_back(scope_node.get_value<std::string>());
}
return result;
}
// Parseja un mapping de path → [options] en l'unordered_map de destí
void parseCompletionsNode(const fkyaml::node& completions_node, std::unordered_map<std::string, std::vector<std::string>>& out) {
for (auto it = completions_node.begin(); it != completions_node.end(); ++it) {
auto path = it.key().get_value<std::string>();
std::vector<std::string> opts;
std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
out[path] = std::move(opts);
}
}
#ifdef _DEBUG
// Aplica camps "debug_extras" sobre un CommandDef ja inicialitzat (només en _DEBUG)
void applyDebugExtras(const fkyaml::node& extras, CommandDef& def) {
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("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
if (extras.contains("completions")) {
def.completions.clear();
parseCompletionsNode(extras["completions"], def.completions);
}
}
#endif
// Parseja un cmd_node a CommandDef. Hereta de la categoria si el comand no defineix scope/debug_only.
auto parseCommandDef(const fkyaml::node& cmd_node, const std::string& category, bool cat_debug_only, const std::vector<std::string>& cat_scopes) -> CommandDef {
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>();
if (cmd_node.contains("scope")) {
def.scopes = parseScopeNode(cmd_node["scope"]);
} else if (!cat_scopes.empty()) {
def.scopes = cat_scopes;
} else {
def.scopes.emplace_back("global");
}
if (cmd_node.contains("completions")) {
parseCompletionsNode(cmd_node["completions"], def.completions);
}
#ifdef _DEBUG
if (cmd_node.contains("debug_extras")) {
applyDebugExtras(cmd_node["debug_extras"], def);
}
#endif
return def;
}
} // namespace
// Implementació del handler "cmd_help"
auto CommandRegistry::buildHelp(const std::vector<std::string>& args) const -> std::string {
if (!args.empty()) {
// HELP KEYS [scope]: referencia de atajos de teclado
if (args[0] == "KEYS") {
return generateKeysHelp(args.size() > 1 ? args[1] : "");
}
// HELP <command>: mostrar ayuda detallada de un comando
const auto* cmd = findCommand(args[0]);
if (cmd == nullptr) { return "Unknown command: " + args[0]; }
std::string kw_lower = cmd->keyword;
std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);
std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage;
// Listar subcomandos/opciones si hay completions
const auto OPTS = getCompletions(cmd->keyword);
if (!OPTS.empty()) {
result += "\noptions:";
for (const auto& opt : OPTS) {
std::string opt_lower = opt;
std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower);
result += " " + opt_lower;
}
}
return result;
}
std::cout << generateTerminalHelp();
return generateConsoleHelp();
}
void CommandRegistry::load(const std::string& yaml_path) {
registerHandlers();
// Cargar y parsear el YAML
@@ -1098,121 +1190,21 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit
for (const auto& cat_node : yaml["categories"]) {
const auto 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>();
// Scopes por defecto de la categoría
std::vector<std::string> cat_scopes;
if (cat_node.contains("scope")) {
const auto& scope_node = cat_node["scope"];
if (scope_node.is_sequence()) {
std::ranges::transform(scope_node, std::back_inserter(cat_scopes), [](const auto& s) { return s.template get_value<std::string>(); });
} else {
cat_scopes.push_back(scope_node.get_value<std::string>());
}
}
const std::vector<std::string> CAT_SCOPES = cat_node.contains("scope") ? parseScopeNode(cat_node["scope"]) : std::vector<std::string>{};
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>();
// Scopes: del comando, o hereda de la categoría, o "global" por defecto
if (cmd_node.contains("scope")) {
const auto& scope_node = cmd_node["scope"];
if (scope_node.is_sequence()) {
std::ranges::transform(scope_node, std::back_inserter(def.scopes), [](const auto& s) { return s.template get_value<std::string>(); });
} else {
def.scopes.push_back(scope_node.get_value<std::string>());
}
} else if (!cat_scopes.empty()) {
def.scopes = cat_scopes;
} else {
def.scopes.emplace_back("global");
}
// 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) {
auto path = it.key().get_value<std::string>();
std::vector<std::string> opts;
const auto& options_node = *it;
std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template 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("help_hidden")) { def.help_hidden = extras["help_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) {
auto path = it.key().get_value<std::string>();
std::vector<std::string> opts;
const auto& options_node = *it;
std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
def.completions[path] = std::move(opts);
}
}
}
#endif
// En Release: saltar comandos debug_only
CommandDef def = parseCommandDef(cmd_node, CATEGORY, CAT_DEBUG_ONLY, CAT_SCOPES);
#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>& args) -> std::string {
if (!args.empty()) {
// HELP KEYS [scope]: referencia de atajos de teclado
if (args[0] == "KEYS") {
return generateKeysHelp(args.size() > 1 ? args[1] : "");
}
// HELP <command>: mostrar ayuda detallada de un comando
const auto* cmd = findCommand(args[0]);
if (cmd != nullptr) {
std::string kw_lower = cmd->keyword;
std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);
std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage;
// Listar subcomandos/opciones si hay completions
auto opts = getCompletions(cmd->keyword);
if (!opts.empty()) {
result += "\noptions:";
for (const auto& opt : opts) {
std::string opt_lower = opt;
std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower);
result += " " + opt_lower;
}
}
return result;
}
return "Unknown command: " + args[0];
}
std::cout << generateTerminalHelp();
return generateConsoleHelp();
};
// Registrar el handler de HELP (delega a buildHelp per mantenir baixa la complexitat de load)
handlers_["cmd_help"] = [this](const std::vector<std::string>& args) -> std::string { return buildHelp(args); };
// Aplanar completions en el mapa global
for (const auto& cmd : commands_) {
@@ -1300,12 +1292,17 @@ auto CommandRegistry::generateTerminalHelp() const -> std::string {
return out.str();
}
auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto CommandRegistry::generateConsoleHelp() const -> std::string {
// Agrupar comandos visibles por scope
std::string global_cmds;
std::string debug_cmds;
std::string editor_cmds;
auto append_csv = [](std::string& dst, const std::string& token) {
if (!dst.empty()) { dst += ", "; }
dst += token;
};
for (const auto& cmd : commands_) {
if (cmd.help_hidden) { continue; }
if (!isCommandVisible(cmd)) { continue; }
@@ -1315,16 +1312,12 @@ auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(re
// Clasificar por el PRIMER scope del comando
const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0];
if (primary == "editor") {
if (!editor_cmds.empty()) { editor_cmds += ", "; }
editor_cmds += kw_lower;
append_csv(editor_cmds, kw_lower);
} else if (primary == "debug") {
if (!debug_cmds.empty()) { debug_cmds += ", "; }
debug_cmds += kw_lower;
append_csv(debug_cmds, kw_lower);
} else {
if (!global_cmds.empty()) { global_cmds += ", "; }
global_cmds += kw_lower;
append_csv(global_cmds, kw_lower);
}
}
+1
View File
@@ -59,4 +59,5 @@ class CommandRegistry {
void registerHandlers();
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
[[nodiscard]] static auto generateKeysHelp(const std::string& scope_filter) -> std::string;
[[nodiscard]] auto buildHelp(const std::vector<std::string>& args) const -> std::string; // Implementació del handler "cmd_help"
};