7fb933a4b9
- Call check_cmdline_and_start() from register_types so --agent-api flag works - Connect poll() to SceneTree::process_frame via deferred callable so screenshot/depth/command requests get serviced on the main thread - Default port changed to 4329 to avoid conflict with C# AgentServer on 4328 - Clean disconnect on stop() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1209 lines
36 KiB
C++
1209 lines
36 KiB
C++
#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<String> 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<AgentServer *>(p_userdata);
|
|
self->_server_loop();
|
|
}
|
|
|
|
void AgentServer::_server_loop() {
|
|
while (running) {
|
|
if (tcp_server->is_connection_available()) {
|
|
Ref<StreamPeerTCP> 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<StreamPeerTCP> 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<uint8_t> 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<String> lines = header_section.split("\r\n");
|
|
if (lines.size() == 0) {
|
|
return req;
|
|
}
|
|
|
|
// Parse request line: METHOD /path?query HTTP/1.1
|
|
Vector<String> 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<String> 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<StreamPeerTCP> 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<StreamPeerTCP> 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<Image> 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<Image> 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<Expression> 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<PropertyInfo> 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");
|
|
}
|