Files
engine/modules/agent_console/game_console.cpp
ozan d291dcdc74 feat: 9 agentic engine modules for agent-native Godot
agent_api (HTTP server), agent_log (structured logging), agent_events (event bus),
agent_console (GameConsole), agent_replay (snapshots), agent_vision (depth/segmentation),
agent_fbx (bone remapping), agent_auth (multi-agent), agent_analytics (feature flags + tracking)

All modules compile clean with mono. Binary uploaded to S3 v1.0.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 03:44:28 +01:00

254 lines
7.5 KiB
C++

#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<String> 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<String, Command> &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<String, Command> &kv : commands) {
if (kv.key.to_lower().begins_with(lower_partial)) {
result.push_back(kv.key);
}
}
// Also check aliases.
for (const KeyValue<String, String> &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 <name> <command>");
register_command("cheats", callable_mp(this, &GameConsole::_cmd_cheats), "Enable/disable cheats", "cheats <on|off>");
}
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<String, Command> &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 <name> <command>", "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"));
}