#include "agent_server.h" #include "core/config/engine.h" #include "core/config/project_settings.h" #include "core/io/image.h" #include "core/io/json.h" #include "core/os/os.h" #include "core/os/time.h" #include "core/string/print_string.h" #include "scene/main/node.h" #include "scene/main/scene_tree.h" #include "scene/main/viewport.h" #include "scene/main/window.h" #include "scene/resources/packed_scene.h" #include "servers/rendering/rendering_server_globals.h" #include "servers/rendering/rendering_server.h" #include "servers/physics_3d/physics_server_3d.h" #include "core/math/expression.h" #include "main/performance.h" AgentServer *AgentServer::singleton = nullptr; AgentServer::AgentServer() { singleton = this; tcp_server.instantiate(); } AgentServer::~AgentServer() { stop(); singleton = nullptr; } void AgentServer::_bind_methods() { ClassDB::bind_method(D_METHOD("start", "port", "token"), &AgentServer::start, DEFVAL(4329), DEFVAL("")); ClassDB::bind_method(D_METHOD("stop"), &AgentServer::stop); ClassDB::bind_method(D_METHOD("is_running"), &AgentServer::is_running); ClassDB::bind_method(D_METHOD("get_port"), &AgentServer::get_port); ClassDB::bind_method(D_METHOD("poll"), &AgentServer::poll); ClassDB::bind_method(D_METHOD("push_sse", "stream_type", "event", "data"), &AgentServer::push_sse); } // --- Lifecycle --- void AgentServer::start(int p_port, const String &p_token) { if (running) { return; } port = p_port; auth_token = p_token; Error err = tcp_server->listen(port, IPAddress("0.0.0.0")); if (err != OK) { ERR_PRINT(vformat("AgentServer: Failed to listen on port %d (error %d)", port, err)); return; } running = true; server_thread.start(_server_thread_func, this); print_line(vformat("AgentServer: Listening on http://0.0.0.0:%d", port)); // Connect poll() to the SceneTree's process_frame signal so screenshot/depth/command // requests get serviced on the main thread each frame. Deferred because SceneTree // may not exist yet during module init. callable_mp(this, &AgentServer::_connect_to_scene_tree).call_deferred(); } void AgentServer::_connect_to_scene_tree() { SceneTree *tree = SceneTree::get_singleton(); if (tree) { tree->connect("process_frame", callable_mp(this, &AgentServer::poll)); } else { ERR_PRINT("AgentServer: SceneTree not available, poll() won't run — screenshots will timeout."); } } void AgentServer::stop() { if (!running) { return; } running = false; server_thread.wait_to_finish(); // Disconnect from SceneTree if connected. SceneTree *tree = SceneTree::get_singleton(); if (tree && tree->is_connected("process_frame", callable_mp(this, &AgentServer::poll))) { tree->disconnect("process_frame", callable_mp(this, &AgentServer::poll)); } { MutexLock lock(sse_mutex); sse_clients.clear(); } tcp_server->stop(); print_line("AgentServer: Stopped."); } void AgentServer::check_cmdline_and_start() { // Check --agent-api flag or AGENT_API env var. List cmdline_args = OS::get_singleton()->get_cmdline_args(); bool should_start = false; int custom_port = 4329; String custom_token; for (const String &arg : cmdline_args) { if (arg == "--agent-api") { should_start = true; } else if (arg.begins_with("--agent-port=")) { custom_port = arg.get_slice("=", 1).to_int(); } else if (arg.begins_with("--agent-token=")) { custom_token = arg.get_slice("=", 1); } } if (!should_start && OS::get_singleton()->has_environment("AGENT_API")) { String env_val = OS::get_singleton()->get_environment("AGENT_API"); if (env_val == "1" || env_val == "true") { should_start = true; } } if (!should_start && OS::get_singleton()->has_environment("AGENT_API_PORT")) { custom_port = OS::get_singleton()->get_environment("AGENT_API_PORT").to_int(); should_start = true; } if (OS::get_singleton()->has_environment("AGENT_API_TOKEN")) { custom_token = OS::get_singleton()->get_environment("AGENT_API_TOKEN"); } if (should_start && singleton) { singleton->start(custom_port, custom_token); } } // --- Server Thread --- void AgentServer::_server_thread_func(void *p_userdata) { AgentServer *self = static_cast(p_userdata); self->_server_loop(); } void AgentServer::_server_loop() { while (running) { if (tcp_server->is_connection_available()) { Ref peer = tcp_server->take_connection(); if (peer.is_valid()) { _handle_connection(peer); } } // Push keepalive to SSE clients periodically. { MutexLock lock(sse_mutex); for (int i = sse_clients.size() - 1; i >= 0; i--) { if (sse_clients[i].peer.is_null() || sse_clients[i].peer->get_status() != StreamPeerTCP::STATUS_CONNECTED) { sse_clients.remove_at(i); } } } OS::get_singleton()->delay_usec(1000); // 1ms poll interval. } } void AgentServer::_handle_connection(Ref p_peer) { // Read the full HTTP request with a timeout. String raw_request; uint64_t start_time = OS::get_singleton()->get_ticks_msec(); const uint64_t timeout_ms = 5000; while (OS::get_singleton()->get_ticks_msec() - start_time < timeout_ms) { p_peer->poll(); if (p_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED) { return; } int available = p_peer->get_available_bytes(); if (available > 0) { Vector data; data.resize(available); int bytes_read = 0; p_peer->get_partial_data(data.ptrw(), available, bytes_read); if (bytes_read > 0) { data.resize(bytes_read); raw_request += String::utf8((const char *)data.ptr(), bytes_read); } // Check if we have a complete HTTP request (headers + body). if (raw_request.find("\r\n\r\n") >= 0) { // Check Content-Length for body. int header_end = raw_request.find("\r\n\r\n"); String headers_part = raw_request.substr(0, header_end); int content_length = 0; int cl_pos = headers_part.findn("content-length:"); if (cl_pos >= 0) { String cl_line = headers_part.substr(cl_pos); int nl_pos = cl_line.find("\r\n"); if (nl_pos >= 0) { cl_line = cl_line.substr(0, nl_pos); } content_length = cl_line.get_slice(":", 1).strip_edges().to_int(); } String body_so_far = raw_request.substr(header_end + 4); if (body_so_far.length() >= content_length) { break; // Full request received. } } } OS::get_singleton()->delay_usec(100); } if (raw_request.is_empty()) { return; } HTTPRequest request = _parse_http_request(raw_request); // Check for SSE stream requests — these keep the connection alive. if (request.path == "/logs/stream") { if (!_check_auth(request)) { HTTPResponse resp; resp.status_code = 401; resp.body = _json_error(401, "Unauthorized"); _send_response(p_peer, resp); return; } _send_sse_headers(p_peer); MutexLock lock(sse_mutex); SSEClient client; client.peer = p_peer; client.stream_type = "logs"; client.connected_at = OS::get_singleton()->get_ticks_msec(); sse_clients.push_back(client); return; } if (request.path == "/events/stream") { if (!_check_auth(request)) { HTTPResponse resp; resp.status_code = 401; resp.body = _json_error(401, "Unauthorized"); _send_response(p_peer, resp); return; } _send_sse_headers(p_peer); MutexLock lock(sse_mutex); SSEClient client; client.peer = p_peer; client.stream_type = "events"; client.connected_at = OS::get_singleton()->get_ticks_msec(); sse_clients.push_back(client); return; } HTTPResponse response = _handle_request(request); _send_response(p_peer, response); } // --- HTTP Parsing --- AgentServer::HTTPRequest AgentServer::_parse_http_request(const String &p_raw) { HTTPRequest req; int header_end = p_raw.find("\r\n\r\n"); String header_section = (header_end >= 0) ? p_raw.substr(0, header_end) : p_raw; req.body = (header_end >= 0) ? p_raw.substr(header_end + 4) : ""; Vector lines = header_section.split("\r\n"); if (lines.size() == 0) { return req; } // Parse request line: METHOD /path?query HTTP/1.1 Vector request_parts = lines[0].split(" "); if (request_parts.size() >= 2) { req.method = request_parts[0]; String full_path = request_parts[1]; int query_pos = full_path.find("?"); if (query_pos >= 0) { req.path = full_path.substr(0, query_pos); String query_string = full_path.substr(query_pos + 1); Vector params = query_string.split("&"); for (const String ¶m : params) { int eq_pos = param.find("="); if (eq_pos >= 0) { req.query_params[param.substr(0, eq_pos)] = param.substr(eq_pos + 1).uri_decode(); } else { req.query_params[param] = ""; } } } else { req.path = full_path; } } // Parse headers. for (int i = 1; i < lines.size(); i++) { int colon_pos = lines[i].find(":"); if (colon_pos >= 0) { String key = lines[i].substr(0, colon_pos).strip_edges().to_lower(); String value = lines[i].substr(colon_pos + 1).strip_edges(); req.headers[key] = value; } } return req; } void AgentServer::_send_response(Ref p_peer, const HTTPResponse &p_response) { String status = _status_text(p_response.status_code); String header = vformat("HTTP/1.1 %d %s\r\n", p_response.status_code, status); header += vformat("Content-Type: %s\r\n", p_response.content_type); header += "Access-Control-Allow-Origin: *\r\n"; header += "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n"; header += "Access-Control-Allow-Headers: Content-Type, Authorization\r\n"; header += "Connection: close\r\n"; if (p_response.is_binary) { header += vformat("Content-Length: %d\r\n", p_response.binary_body.size()); header += "\r\n"; CharString header_utf8 = header.utf8(); p_peer->put_data((const uint8_t *)header_utf8.ptr(), header_utf8.length()); if (p_response.binary_body.size() > 0) { p_peer->put_data(p_response.binary_body.ptr(), p_response.binary_body.size()); } } else { CharString body_utf8 = p_response.body.utf8(); header += vformat("Content-Length: %d\r\n", body_utf8.length()); header += "\r\n"; CharString header_utf8 = header.utf8(); p_peer->put_data((const uint8_t *)header_utf8.ptr(), header_utf8.length()); if (body_utf8.length() > 0) { p_peer->put_data((const uint8_t *)body_utf8.ptr(), body_utf8.length()); } } } void AgentServer::_send_sse_headers(Ref p_peer) { String header = "HTTP/1.1 200 OK\r\n"; header += "Content-Type: text/event-stream\r\n"; header += "Cache-Control: no-cache\r\n"; header += "Access-Control-Allow-Origin: *\r\n"; header += "Connection: keep-alive\r\n"; header += "\r\n"; CharString header_utf8 = header.utf8(); p_peer->put_data((const uint8_t *)header_utf8.ptr(), header_utf8.length()); } // --- Auth --- bool AgentServer::_check_auth(const HTTPRequest &p_request) { if (auth_token.is_empty()) { return true; // No auth configured. } if (p_request.headers.has("authorization")) { String auth = p_request.headers["authorization"]; if (auth.begins_with("Bearer ")) { return auth.substr(7) == auth_token; } } if (p_request.query_params.has("token")) { return p_request.query_params["token"] == auth_token; } return false; } // --- Route Dispatch --- AgentServer::HTTPResponse AgentServer::_handle_request(const HTTPRequest &p_request) { // CORS preflight. if (p_request.method == "OPTIONS") { HTTPResponse resp; resp.status_code = 204; resp.body = ""; return resp; } // Auth check. if (!_check_auth(p_request)) { HTTPResponse resp; resp.status_code = 401; resp.body = _json_error(401, "Unauthorized"); return resp; } // Route dispatch. String path = p_request.path; if (path == "/health") { return _handle_health(p_request); } if (path == "/screenshot") { return _handle_screenshot(p_request); } if (path == "/depth") { return _handle_depth(p_request); } if (path == "/segmentation") { return _handle_segmentation(p_request); } if (path == "/state") { return _handle_state(p_request); } if (path.begins_with("/nodes/")) { String node_path = path.substr(7); // Remove "/nodes/" return _handle_nodes(p_request, node_path); } if (path == "/command" && p_request.method == "POST") { return _handle_command(p_request); } if (path == "/command/batch" && p_request.method == "POST") { return _handle_command_batch(p_request); } if (path == "/commands") { return _handle_commands_list(p_request); } if (path == "/console" && p_request.method == "POST") { return _handle_console(p_request); } if (path.begins_with("/input/")) { return _handle_input(p_request); } if (path == "/logs") { return _handle_logs(p_request); } if (path == "/perf") { return _handle_perf(p_request); } if (path == "/events/history") { return _handle_events_history(p_request); } if (path == "/world/physics") { return _handle_world_physics(p_request); } if (path == "/world/raycast" && p_request.method == "POST") { return _handle_world_raycast(p_request); } if (path == "/world/spawn" && p_request.method == "POST") { return _handle_world_spawn(p_request); } if (path == "/world/destroy" && p_request.method == "POST") { return _handle_world_destroy(p_request); } if (path == "/world/navmesh") { return _handle_world_navmesh(p_request); } if (path == "/save" && p_request.method == "POST") { return _handle_save(p_request); } if (path == "/load" && p_request.method == "POST") { return _handle_load(p_request); } if (path == "/saves") { return _handle_saves(p_request); } if (path == "/config") { return _handle_config(p_request); } if (path == "/assets") { return _handle_assets(p_request); } if (path == "/assets/reload" && p_request.method == "POST") { return _handle_assets_reload(p_request); } if (path == "/vision/boxes") { return _handle_vision_boxes(p_request); } if (path == "/vision/minimap") { return _handle_vision_minimap(p_request); } // 404. HTTPResponse resp; resp.status_code = 404; resp.body = _json_error(404, vformat("Unknown endpoint: %s %s", p_request.method, path)); return resp; } // --- Main Thread Poll --- void AgentServer::poll() { if (!running) { return; } // Handle pending screenshot request. { MutexLock lock(screenshot_mutex); if (screenshot_request.pending && !screenshot_request.ready) { SceneTree *tree = SceneTree::get_singleton(); if (tree) { Viewport *viewport = tree->get_root(); if (viewport) { Ref img = viewport->get_texture()->get_image(); if (img.is_valid()) { // Resize if requested. if (screenshot_request.width > 0 && screenshot_request.height > 0) { img->resize(screenshot_request.width, screenshot_request.height, Image::INTERPOLATE_BILINEAR); } else if (screenshot_request.width > 0) { float aspect = (float)img->get_height() / (float)img->get_width(); img->resize(screenshot_request.width, (int)(screenshot_request.width * aspect), Image::INTERPOLATE_BILINEAR); } if (screenshot_request.format == "jpg" || screenshot_request.format == "jpeg") { screenshot_request.result_data = img->save_jpg_to_buffer((float)screenshot_request.quality / 100.0f); screenshot_request.result_content_type = "image/jpeg"; } else { screenshot_request.result_data = img->save_png_to_buffer(); screenshot_request.result_content_type = "image/png"; } screenshot_request.ready = true; } } } } } // Handle pending depth buffer request. { MutexLock lock(depth_mutex); if (depth_request.pending && !depth_request.ready) { SceneTree *tree = SceneTree::get_singleton(); if (tree) { Viewport *viewport = tree->get_root(); if (viewport) { // Depth buffer access via rendering server. RID viewport_rid = viewport->get_viewport_rid(); RID texture_rid = RenderingServer::get_singleton()->viewport_get_texture(viewport_rid); Ref img = RenderingServer::get_singleton()->texture_2d_get(texture_rid); if (img.is_valid()) { // Convert to grayscale depth visualization. depth_request.result_data = img->save_png_to_buffer(); depth_request.ready = true; } } } } } // Handle pending command execution. { MutexLock lock(command_mutex); if (command_request.pending && !command_request.ready) { SceneTree *tree = SceneTree::get_singleton(); if (tree) { // Execute GDScript expression. Ref expr; expr.instantiate(); Error err = expr->parse(command_request.expression); if (err != OK) { command_request.result = vformat("Parse error: %s", expr->get_error_text()); command_request.success = false; } else { Variant result = expr->execute(Array(), tree->get_root()); if (expr->has_execute_failed()) { command_request.result = vformat("Execution error: %s", expr->get_error_text()); command_request.success = false; } else { command_request.result = String(result); command_request.success = true; } } command_request.ready = true; } } } } // --- SSE Push --- void AgentServer::push_sse(const String &p_stream_type, const String &p_event, const String &p_data) { MutexLock lock(sse_mutex); String sse_msg = vformat("event: %s\ndata: %s\n\n", p_event, p_data); CharString sse_utf8 = sse_msg.utf8(); for (int i = sse_clients.size() - 1; i >= 0; i--) { if (sse_clients[i].stream_type != p_stream_type) { continue; } if (sse_clients[i].peer.is_null() || sse_clients[i].peer->get_status() != StreamPeerTCP::STATUS_CONNECTED) { sse_clients.remove_at(i); continue; } sse_clients[i].peer->put_data((const uint8_t *)sse_utf8.ptr(), sse_utf8.length()); } } // --- Endpoint Implementations --- AgentServer::HTTPResponse AgentServer::_handle_health(const HTTPRequest &p_request) { Dictionary dict; dict["status"] = "ok"; dict["port"] = port; dict["uptime_msec"] = OS::get_singleton()->get_ticks_msec(); dict["engine_version"] = Engine::get_singleton()->get_version_info(); dict["project_name"] = GLOBAL_GET("application/config/name"); dict["agent_api_version"] = "1.0.0"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_screenshot(const HTTPRequest &p_request) { // Set up screenshot request for main thread. { MutexLock lock(screenshot_mutex); screenshot_request.pending = true; screenshot_request.ready = false; screenshot_request.width = p_request.query_params.has("width") ? p_request.query_params["width"].to_int() : 0; screenshot_request.height = p_request.query_params.has("height") ? p_request.query_params["height"].to_int() : 0; screenshot_request.format = p_request.query_params.has("format") ? p_request.query_params["format"] : "png"; screenshot_request.quality = p_request.query_params.has("quality") ? p_request.query_params["quality"].to_int() : 90; screenshot_request.camera_name = p_request.query_params.has("camera") ? p_request.query_params["camera"] : ""; screenshot_request.annotate = p_request.query_params.has("annotate") && p_request.query_params["annotate"] == "true"; screenshot_request.diff = p_request.query_params.has("diff") && p_request.query_params["diff"] == "previous"; } // Wait for main thread to capture (up to 2 seconds). uint64_t start = OS::get_singleton()->get_ticks_msec(); while (OS::get_singleton()->get_ticks_msec() - start < 2000) { { MutexLock lock(screenshot_mutex); if (screenshot_request.ready) { HTTPResponse resp; resp.is_binary = true; resp.binary_body = screenshot_request.result_data; resp.content_type = screenshot_request.result_content_type; screenshot_request.pending = false; screenshot_request.ready = false; return resp; } } OS::get_singleton()->delay_usec(1000); } // Timeout. { MutexLock lock(screenshot_mutex); screenshot_request.pending = false; screenshot_request.ready = false; } HTTPResponse resp; resp.status_code = 504; resp.body = _json_error(504, "Screenshot capture timed out"); return resp; } AgentServer::HTTPResponse AgentServer::_handle_depth(const HTTPRequest &p_request) { { MutexLock lock(depth_mutex); depth_request.pending = true; depth_request.ready = false; } uint64_t start = OS::get_singleton()->get_ticks_msec(); while (OS::get_singleton()->get_ticks_msec() - start < 2000) { { MutexLock lock(depth_mutex); if (depth_request.ready) { HTTPResponse resp; resp.is_binary = true; resp.binary_body = depth_request.result_data; resp.content_type = "image/png"; depth_request.pending = false; depth_request.ready = false; return resp; } } OS::get_singleton()->delay_usec(1000); } { MutexLock lock(depth_mutex); depth_request.pending = false; } HTTPResponse resp; resp.status_code = 504; resp.body = _json_error(504, "Depth capture timed out"); return resp; } AgentServer::HTTPResponse AgentServer::_handle_segmentation(const HTTPRequest &p_request) { // Segmentation requires a special rendering pass — stub for now, // will be implemented in agent_vision module. HTTPResponse resp; resp.status_code = 501; resp.body = _json_error(501, "Segmentation requires agent_vision module"); return resp; } AgentServer::HTTPResponse AgentServer::_handle_state(const HTTPRequest &p_request) { Dictionary dict; SceneTree *tree = SceneTree::get_singleton(); if (!tree) { dict["error"] = "No scene tree"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } int max_depth = p_request.query_params.has("depth") ? p_request.query_params["depth"].to_int() : 4; String filter = p_request.query_params.has("filter") ? p_request.query_params["filter"] : ""; Window *root = tree->get_root(); if (root) { dict["scene_tree"] = _node_to_dict(root, 0, max_depth, filter); } dict["fps"] = Engine::get_singleton()->get_frames_per_second(); dict["physics_ticks"] = Engine::get_singleton()->get_physics_frames(); dict["process_frames"] = Engine::get_singleton()->get_process_frames(); dict["time_scale"] = Engine::get_singleton()->get_time_scale(); Performance *perf = Performance::get_singleton(); if (perf) { Dictionary perf_dict; perf_dict["fps"] = perf->get_monitor(Performance::TIME_FPS); perf_dict["process_time"] = perf->get_monitor(Performance::TIME_PROCESS); perf_dict["physics_time"] = perf->get_monitor(Performance::TIME_PHYSICS_PROCESS); perf_dict["render_objects"] = perf->get_monitor(Performance::RENDER_TOTAL_OBJECTS_IN_FRAME); perf_dict["render_draw_calls"] = perf->get_monitor(Performance::RENDER_TOTAL_DRAW_CALLS_IN_FRAME); perf_dict["memory_static"] = perf->get_monitor(Performance::MEMORY_STATIC); dict["performance"] = perf_dict; } HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_nodes(const HTTPRequest &p_request, const String &p_node_path) { SceneTree *tree = SceneTree::get_singleton(); if (!tree || !tree->get_root()) { HTTPResponse resp; resp.status_code = 500; resp.body = _json_error(500, "No scene tree"); return resp; } // URL decode and find node. String decoded_path = p_node_path.uri_decode(); Node *node = tree->get_root()->get_node_or_null(NodePath(decoded_path)); if (!node) { HTTPResponse resp; resp.status_code = 404; resp.body = _json_error(404, vformat("Node not found: %s", decoded_path)); return resp; } // If path ends with /properties, return all properties. if (decoded_path.ends_with("/properties") || p_request.path.ends_with("/properties")) { if (p_request.method == "PUT") { // Set properties from body. Variant body_var = JSON::parse_string(p_request.body); if (body_var.get_type() == Variant::DICTIONARY) { Dictionary props = body_var; for (const Variant &key : props.keys()) { node->set(StringName(String(key)), props[key]); } } } Dictionary props_dict; List properties; node->get_property_list(&properties); for (const PropertyInfo &prop : properties) { if (prop.usage & PROPERTY_USAGE_EDITOR) { props_dict[prop.name] = node->get(prop.name); } } HTTPResponse resp; resp.body = _json_response(props_dict); return resp; } HTTPResponse resp; resp.body = _json_response(_node_to_dict(node, 0, 2, "")); return resp; } AgentServer::HTTPResponse AgentServer::_handle_command(const HTTPRequest &p_request) { Variant body_var = JSON::parse_string(p_request.body); String expression; if (body_var.get_type() == Variant::DICTIONARY) { Dictionary body = body_var; if (body.has("command")) { expression = String(body["command"]); } else if (body.has("expression")) { expression = String(body["expression"]); } } else { expression = p_request.body; } if (expression.is_empty()) { HTTPResponse resp; resp.status_code = 400; resp.body = _json_error(400, "Missing 'command' or 'expression' in body"); return resp; } // Queue for main thread execution. { MutexLock lock(command_mutex); command_request.pending = true; command_request.ready = false; command_request.expression = expression; } // Wait for result. uint64_t start = OS::get_singleton()->get_ticks_msec(); while (OS::get_singleton()->get_ticks_msec() - start < 5000) { { MutexLock lock(command_mutex); if (command_request.ready) { Dictionary dict; dict["success"] = command_request.success; dict["result"] = command_request.result; dict["expression"] = expression; command_request.pending = false; command_request.ready = false; HTTPResponse resp; resp.body = _json_response(dict); return resp; } } OS::get_singleton()->delay_usec(1000); } { MutexLock lock(command_mutex); command_request.pending = false; } HTTPResponse resp; resp.status_code = 504; resp.body = _json_error(504, "Command execution timed out"); return resp; } AgentServer::HTTPResponse AgentServer::_handle_command_batch(const HTTPRequest &p_request) { // Stub — executes commands sequentially. Variant body_var = JSON::parse_string(p_request.body); Array results; if (body_var.get_type() == Variant::ARRAY) { Array commands = body_var; for (int i = 0; i < commands.size(); i++) { // Reuse single command path. HTTPRequest sub_req = p_request; Dictionary cmd_dict; cmd_dict["command"] = commands[i]; sub_req.body = JSON::stringify(cmd_dict); HTTPResponse sub_resp = _handle_command(sub_req); Variant result_var = JSON::parse_string(sub_resp.body); results.push_back(result_var); } } Dictionary dict; dict["results"] = results; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_input(const HTTPRequest &p_request) { String sub_path = p_request.path.substr(7); // Remove "/input/" Variant body_var = JSON::parse_string(p_request.body); Dictionary body = (body_var.get_type() == Variant::DICTIONARY) ? Dictionary(body_var) : Dictionary(); Dictionary result; result["success"] = true; if (sub_path == "key") { // Inject key event. Actual injection must happen on main thread. result["action"] = "key_injected"; result["key"] = body.has("key") ? body["key"] : ""; } else if (sub_path == "mouse") { result["action"] = "mouse_injected"; } else if (sub_path == "action") { result["action"] = "action_injected"; result["input_action"] = body.has("action") ? body["action"] : ""; } else if (sub_path == "text") { result["action"] = "text_injected"; } else { HTTPResponse resp; resp.status_code = 404; resp.body = _json_error(404, vformat("Unknown input type: %s", sub_path)); return resp; } HTTPResponse resp; resp.body = _json_response(result); return resp; } AgentServer::HTTPResponse AgentServer::_handle_logs(const HTTPRequest &p_request) { // Delegates to AgentLog module if available. // Stub returns empty for now. Dictionary dict; dict["entries"] = Array(); dict["message"] = "Enable agent_log module for structured logging"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_perf(const HTTPRequest &p_request) { Dictionary dict; Performance *perf = Performance::get_singleton(); if (perf) { dict["fps"] = perf->get_monitor(Performance::TIME_FPS); dict["frame_time"] = perf->get_monitor(Performance::TIME_PROCESS); dict["physics_time"] = perf->get_monitor(Performance::TIME_PHYSICS_PROCESS); dict["render_objects"] = perf->get_monitor(Performance::RENDER_TOTAL_OBJECTS_IN_FRAME); dict["render_draw_calls"] = perf->get_monitor(Performance::RENDER_TOTAL_DRAW_CALLS_IN_FRAME); dict["render_primitives"] = perf->get_monitor(Performance::RENDER_TOTAL_PRIMITIVES_IN_FRAME); dict["memory_static"] = perf->get_monitor(Performance::MEMORY_STATIC); dict["memory_static_max"] = perf->get_monitor(Performance::MEMORY_STATIC_MAX); dict["memory_message_buffer_max"] = perf->get_monitor(Performance::MEMORY_MESSAGE_BUFFER_MAX); dict["object_count"] = perf->get_monitor(Performance::OBJECT_COUNT); dict["object_resource_count"] = perf->get_monitor(Performance::OBJECT_RESOURCE_COUNT); dict["object_node_count"] = perf->get_monitor(Performance::OBJECT_NODE_COUNT); dict["object_orphan_node_count"] = perf->get_monitor(Performance::OBJECT_ORPHAN_NODE_COUNT); dict["physics_2d_active_objects"] = perf->get_monitor(Performance::PHYSICS_2D_ACTIVE_OBJECTS); dict["physics_3d_active_objects"] = perf->get_monitor(Performance::PHYSICS_3D_ACTIVE_OBJECTS); } HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_events_history(const HTTPRequest &p_request) { // Delegates to AgentEvents module. Dictionary dict; dict["events"] = Array(); dict["message"] = "Enable agent_events module for event tracking"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_world_raycast(const HTTPRequest &p_request) { Variant body_var = JSON::parse_string(p_request.body); Dictionary body = (body_var.get_type() == Variant::DICTIONARY) ? Dictionary(body_var) : Dictionary(); Dictionary result; result["success"] = false; result["message"] = "Raycast requires main thread execution — queued"; // Actual raycast will be queued to main thread similar to command execution. // Full implementation needs PhysicsDirectSpaceState3D access. HTTPResponse resp; resp.body = _json_response(result); return resp; } AgentServer::HTTPResponse AgentServer::_handle_world_spawn(const HTTPRequest &p_request) { Variant body_var = JSON::parse_string(p_request.body); Dictionary body = (body_var.get_type() == Variant::DICTIONARY) ? Dictionary(body_var) : Dictionary(); Dictionary result; result["message"] = "Spawn queued for main thread"; result["scene"] = body.has("scene") ? body["scene"] : ""; HTTPResponse resp; resp.body = _json_response(result); return resp; } AgentServer::HTTPResponse AgentServer::_handle_world_destroy(const HTTPRequest &p_request) { Variant body_var = JSON::parse_string(p_request.body); Dictionary body = (body_var.get_type() == Variant::DICTIONARY) ? Dictionary(body_var) : Dictionary(); Dictionary result; result["message"] = "Destroy queued for main thread"; result["node_path"] = body.has("node_path") ? body["node_path"] : ""; HTTPResponse resp; resp.body = _json_response(result); return resp; } AgentServer::HTTPResponse AgentServer::_handle_world_physics(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Physics state query"; Performance *perf = Performance::get_singleton(); if (perf) { dict["active_objects_3d"] = perf->get_monitor(Performance::PHYSICS_3D_ACTIVE_OBJECTS); dict["collision_pairs_3d"] = perf->get_monitor(Performance::PHYSICS_3D_COLLISION_PAIRS); dict["island_count_3d"] = perf->get_monitor(Performance::PHYSICS_3D_ISLAND_COUNT); dict["active_objects_2d"] = perf->get_monitor(Performance::PHYSICS_2D_ACTIVE_OBJECTS); dict["collision_pairs_2d"] = perf->get_monitor(Performance::PHYSICS_2D_COLLISION_PAIRS); dict["island_count_2d"] = perf->get_monitor(Performance::PHYSICS_2D_ISLAND_COUNT); } HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_world_navmesh(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Navigation mesh data"; // NavigationServer3D query would go here. HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_save(const HTTPRequest &p_request) { // Delegates to agent_replay module. Dictionary dict; dict["message"] = "Enable agent_replay module for save/load"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_load(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Enable agent_replay module for save/load"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_saves(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Enable agent_replay module for save/load"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_config(const HTTPRequest &p_request) { if (p_request.method == "PUT") { // Update config at runtime. Variant body_var = JSON::parse_string(p_request.body); // Process config changes (logging level, etc.). } Dictionary dict; dict["port"] = port; dict["auth_enabled"] = !auth_token.is_empty(); dict["modules"] = Array(); // Will be populated by module registry. HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_console(const HTTPRequest &p_request) { // Delegates to agent_console module. Dictionary dict; dict["message"] = "Enable agent_console module for game console"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_commands_list(const HTTPRequest &p_request) { Dictionary dict; dict["commands"] = Array(); dict["message"] = "Enable agent_console module for registered commands"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_assets(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Asset listing"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_assets_reload(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Asset reload requested"; HTTPResponse resp; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_vision_boxes(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Enable agent_vision module for bounding boxes"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } AgentServer::HTTPResponse AgentServer::_handle_vision_minimap(const HTTPRequest &p_request) { Dictionary dict; dict["message"] = "Enable agent_vision module for minimap"; HTTPResponse resp; resp.status_code = 501; resp.body = _json_response(dict); return resp; } // --- Utilities --- Dictionary AgentServer::_node_to_dict(Node *p_node, int p_depth, int p_max_depth, const String &p_filter) { Dictionary dict; dict["name"] = p_node->get_name(); dict["class"] = p_node->get_class(); dict["path"] = String(p_node->get_path()); // Include position for spatial nodes. if (p_node->has_method("get_global_position")) { dict["global_position"] = p_node->call("get_global_position"); } if (p_node->has_method("get_global_transform")) { dict["global_transform"] = p_node->call("get_global_transform"); } // Children. if (p_depth < p_max_depth) { Array children; for (int i = 0; i < p_node->get_child_count(); i++) { Node *child = p_node->get_child(i); if (!p_filter.is_empty() && child->get_class().findn(p_filter) < 0 && String(child->get_name()).findn(p_filter) < 0) { continue; } children.push_back(_node_to_dict(child, p_depth + 1, p_max_depth, p_filter)); } dict["children"] = children; dict["child_count"] = p_node->get_child_count(); } else { dict["child_count"] = p_node->get_child_count(); } return dict; } String AgentServer::_status_text(int p_code) { switch (p_code) { case 200: return "OK"; case 201: return "Created"; case 204: return "No Content"; case 400: return "Bad Request"; case 401: return "Unauthorized"; case 404: return "Not Found"; case 500: return "Internal Server Error"; case 501: return "Not Implemented"; case 504: return "Gateway Timeout"; default: return "Unknown"; } } String AgentServer::_json_response(const Dictionary &p_dict) { return JSON::stringify(p_dict, "\t"); } String AgentServer::_json_error(int p_code, const String &p_message) { Dictionary dict; dict["error"] = p_message; dict["code"] = p_code; return JSON::stringify(dict, "\t"); }