#include "game_console.h" #include "core/io/json.h" #include "core/os/os.h" #include "core/string/print_string.h" GameConsole *GameConsole::singleton = nullptr; GameConsole::GameConsole() { singleton = this; _register_builtins(); } GameConsole::~GameConsole() { singleton = nullptr; } void GameConsole::_bind_methods() { ClassDB::bind_method(D_METHOD("register_command", "name", "callback", "description", "usage", "is_cheat"), &GameConsole::register_command, DEFVAL(""), DEFVAL(""), DEFVAL(false)); ClassDB::bind_method(D_METHOD("unregister_command", "name"), &GameConsole::unregister_command); ClassDB::bind_method(D_METHOD("has_command", "name"), &GameConsole::has_command); ClassDB::bind_method(D_METHOD("execute", "input"), &GameConsole::execute); ClassDB::bind_method(D_METHOD("execute_command", "name", "args"), &GameConsole::execute_command); ClassDB::bind_method(D_METHOD("set_alias", "alias", "command"), &GameConsole::set_alias); ClassDB::bind_method(D_METHOD("remove_alias", "alias"), &GameConsole::remove_alias); ClassDB::bind_method(D_METHOD("get_commands"), &GameConsole::get_commands); ClassDB::bind_method(D_METHOD("get_history", "count"), &GameConsole::get_history, DEFVAL(50)); ClassDB::bind_method(D_METHOD("get_output", "count"), &GameConsole::get_output, DEFVAL(100)); ClassDB::bind_method(D_METHOD("autocomplete", "partial"), &GameConsole::autocomplete); ClassDB::bind_method(D_METHOD("set_cheats_enabled", "enabled"), &GameConsole::set_cheats_enabled); ClassDB::bind_method(D_METHOD("get_cheats_enabled"), &GameConsole::get_cheats_enabled); } void GameConsole::register_command(const String &p_name, const Callable &p_callback, const String &p_description, const String &p_usage, bool p_is_cheat) { MutexLock lock(console_mutex); Command cmd; cmd.name = p_name; cmd.callback = p_callback; cmd.description = p_description; cmd.usage = p_usage; cmd.is_cheat = p_is_cheat; commands[p_name] = cmd; } void GameConsole::unregister_command(const String &p_name) { MutexLock lock(console_mutex); commands.erase(p_name); } bool GameConsole::has_command(const String &p_name) const { return commands.has(p_name); } String GameConsole::execute(const String &p_input) { String input = p_input.strip_edges(); if (input.is_empty()) { return ""; } // Add to history. { MutexLock lock(console_mutex); command_history.push_back(input); if (command_history.size() > MAX_HISTORY) { command_history.remove_at(0); } } // Parse command and arguments. Vector parts = input.split(" ", false); if (parts.size() == 0) { return ""; } String cmd_name = parts[0]; // Check aliases. if (aliases.has(cmd_name)) { cmd_name = aliases[cmd_name]; } // Build args array. Array args; for (int i = 1; i < parts.size(); i++) { args.push_back(parts[i]); } return execute_command(cmd_name, args); } String GameConsole::execute_command(const String &p_name, const Array &p_args) { MutexLock lock(console_mutex); if (!commands.has(p_name)) { String err = vformat("Unknown command: %s", p_name); _add_output(err, "error"); return err; } const Command &cmd = commands[p_name]; // Check cheat protection. if (cmd.is_cheat && !cheats_enabled) { String err = vformat("Command '%s' requires cheats to be enabled", p_name); _add_output(err, "error"); return err; } // Execute callback. Variant result; Callable::CallError call_error; const Variant args_var = p_args; const Variant *argptrs[1] = { &args_var }; cmd.callback.callp(argptrs, 1, result, call_error); String result_str; if (call_error.error != Callable::CallError::CALL_OK) { result_str = vformat("Error executing '%s': call error %d", p_name, call_error.error); _add_output(result_str, "error"); } else { result_str = String(result); if (!result_str.is_empty()) { _add_output(result_str); } } return result_str; } void GameConsole::set_alias(const String &p_alias, const String &p_command) { aliases[p_alias] = p_command; } void GameConsole::remove_alias(const String &p_alias) { aliases.erase(p_alias); } Array GameConsole::get_commands() const { Array result; for (const KeyValue &kv : commands) { Dictionary dict; dict["name"] = kv.value.name; dict["description"] = kv.value.description; dict["usage"] = kv.value.usage; dict["is_cheat"] = kv.value.is_cheat; result.push_back(dict); } return result; } Array GameConsole::get_history(int p_count) const { Array result; int start = MAX(0, command_history.size() - p_count); for (int i = start; i < command_history.size(); i++) { result.push_back(command_history[i]); } return result; } Array GameConsole::get_output(int p_count) const { Array result; int start = MAX(0, output_log.size() - p_count); for (int i = start; i < output_log.size(); i++) { Dictionary dict; dict["timestamp_msec"] = output_log[i].timestamp_msec; dict["text"] = output_log[i].text; dict["type"] = output_log[i].type; result.push_back(dict); } return result; } Array GameConsole::autocomplete(const String &p_partial) const { Array result; String lower_partial = p_partial.to_lower(); for (const KeyValue &kv : commands) { if (kv.key.to_lower().begins_with(lower_partial)) { result.push_back(kv.key); } } // Also check aliases. for (const KeyValue &kv : aliases) { if (kv.key.to_lower().begins_with(lower_partial)) { result.push_back(kv.key); } } return result; } void GameConsole::_add_output(const String &p_text, const String &p_type) { OutputEntry entry; entry.timestamp_msec = OS::get_singleton()->get_ticks_msec(); entry.text = p_text; entry.type = p_type; output_log.push_back(entry); if (output_log.size() > MAX_OUTPUT) { output_log.remove_at(0); } } // --- Built-in Commands --- void GameConsole::_register_builtins() { register_command("help", callable_mp(this, &GameConsole::_cmd_help), "List all commands", "help [command]"); register_command("clear", callable_mp(this, &GameConsole::_cmd_clear), "Clear console output", "clear"); register_command("alias", callable_mp(this, &GameConsole::_cmd_alias), "Create command alias", "alias "); register_command("cheats", callable_mp(this, &GameConsole::_cmd_cheats), "Enable/disable cheats", "cheats "); } void GameConsole::_cmd_help(const Array &p_args) { if (p_args.size() > 0) { String cmd_name = p_args[0]; if (commands.has(cmd_name)) { const Command &cmd = commands[cmd_name]; _add_output(vformat("%s - %s", cmd.name, cmd.description)); if (!cmd.usage.is_empty()) { _add_output(vformat(" Usage: %s", cmd.usage)); } return; } _add_output(vformat("Unknown command: %s", cmd_name), "error"); return; } _add_output("Available commands:", "info"); for (const KeyValue &kv : commands) { String cheat_tag = kv.value.is_cheat ? " [CHEAT]" : ""; _add_output(vformat(" %s - %s%s", kv.key, kv.value.description, cheat_tag)); } } void GameConsole::_cmd_clear(const Array &p_args) { output_log.clear(); } void GameConsole::_cmd_alias(const Array &p_args) { if (p_args.size() < 2) { _add_output("Usage: alias ", "error"); return; } set_alias(p_args[0], p_args[1]); _add_output(vformat("Alias '%s' -> '%s'", String(p_args[0]), String(p_args[1]))); } void GameConsole::_cmd_cheats(const Array &p_args) { if (p_args.size() == 0) { _add_output(vformat("Cheats: %s", cheats_enabled ? "ON" : "OFF")); return; } String val = String(p_args[0]).to_lower(); cheats_enabled = (val == "on" || val == "true" || val == "1"); _add_output(vformat("Cheats: %s", cheats_enabled ? "ON" : "OFF")); }