initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled

This commit is contained in:
2025-09-16 20:46:46 -04:00
commit 9d30169a8d
13378 changed files with 7050105 additions and 0 deletions

8
editor/debugger/SCsub Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python
from misc.utility.scons_hints import *
Import("env")
env.add_source_files(env.editor_sources, "*.cpp")
SConscript("debug_adapter/SCsub")

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python
from misc.utility.scons_hints import *
Import("env")
env.add_source_files(env.editor_sources, "*.cpp")

View File

@@ -0,0 +1,650 @@
/**************************************************************************/
/* debug_adapter_parser.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "debug_adapter_parser.h"
#include "editor/debugger/debug_adapter/debug_adapter_types.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/export/editor_export_platform.h"
#include "editor/run/editor_run_bar.h"
#include "editor/script/script_editor_plugin.h"
void DebugAdapterParser::_bind_methods() {
// Requests
ClassDB::bind_method(D_METHOD("req_initialize", "params"), &DebugAdapterParser::req_initialize);
ClassDB::bind_method(D_METHOD("req_disconnect", "params"), &DebugAdapterParser::req_disconnect);
ClassDB::bind_method(D_METHOD("req_launch", "params"), &DebugAdapterParser::req_launch);
ClassDB::bind_method(D_METHOD("req_attach", "params"), &DebugAdapterParser::req_attach);
ClassDB::bind_method(D_METHOD("req_restart", "params"), &DebugAdapterParser::req_restart);
ClassDB::bind_method(D_METHOD("req_terminate", "params"), &DebugAdapterParser::req_terminate);
ClassDB::bind_method(D_METHOD("req_configurationDone", "params"), &DebugAdapterParser::req_configurationDone);
ClassDB::bind_method(D_METHOD("req_pause", "params"), &DebugAdapterParser::req_pause);
ClassDB::bind_method(D_METHOD("req_continue", "params"), &DebugAdapterParser::req_continue);
ClassDB::bind_method(D_METHOD("req_threads", "params"), &DebugAdapterParser::req_threads);
ClassDB::bind_method(D_METHOD("req_stackTrace", "params"), &DebugAdapterParser::req_stackTrace);
ClassDB::bind_method(D_METHOD("req_setBreakpoints", "params"), &DebugAdapterParser::req_setBreakpoints);
ClassDB::bind_method(D_METHOD("req_breakpointLocations", "params"), &DebugAdapterParser::req_breakpointLocations);
ClassDB::bind_method(D_METHOD("req_scopes", "params"), &DebugAdapterParser::req_scopes);
ClassDB::bind_method(D_METHOD("req_variables", "params"), &DebugAdapterParser::req_variables);
ClassDB::bind_method(D_METHOD("req_next", "params"), &DebugAdapterParser::req_next);
ClassDB::bind_method(D_METHOD("req_stepIn", "params"), &DebugAdapterParser::req_stepIn);
ClassDB::bind_method(D_METHOD("req_evaluate", "params"), &DebugAdapterParser::req_evaluate);
ClassDB::bind_method(D_METHOD("req_godot/put_msg", "params"), &DebugAdapterParser::req_godot_put_msg);
}
Dictionary DebugAdapterParser::prepare_base_event() const {
Dictionary event;
event["type"] = "event";
return event;
}
Dictionary DebugAdapterParser::prepare_success_response(const Dictionary &p_params) const {
Dictionary response;
response["type"] = "response";
response["request_seq"] = p_params["seq"];
response["command"] = p_params["command"];
response["success"] = true;
return response;
}
Dictionary DebugAdapterParser::prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables) const {
Dictionary response, body;
response["type"] = "response";
response["request_seq"] = p_params["seq"];
response["command"] = p_params["command"];
response["success"] = false;
response["body"] = body;
DAP::Message message;
String error, error_desc;
switch (err_type) {
case DAP::ErrorType::WRONG_PATH:
error = "wrong_path";
error_desc = "The editor and client are working on different paths; the client is on \"{clientPath}\", but the editor is on \"{editorPath}\"";
break;
case DAP::ErrorType::NOT_RUNNING:
error = "not_running";
error_desc = "Can't attach to a running session since there isn't one.";
break;
case DAP::ErrorType::TIMEOUT:
error = "timeout";
error_desc = "Timeout reached while processing a request.";
break;
case DAP::ErrorType::UNKNOWN_PLATFORM:
error = "unknown_platform";
error_desc = "The specified platform is unknown.";
break;
case DAP::ErrorType::MISSING_DEVICE:
error = "missing_device";
error_desc = "There's no connected device with specified id.";
break;
case DAP::ErrorType::UNKNOWN:
default:
error = "unknown";
error_desc = "An unknown error has occurred when processing the request.";
break;
}
message.id = err_type;
message.format = error_desc;
message.variables = variables;
response["message"] = error;
body["error"] = message.to_json();
return response;
}
Dictionary DebugAdapterParser::req_initialize(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params);
Dictionary args = p_params["arguments"];
Ref<DAPeer> peer = DebugAdapterProtocol::get_singleton()->get_current_peer();
peer->linesStartAt1 = args.get("linesStartAt1", false);
peer->columnsStartAt1 = args.get("columnsStartAt1", false);
peer->supportsVariableType = args.get("supportsVariableType", false);
peer->supportsInvalidatedEvent = args.get("supportsInvalidatedEvent", false);
DAP::Capabilities caps;
response["body"] = caps.to_json();
DebugAdapterProtocol::get_singleton()->notify_initialized();
if (DebugAdapterProtocol::get_singleton()->_sync_breakpoints) {
// Send all current breakpoints
List<String> breakpoints;
ScriptEditor::get_singleton()->get_breakpoints(&breakpoints);
for (const String &breakpoint : breakpoints) {
String path = breakpoint.left(breakpoint.find_char(':', 6)); // Skip initial part of path, aka "res://"
int line = breakpoint.substr(path.size()).to_int();
DebugAdapterProtocol::get_singleton()->on_debug_breakpoint_toggled(path, line, true);
}
} else {
// Remove all current breakpoints
EditorDebuggerNode::get_singleton()->get_default_debugger()->_clear_breakpoints();
}
return response;
}
Dictionary DebugAdapterParser::req_disconnect(const Dictionary &p_params) const {
if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->attached) {
EditorRunBar::get_singleton()->stop_playing();
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_launch(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
if (args.has("project") && !is_valid_path(args["project"])) {
Dictionary variables;
variables["clientPath"] = args["project"];
variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path();
return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables);
}
if (args.has("godot/custom_data")) {
DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsCustomData = args["godot/custom_data"];
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->pending_launch = p_params;
return Dictionary();
}
Dictionary DebugAdapterParser::_launch_process(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
ScriptEditorDebugger *dbg = EditorDebuggerNode::get_singleton()->get_default_debugger();
if ((bool)args["noDebug"] != dbg->is_skip_breakpoints()) {
dbg->debug_skip_breakpoints();
}
String platform_string = args.get("platform", "host");
if (platform_string == "host") {
EditorRunBar::get_singleton()->play_main_scene();
} else {
int device = args.get("device", -1);
int idx = -1;
if (platform_string == "android") {
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
if (EditorExport::get_singleton()->get_export_platform(i)->get_name() == "Android") {
idx = i;
break;
}
}
} else if (platform_string == "web") {
for (int i = 0; i < EditorExport::get_singleton()->get_export_platform_count(); i++) {
if (EditorExport::get_singleton()->get_export_platform(i)->get_name() == "Web") {
idx = i;
break;
}
}
}
if (idx == -1) {
return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN_PLATFORM);
}
EditorRunBar *run_bar = EditorRunBar::get_singleton();
Error err = platform_string == "android" ? run_bar->start_native_device(device * 10000 + idx) : run_bar->start_native_device(idx);
if (err) {
if (err == ERR_INVALID_PARAMETER && platform_string == "android") {
return prepare_error_response(p_params, DAP::ErrorType::MISSING_DEVICE);
} else {
return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN);
}
}
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->attached = false;
DebugAdapterProtocol::get_singleton()->notify_process();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_attach(const Dictionary &p_params) const {
ScriptEditorDebugger *dbg = EditorDebuggerNode::get_singleton()->get_default_debugger();
if (!dbg->is_session_active()) {
return prepare_error_response(p_params, DAP::ErrorType::NOT_RUNNING);
}
DebugAdapterProtocol::get_singleton()->get_current_peer()->attached = true;
DebugAdapterProtocol::get_singleton()->notify_process();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_restart(const Dictionary &p_params) const {
// Extract embedded "arguments" so it can be given to req_launch/req_attach
Dictionary params = p_params, args;
args = params["arguments"];
args = args["arguments"];
params["arguments"] = args;
Dictionary response = DebugAdapterProtocol::get_singleton()->get_current_peer()->attached ? req_attach(params) : _launch_process(params);
if (!response["success"]) {
response["command"] = p_params["command"];
return response;
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_terminate(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->stop_playing();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_configurationDone(const Dictionary &p_params) const {
Ref<DAPeer> peer = DebugAdapterProtocol::get_singleton()->get_current_peer();
if (!peer->pending_launch.is_empty()) {
peer->res_queue.push_back(_launch_process(peer->pending_launch));
peer->pending_launch.clear();
}
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_pause(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(true);
EditorDebuggerNode::get_singleton()->_paused();
DebugAdapterProtocol::get_singleton()->notify_stopped_paused();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_continue(const Dictionary &p_params) const {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(false);
EditorDebuggerNode::get_singleton()->_paused();
DebugAdapterProtocol::get_singleton()->notify_continued();
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_threads(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
DAP::Thread thread;
thread.id = 1; // Hardcoded because Godot only supports debugging one thread at the moment
thread.name = "Main";
Array arr = { thread.to_json() };
body["threads"] = arr;
return response;
}
Dictionary DebugAdapterParser::req_stackTrace(const Dictionary &p_params) const {
if (DebugAdapterProtocol::get_singleton()->_processing_stackdump) {
return Dictionary();
}
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1;
bool columns_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->columnsStartAt1;
Array arr;
DebugAdapterProtocol *dap = DebugAdapterProtocol::get_singleton();
for (DAP::StackFrame sf : dap->stackframe_list) {
if (!lines_at_one) {
sf.line--;
}
if (!columns_at_one) {
sf.column--;
}
arr.push_back(sf.to_json());
}
body["stackFrames"] = arr;
return response;
}
Dictionary DebugAdapterParser::req_setBreakpoints(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
DAP::Source source;
source.from_json(args["source"]);
bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1;
if (!is_valid_path(source.path)) {
Dictionary variables;
variables["clientPath"] = source.path;
variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path();
return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables);
}
// If path contains \, it's a Windows path, so we need to convert it to /, and make the drive letter uppercase
if (source.path.contains_char('\\')) {
source.path = source.path.replace_char('\\', '/');
source.path = source.path.substr(0, 1).to_upper() + source.path.substr(1);
}
Array breakpoints = args["breakpoints"], lines;
for (int i = 0; i < breakpoints.size(); i++) {
DAP::SourceBreakpoint breakpoint;
breakpoint.from_json(breakpoints[i]);
lines.push_back(breakpoint.line + !lines_at_one);
}
// Always update the source checksum for the requested path, as it might have been modified externally.
DebugAdapterProtocol::get_singleton()->update_source(source.path);
Array updated_breakpoints = DebugAdapterProtocol::get_singleton()->update_breakpoints(source.path, lines);
body["breakpoints"] = updated_breakpoints;
return response;
}
Dictionary DebugAdapterParser::req_breakpointLocations(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
DAP::BreakpointLocation location;
location.line = args["line"];
if (args.has("endLine")) {
location.endLine = args["endLine"];
}
Array locations = { location.to_json() };
body["breakpoints"] = locations;
return response;
}
Dictionary DebugAdapterParser::req_scopes(const Dictionary &p_params) const {
Dictionary response = prepare_success_response(p_params), body;
response["body"] = body;
Dictionary args = p_params["arguments"];
int frame_id = args["frameId"];
Array scope_list;
HashMap<DebugAdapterProtocol::DAPStackFrameID, Vector<int>>::Iterator E = DebugAdapterProtocol::get_singleton()->scope_list.find(frame_id);
if (E) {
const Vector<int> &scope_ids = E->value;
ERR_FAIL_COND_V(scope_ids.size() != 3, prepare_error_response(p_params, DAP::ErrorType::UNKNOWN));
for (int i = 0; i < 3; ++i) {
DAP::Scope scope;
scope.variablesReference = scope_ids[i];
switch (i) {
case 0:
scope.name = "Locals";
scope.presentationHint = "locals";
break;
case 1:
scope.name = "Members";
scope.presentationHint = "members";
break;
case 2:
scope.name = "Globals";
scope.presentationHint = "globals";
}
scope_list.push_back(scope.to_json());
}
}
EditorDebuggerNode::get_singleton()->get_default_debugger()->request_stack_dump(frame_id);
DebugAdapterProtocol::get_singleton()->_current_frame = frame_id;
body["scopes"] = scope_list;
return response;
}
Dictionary DebugAdapterParser::req_variables(const Dictionary &p_params) const {
// If _remaining_vars > 0, the debuggee is still sending a stack dump to the editor.
if (DebugAdapterProtocol::get_singleton()->_remaining_vars > 0) {
return Dictionary();
}
Dictionary args = p_params["arguments"];
int variable_id = args["variablesReference"];
if (HashMap<int, Array>::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); E) {
Dictionary response = prepare_success_response(p_params);
Dictionary body;
response["body"] = body;
if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsVariableType) {
for (int i = 0; i < E->value.size(); i++) {
Dictionary variable = E->value[i];
variable.erase("type");
}
}
body["variables"] = E ? E->value : Array();
return response;
} else {
// If the requested variable is an object, it needs to be requested from the debuggee.
ObjectID object_id = DebugAdapterProtocol::get_singleton()->search_object_id(variable_id);
if (object_id.is_null()) {
return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN);
}
DebugAdapterProtocol::get_singleton()->request_remote_object(object_id);
}
return Dictionary();
}
Dictionary DebugAdapterParser::req_next(const Dictionary &p_params) const {
EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_next();
DebugAdapterProtocol::get_singleton()->_stepping = true;
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_stepIn(const Dictionary &p_params) const {
EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_step();
DebugAdapterProtocol::get_singleton()->_stepping = true;
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::req_evaluate(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
String expression = args["expression"];
int frame_id = args.has("frameId") ? static_cast<int>(args["frameId"]) : DebugAdapterProtocol::get_singleton()->_current_frame;
if (HashMap<String, DAP::Variable>::Iterator E = DebugAdapterProtocol::get_singleton()->eval_list.find(expression); E) {
Dictionary response = prepare_success_response(p_params);
Dictionary body;
response["body"] = body;
DAP::Variable var = E->value;
body["result"] = var.value;
body["variablesReference"] = var.variablesReference;
// Since an evaluation can alter the state of the debuggee, they are volatile, and should only be used once
DebugAdapterProtocol::get_singleton()->eval_list.erase(E->key);
return response;
} else {
DebugAdapterProtocol::get_singleton()->request_remote_evaluate(expression, frame_id);
}
return Dictionary();
}
Dictionary DebugAdapterParser::req_godot_put_msg(const Dictionary &p_params) const {
Dictionary args = p_params["arguments"];
String msg = args["message"];
Array data = args["data"];
EditorDebuggerNode::get_singleton()->get_default_debugger()->_put_msg(msg, data);
return prepare_success_response(p_params);
}
Dictionary DebugAdapterParser::ev_initialized() const {
Dictionary event = prepare_base_event();
event["event"] = "initialized";
return event;
}
Dictionary DebugAdapterParser::ev_process(const String &p_command) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "process";
event["body"] = body;
body["name"] = OS::get_singleton()->get_executable_path();
body["startMethod"] = p_command;
return event;
}
Dictionary DebugAdapterParser::ev_terminated() const {
Dictionary event = prepare_base_event();
event["event"] = "terminated";
return event;
}
Dictionary DebugAdapterParser::ev_exited(const int &p_exitcode) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "exited";
event["body"] = body;
body["exitCode"] = p_exitcode;
return event;
}
Dictionary DebugAdapterParser::ev_stopped() const {
Dictionary event = prepare_base_event(), body;
event["event"] = "stopped";
event["body"] = body;
body["threadId"] = 1;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_paused() const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "paused";
body["description"] = "Paused";
return event;
}
Dictionary DebugAdapterParser::ev_stopped_exception(const String &p_error) const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "exception";
body["description"] = "Exception";
body["text"] = p_error;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_breakpoint(const int &p_id) const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "breakpoint";
body["description"] = "Breakpoint";
Array breakpoints = { p_id };
body["hitBreakpointIds"] = breakpoints;
return event;
}
Dictionary DebugAdapterParser::ev_stopped_step() const {
Dictionary event = ev_stopped();
Dictionary body = event["body"];
body["reason"] = "step";
body["description"] = "Breakpoint";
return event;
}
Dictionary DebugAdapterParser::ev_continued() const {
Dictionary event = prepare_base_event(), body;
event["event"] = "continued";
event["body"] = body;
body["threadId"] = 1;
return event;
}
Dictionary DebugAdapterParser::ev_output(const String &p_message, RemoteDebugger::MessageType p_type) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "output";
event["body"] = body;
body["category"] = (p_type == RemoteDebugger::MessageType::MESSAGE_TYPE_ERROR) ? "stderr" : "stdout";
body["output"] = p_message + "\r\n";
return event;
}
Dictionary DebugAdapterParser::ev_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "breakpoint";
event["body"] = body;
body["reason"] = p_enabled ? "new" : "removed";
body["breakpoint"] = p_breakpoint.to_json();
return event;
}
Dictionary DebugAdapterParser::ev_custom_data(const String &p_msg, const Array &p_data) const {
Dictionary event = prepare_base_event(), body;
event["event"] = "godot/custom_data";
event["body"] = body;
body["message"] = p_msg;
body["data"] = p_data;
return event;
}

View File

@@ -0,0 +1,104 @@
/**************************************************************************/
/* debug_adapter_parser.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/config/project_settings.h"
#include "core/debugger/remote_debugger.h"
#include "debug_adapter_protocol.h"
#include "debug_adapter_types.h"
struct DAPeer;
class DebugAdapterProtocol;
class DebugAdapterParser : public Object {
GDCLASS(DebugAdapterParser, Object);
private:
friend DebugAdapterProtocol;
_FORCE_INLINE_ bool is_valid_path(const String &p_path) const {
// If path contains \, it's a Windows path, so we need to convert it to /, and check as case-insensitive.
if (p_path.contains_char('\\')) {
String project_path = ProjectSettings::get_singleton()->get_resource_path();
String path = p_path.replace_char('\\', '/');
return path.containsn(project_path);
}
return p_path.begins_with(ProjectSettings::get_singleton()->get_resource_path());
}
protected:
static void _bind_methods();
Dictionary prepare_base_event() const;
Dictionary prepare_success_response(const Dictionary &p_params) const;
Dictionary prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables = Dictionary()) const;
Dictionary ev_stopped() const;
public:
// Requests
Dictionary req_initialize(const Dictionary &p_params) const;
Dictionary req_launch(const Dictionary &p_params) const;
Dictionary req_disconnect(const Dictionary &p_params) const;
Dictionary req_attach(const Dictionary &p_params) const;
Dictionary req_restart(const Dictionary &p_params) const;
Dictionary req_terminate(const Dictionary &p_params) const;
Dictionary req_configurationDone(const Dictionary &p_params) const;
Dictionary req_pause(const Dictionary &p_params) const;
Dictionary req_continue(const Dictionary &p_params) const;
Dictionary req_threads(const Dictionary &p_params) const;
Dictionary req_stackTrace(const Dictionary &p_params) const;
Dictionary req_setBreakpoints(const Dictionary &p_params) const;
Dictionary req_breakpointLocations(const Dictionary &p_params) const;
Dictionary req_scopes(const Dictionary &p_params) const;
Dictionary req_variables(const Dictionary &p_params) const;
Dictionary req_next(const Dictionary &p_params) const;
Dictionary req_stepIn(const Dictionary &p_params) const;
Dictionary req_evaluate(const Dictionary &p_params) const;
Dictionary req_godot_put_msg(const Dictionary &p_params) const;
// Internal requests
Dictionary _launch_process(const Dictionary &p_params) const;
// Events
Dictionary ev_initialized() const;
Dictionary ev_process(const String &p_command) const;
Dictionary ev_terminated() const;
Dictionary ev_exited(const int &p_exitcode) const;
Dictionary ev_stopped_paused() const;
Dictionary ev_stopped_exception(const String &p_error) const;
Dictionary ev_stopped_breakpoint(const int &p_id) const;
Dictionary ev_stopped_step() const;
Dictionary ev_continued() const;
Dictionary ev_output(const String &p_message, RemoteDebugger::MessageType p_type) const;
Dictionary ev_custom_data(const String &p_msg, const Array &p_data) const;
Dictionary ev_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled) const;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,175 @@
/**************************************************************************/
/* debug_adapter_protocol.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/debugger/debugger_marshalls.h"
#include "core/io/stream_peer_tcp.h"
#include "core/io/tcp_server.h"
#include "debug_adapter_parser.h"
#include "debug_adapter_types.h"
#include "scene/debugger/scene_debugger.h"
#define DAP_MAX_BUFFER_SIZE 4194304 // 4MB
#define DAP_MAX_CLIENTS 8
class DebugAdapterParser;
struct DAPeer : RefCounted {
Ref<StreamPeerTCP> connection;
uint8_t req_buf[DAP_MAX_BUFFER_SIZE];
int req_pos = 0;
bool has_header = false;
int content_length = 0;
List<Dictionary> res_queue;
int seq = 0;
uint64_t timestamp = 0;
// Client specific info
bool linesStartAt1 = false;
bool columnsStartAt1 = false;
bool supportsVariableType = false;
bool supportsInvalidatedEvent = false;
bool supportsCustomData = false;
// Internal client info
bool attached = false;
Dictionary pending_launch;
Error handle_data();
Error send_data();
Vector<uint8_t> format_output(const Dictionary &p_params) const;
};
class DebugAdapterProtocol : public Object {
GDCLASS(DebugAdapterProtocol, Object)
friend class DebugAdapterParser;
using DAPVarID = int;
using DAPStackFrameID = int;
private:
static DebugAdapterProtocol *singleton;
DebugAdapterParser *parser = nullptr;
List<Ref<DAPeer>> clients;
Ref<TCPServer> server;
Error on_client_connected();
void on_client_disconnected(const Ref<DAPeer> &p_peer);
void on_debug_paused();
void on_debug_stopped();
void on_debug_output(const String &p_message, int p_type);
void on_debug_breaked(const bool &p_reallydid, const bool &p_can_debug, const String &p_reason, const bool &p_has_stackdump);
void on_debug_breakpoint_toggled(const String &p_path, const int &p_line, const bool &p_enabled);
void on_debug_stack_dump(const Array &p_stack_dump);
void on_debug_stack_frame_vars(const int &p_size);
void on_debug_stack_frame_var(const Array &p_data);
void on_debug_data(const String &p_msg, const Array &p_data);
void reset_current_info();
void reset_ids();
void reset_stack_info();
int parse_variant(const Variant &p_var);
void parse_object(SceneDebuggerObject &p_obj);
const Variant parse_object_variable(const SceneDebuggerObject::SceneDebuggerProperty &p_property);
void parse_evaluation(DebuggerMarshalls::ScriptStackVariable &p_var);
ObjectID search_object_id(DAPVarID p_var_id);
bool request_remote_object(const ObjectID &p_object_id);
bool request_remote_evaluate(const String &p_eval, int p_stack_frame);
const DAP::Source &fetch_source(const String &p_path);
void update_source(const String &p_path);
bool _initialized = false;
bool _processing_breakpoint = false;
bool _stepping = false;
bool _processing_stackdump = false;
int _remaining_vars = 0;
int _current_frame = 0;
uint64_t _request_timeout = 5000;
bool _sync_breakpoints = false;
String _current_request;
Ref<DAPeer> _current_peer;
int breakpoint_id = 0;
int stackframe_id = 0;
DAPVarID variable_id = 0;
List<DAP::Breakpoint> breakpoint_list;
HashMap<String, DAP::Source> breakpoint_source_list;
List<DAP::StackFrame> stackframe_list;
HashMap<DAPStackFrameID, Vector<int>> scope_list;
HashMap<DAPVarID, Array> variable_list;
HashMap<ObjectID, DAPVarID> object_list;
HashSet<ObjectID> object_pending_set;
HashMap<String, DAP::Variable> eval_list;
HashSet<String> eval_pending_list;
public:
friend class DebugAdapterServer;
_FORCE_INLINE_ static DebugAdapterProtocol *get_singleton() { return singleton; }
_FORCE_INLINE_ bool is_active() const { return _initialized && clients.size() > 0; }
bool process_message(const String &p_text);
String get_current_request() const { return _current_request; }
Ref<DAPeer> get_current_peer() const { return _current_peer; }
void notify_initialized();
void notify_process();
void notify_terminated();
void notify_exited(const int &p_exitcode = 0);
void notify_stopped_paused();
void notify_stopped_exception(const String &p_error);
void notify_stopped_breakpoint(const int &p_id);
void notify_stopped_step();
void notify_continued();
void notify_output(const String &p_message, RemoteDebugger::MessageType p_type);
void notify_custom_data(const String &p_msg, const Array &p_data);
void notify_breakpoint(const DAP::Breakpoint &p_breakpoint, const bool &p_enabled);
Array update_breakpoints(const String &p_path, const Array &p_lines);
void poll();
Error start(int p_port, const IPAddress &p_bind_ip);
void stop();
DebugAdapterProtocol();
~DebugAdapterProtocol();
};

View File

@@ -0,0 +1,94 @@
/**************************************************************************/
/* debug_adapter_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "debug_adapter_server.h"
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/settings/editor_settings.h"
int DebugAdapterServer::port_override = -1;
DebugAdapterServer::DebugAdapterServer() {
// TODO: Move to editor_settings.cpp
_EDITOR_DEF("network/debug_adapter/remote_port", remote_port);
_EDITOR_DEF("network/debug_adapter/request_timeout", protocol._request_timeout);
_EDITOR_DEF("network/debug_adapter/sync_breakpoints", protocol._sync_breakpoints);
}
void DebugAdapterServer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
start();
} break;
case NOTIFICATION_EXIT_TREE: {
stop();
} break;
case NOTIFICATION_INTERNAL_PROCESS: {
// The main loop can be run again during request processing, which modifies internal state of the protocol.
// Thus, "polling" is needed to prevent it from parsing other requests while the current one isn't finished.
if (started && !polling) {
polling = true;
protocol.poll();
polling = false;
}
} break;
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (!EditorSettings::get_singleton()->check_changed_settings_in_group("network/debug_adapter")) {
break;
}
protocol._request_timeout = EDITOR_GET("network/debug_adapter/request_timeout");
protocol._sync_breakpoints = EDITOR_GET("network/debug_adapter/sync_breakpoints");
int port = (DebugAdapterServer::port_override > -1) ? DebugAdapterServer::port_override : (int)_EDITOR_GET("network/debug_adapter/remote_port");
if (port != remote_port) {
stop();
start();
}
} break;
}
}
void DebugAdapterServer::start() {
remote_port = (DebugAdapterServer::port_override > -1) ? DebugAdapterServer::port_override : (int)_EDITOR_GET("network/debug_adapter/remote_port");
if (protocol.start(remote_port, IPAddress("127.0.0.1")) == OK) {
EditorNode::get_log()->add_message("--- Debug adapter server started on port " + itos(remote_port) + " ---", EditorLog::MSG_TYPE_EDITOR);
set_process_internal(true);
started = true;
}
}
void DebugAdapterServer::stop() {
protocol.stop();
started = false;
EditorNode::get_log()->add_message("--- Debug adapter server stopped ---", EditorLog::MSG_TYPE_EDITOR);
}

View File

@@ -0,0 +1,55 @@
/**************************************************************************/
/* debug_adapter_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "debug_adapter_protocol.h"
#include "editor/plugins/editor_plugin.h"
class DebugAdapterServer : public EditorPlugin {
GDCLASS(DebugAdapterServer, EditorPlugin);
DebugAdapterProtocol protocol;
int remote_port = 6006;
bool thread_running = false;
bool started = false;
bool polling = false;
static void thread_func(void *p_userdata);
private:
void _notification(int p_what);
public:
static int port_override;
DebugAdapterServer();
void start();
void stop();
};

View File

@@ -0,0 +1,278 @@
/**************************************************************************/
/* debug_adapter_types.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/file_access.h"
namespace DAP {
enum ErrorType {
UNKNOWN,
WRONG_PATH,
NOT_RUNNING,
TIMEOUT,
UNKNOWN_PLATFORM,
MISSING_DEVICE
};
struct Checksum {
String algorithm;
String checksum;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["algorithm"] = algorithm;
dict["checksum"] = checksum;
return dict;
}
};
struct Source {
private:
Array _checksums;
public:
String name;
String path;
void compute_checksums() {
ERR_FAIL_COND(path.is_empty());
_checksums.clear();
// MD5
Checksum md5;
md5.algorithm = "MD5";
md5.checksum = FileAccess::get_md5(path);
// SHA-256
Checksum sha256;
sha256.algorithm = "SHA256";
sha256.checksum = FileAccess::get_sha256(path);
_checksums.push_back(md5.to_json());
_checksums.push_back(sha256.to_json());
}
_FORCE_INLINE_ void from_json(const Dictionary &p_params) {
name = p_params["name"];
path = p_params["path"];
_checksums = p_params["checksums"];
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["path"] = path;
dict["checksums"] = _checksums;
return dict;
}
};
struct Breakpoint {
int id = 0;
bool verified = false;
const Source *source = nullptr;
int line = 0;
Breakpoint() = default; // Empty constructor is invalid, but is necessary because Godot's collections don't support rvalues.
Breakpoint(const Source &p_source) :
source(&p_source) {}
bool operator==(const Breakpoint &p_other) const {
return source == p_other.source && line == p_other.line;
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["verified"] = verified;
if (source) {
dict["source"] = source->to_json();
}
dict["line"] = line;
return dict;
}
};
struct BreakpointLocation {
int line = 0;
int endLine = -1;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["line"] = line;
if (endLine >= 0) {
dict["endLine"] = endLine;
}
return dict;
}
};
struct Capabilities {
bool supportsConfigurationDoneRequest = true;
bool supportsEvaluateForHovers = true;
bool supportsSetVariable = true;
String supportedChecksumAlgorithms[2] = { "MD5", "SHA256" };
bool supportsRestartRequest = true;
bool supportsValueFormattingOptions = true;
bool supportTerminateDebuggee = true;
bool supportSuspendDebuggee = true;
bool supportsTerminateRequest = true;
bool supportsBreakpointLocationsRequest = true;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["supportsConfigurationDoneRequest"] = supportsConfigurationDoneRequest;
dict["supportsEvaluateForHovers"] = supportsEvaluateForHovers;
dict["supportsSetVariable"] = supportsSetVariable;
dict["supportsRestartRequest"] = supportsRestartRequest;
dict["supportsValueFormattingOptions"] = supportsValueFormattingOptions;
dict["supportTerminateDebuggee"] = supportTerminateDebuggee;
dict["supportSuspendDebuggee"] = supportSuspendDebuggee;
dict["supportsTerminateRequest"] = supportsTerminateRequest;
dict["supportsBreakpointLocationsRequest"] = supportsBreakpointLocationsRequest;
Array arr = { supportedChecksumAlgorithms[0], supportedChecksumAlgorithms[1] };
dict["supportedChecksumAlgorithms"] = arr;
return dict;
}
};
struct Message {
int id = 0;
String format;
bool sendTelemetry = false; // Just in case :)
bool showUser = false;
Dictionary variables;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["format"] = format;
dict["sendTelemetry"] = sendTelemetry;
dict["showUser"] = showUser;
dict["variables"] = variables;
return dict;
}
};
struct Scope {
String name;
String presentationHint;
int variablesReference = 0;
bool expensive = false;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["presentationHint"] = presentationHint;
dict["variablesReference"] = variablesReference;
dict["expensive"] = expensive;
return dict;
}
};
struct SourceBreakpoint {
int line = 0;
_FORCE_INLINE_ void from_json(const Dictionary &p_params) {
line = p_params["line"];
}
};
struct StackFrame {
int id = 0;
String name;
const Source *source = nullptr;
int line = 0;
int column = 0;
StackFrame() = default; // Empty constructor is invalid, but is necessary because Godot's collections don't support rvalues.
StackFrame(const Source &p_source) :
source(&p_source) {}
static uint32_t hash(const StackFrame &p_frame) {
return hash_murmur3_one_32(p_frame.id);
}
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["name"] = name;
if (source) {
dict["source"] = source->to_json();
}
dict["line"] = line;
dict["column"] = column;
return dict;
}
};
struct Thread {
int id = 0;
String name;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["id"] = id;
dict["name"] = name;
return dict;
}
};
struct Variable {
String name;
String value;
String type;
int variablesReference = 0;
_FORCE_INLINE_ Dictionary to_json() const {
Dictionary dict;
dict["name"] = name;
dict["value"] = value;
dict["type"] = type;
dict["variablesReference"] = variablesReference;
return dict;
}
};
} // namespace DAP

View File

@@ -0,0 +1,271 @@
/**************************************************************************/
/* debugger_editor_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "debugger_editor_plugin.h"
#include "core/os/keyboard.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/editor_debugger_server.h"
#include "editor/debugger/editor_file_server.h"
#include "editor/editor_node.h"
#include "editor/gui/editor_bottom_panel.h"
#include "editor/run/run_instances_dialog.h"
#include "editor/script/script_editor_plugin.h"
#include "editor/settings/editor_command_palette.h"
#include "editor/settings/editor_settings.h"
#include "scene/gui/popup_menu.h"
DebuggerEditorPlugin::DebuggerEditorPlugin(PopupMenu *p_debug_menu) {
EditorDebuggerServer::initialize();
ED_SHORTCUT("debugger/step_into", TTRC("Step Into"), Key::F11);
ED_SHORTCUT("debugger/step_over", TTRC("Step Over"), Key::F10);
ED_SHORTCUT("debugger/break", TTRC("Break"));
ED_SHORTCUT("debugger/continue", TTRC("Continue"), Key::F12);
ED_SHORTCUT("debugger/debug_with_external_editor", TTRC("Debug with External Editor"));
// File Server for deploy with remote filesystem.
file_server = memnew(EditorFileServer);
EditorDebuggerNode *debugger = memnew(EditorDebuggerNode);
Button *db = EditorNode::get_bottom_panel()->add_item(TTRC("Debugger"), debugger, ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_debugger_bottom_panel", TTRC("Toggle Debugger Bottom Panel"), KeyModifierMask::ALT | Key::D));
debugger->set_tool_button(db);
// Main editor debug menu.
debug_menu = p_debug_menu;
debug_menu->set_hide_on_checkable_item_selection(false);
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/deploy_with_remote_debug", TTRC("Deploy with Remote Debug")), RUN_DEPLOY_REMOTE_DEBUG);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, using one-click deploy will make the executable attempt to connect to this computer's IP so the running project can be debugged.\nThis option is intended to be used for remote debugging (typically with a mobile device).\nYou don't need to enable it to use the GDScript debugger locally."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/small_deploy_with_network_fs", TTRC("Small Deploy with Network Filesystem")), RUN_FILE_SERVER);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, using one-click deploy for Android will only export an executable without the project data.\nThe filesystem will be provided from the project by the editor over the network.\nOn Android, deploying will use the USB cable for faster performance. This option speeds up testing for projects with large assets."));
debug_menu->add_separator();
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/visible_collision_shapes", TTRC("Visible Collision Shapes")), RUN_DEBUG_COLLISIONS);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, collision shapes and raycast nodes (for 2D and 3D) will be visible in the running project."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/visible_paths", TTRC("Visible Paths")), RUN_DEBUG_PATHS);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, curve resources used by path nodes will be visible in the running project."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/visible_navigation", TTRC("Visible Navigation")), RUN_DEBUG_NAVIGATION);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, navigation meshes, and polygons will be visible in the running project."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/visible_avoidance", TTRC("Visible Avoidance")), RUN_DEBUG_AVOIDANCE);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, avoidance object shapes, radiuses, and velocities will be visible in the running project."));
debug_menu->add_separator();
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/visible_canvas_redraw", TTRC("Debug CanvasItem Redraws")), RUN_DEBUG_CANVAS_REDRAW);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, redraw requests of 2D objects will become visible (as a short flash) in the running project.\nThis is useful to troubleshoot low processor mode."));
debug_menu->add_separator();
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/sync_scene_changes", TTRC("Synchronize Scene Changes")), RUN_LIVE_DEBUG);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, any changes made to the scene in the editor will be replicated in the running project.\nWhen used remotely on a device, this is more efficient when the network filesystem option is enabled."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/sync_script_changes", TTRC("Synchronize Script Changes")), RUN_RELOAD_SCRIPTS);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, any script that is saved will be reloaded in the running project.\nWhen used remotely on a device, this is more efficient when the network filesystem option is enabled."));
debug_menu->add_check_shortcut(ED_SHORTCUT("editor/keep_server_open", TTRC("Keep Debug Server Open")), SERVER_KEEP_OPEN);
debug_menu->set_item_tooltip(-1,
TTRC("When this option is enabled, the editor debug server will stay open and listen for new sessions started outside of the editor itself."));
// Multi-instance, start/stop.
debug_menu->add_separator();
debug_menu->add_item(TTRC("Customize Run Instances..."), RUN_MULTIPLE_INSTANCES);
debug_menu->connect(SceneStringName(id_pressed), callable_mp(this, &DebuggerEditorPlugin::_menu_option));
run_instances_dialog = memnew(RunInstancesDialog);
EditorNode::get_singleton()->get_gui_base()->add_child(run_instances_dialog);
}
DebuggerEditorPlugin::~DebuggerEditorPlugin() {
EditorDebuggerServer::deinitialize();
memdelete(file_server);
}
void DebuggerEditorPlugin::_menu_option(int p_option) {
switch (p_option) {
case RUN_FILE_SERVER: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_FILE_SERVER));
if (ischecked) {
file_server->stop();
set_process(false);
} else {
file_server->start();
set_process(true);
}
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_FILE_SERVER), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_file_server", !ischecked);
}
} break;
case RUN_LIVE_DEBUG: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_LIVE_DEBUG));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_LIVE_DEBUG), !ischecked);
EditorDebuggerNode::get_singleton()->set_live_debugging(!ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_live_debug", !ischecked);
}
} break;
case RUN_DEPLOY_REMOTE_DEBUG: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEPLOY_REMOTE_DEBUG));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEPLOY_REMOTE_DEBUG), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_deploy_remote_debug", !ischecked);
}
} break;
case RUN_DEBUG_COLLISIONS: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEBUG_COLLISIONS));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEBUG_COLLISIONS), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_debug_collisions", !ischecked);
}
} break;
case RUN_DEBUG_PATHS: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEBUG_PATHS));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEBUG_PATHS), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_debug_paths", !ischecked);
}
} break;
case RUN_DEBUG_NAVIGATION: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEBUG_NAVIGATION));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEBUG_NAVIGATION), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_debug_navigation", !ischecked);
}
} break;
case RUN_DEBUG_AVOIDANCE: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEBUG_AVOIDANCE));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEBUG_AVOIDANCE), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_debug_avoidance", !ischecked);
}
} break;
case RUN_DEBUG_CANVAS_REDRAW: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_DEBUG_CANVAS_REDRAW));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_DEBUG_CANVAS_REDRAW), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_debug_canvas_redraw", !ischecked);
}
} break;
case RUN_RELOAD_SCRIPTS: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(RUN_RELOAD_SCRIPTS));
debug_menu->set_item_checked(debug_menu->get_item_index(RUN_RELOAD_SCRIPTS), !ischecked);
ScriptEditor::get_singleton()->set_live_auto_reload_running_scripts(!ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "run_reload_scripts", !ischecked);
}
} break;
case SERVER_KEEP_OPEN: {
bool ischecked = debug_menu->is_item_checked(debug_menu->get_item_index(SERVER_KEEP_OPEN));
debug_menu->set_item_checked(debug_menu->get_item_index(SERVER_KEEP_OPEN), !ischecked);
EditorDebuggerNode::get_singleton()->set_keep_open(!ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "server_keep_open", !ischecked);
}
} break;
case RUN_MULTIPLE_INSTANCES: {
run_instances_dialog->popup_dialog();
} break;
}
}
void DebuggerEditorPlugin::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
_update_debug_options();
initializing = false;
} break;
case NOTIFICATION_PROCESS: {
file_server->poll();
} break;
}
}
void DebuggerEditorPlugin::_update_debug_options() {
bool check_deploy_remote = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_deploy_remote_debug", true);
bool check_file_server = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_file_server", false);
bool check_debug_collisions = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_collisions", false);
bool check_debug_paths = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_paths", false);
bool check_debug_navigation = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_navigation", false);
bool check_debug_avoidance = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_avoidance", false);
bool check_debug_canvas_redraw = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_canvas_redraw", false);
bool check_live_debug = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_live_debug", true);
bool check_reload_scripts = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_reload_scripts", true);
bool check_server_keep_open = EditorSettings::get_singleton()->get_project_metadata("debug_options", "server_keep_open", false);
if (check_deploy_remote) {
_menu_option(RUN_DEPLOY_REMOTE_DEBUG);
}
if (check_file_server) {
_menu_option(RUN_FILE_SERVER);
}
if (check_debug_collisions) {
_menu_option(RUN_DEBUG_COLLISIONS);
}
if (check_debug_paths) {
_menu_option(RUN_DEBUG_PATHS);
}
if (check_debug_navigation) {
_menu_option(RUN_DEBUG_NAVIGATION);
}
if (check_debug_avoidance) {
_menu_option(RUN_DEBUG_AVOIDANCE);
}
if (check_debug_canvas_redraw) {
_menu_option(RUN_DEBUG_CANVAS_REDRAW);
}
if (check_live_debug) {
_menu_option(RUN_LIVE_DEBUG);
}
if (check_reload_scripts) {
_menu_option(RUN_RELOAD_SCRIPTS);
}
if (check_server_keep_open) {
_menu_option(SERVER_KEEP_OPEN);
}
}

View File

@@ -0,0 +1,74 @@
/**************************************************************************/
/* debugger_editor_plugin.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "editor/plugins/editor_plugin.h"
class EditorFileServer;
class MenuButton;
class PopupMenu;
class RunInstancesDialog;
class DebuggerEditorPlugin : public EditorPlugin {
GDCLASS(DebuggerEditorPlugin, EditorPlugin);
private:
PopupMenu *debug_menu = nullptr;
EditorFileServer *file_server = nullptr;
RunInstancesDialog *run_instances_dialog = nullptr;
enum MenuOptions {
RUN_FILE_SERVER,
RUN_LIVE_DEBUG,
RUN_DEBUG_COLLISIONS,
RUN_DEBUG_PATHS,
RUN_DEBUG_NAVIGATION,
RUN_DEBUG_AVOIDANCE,
RUN_DEBUG_CANVAS_REDRAW,
RUN_DEPLOY_REMOTE_DEBUG,
RUN_RELOAD_SCRIPTS,
SERVER_KEEP_OPEN,
RUN_MULTIPLE_INSTANCES,
};
bool initializing = true;
void _update_debug_options();
void _notification(int p_what);
void _menu_option(int p_option);
public:
virtual String get_plugin_name() const override { return "Debugger"; }
bool has_main_screen() const override { return false; }
DebuggerEditorPlugin(PopupMenu *p_menu);
~DebuggerEditorPlugin();
};

View File

@@ -0,0 +1,426 @@
/**************************************************************************/
/* editor_debugger_inspector.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_debugger_inspector.h"
#include "core/debugger/debugger_marshalls.h"
#include "core/io/marshalls.h"
#include "editor/docks/inspector_dock.h"
#include "editor/editor_node.h"
#include "editor/editor_undo_redo_manager.h"
#include "scene/debugger/scene_debugger.h"
bool EditorDebuggerRemoteObjects::_set(const StringName &p_name, const Variant &p_value) {
return _set_impl(p_name, p_value, "");
}
bool EditorDebuggerRemoteObjects::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field) {
String name = p_name;
if (!prop_values.has(name) || String(name).begins_with("Constants/")) {
return false;
}
// Change it back to the real name when fetching.
if (name == "Script") {
name = "script";
} else if (name.begins_with("Metadata/")) {
name = name.replace_first("Metadata/", "metadata/");
}
Dictionary &values = prop_values[p_name];
Dictionary old_values = values.duplicate();
for (const uint64_t key : values.keys()) {
values.set(key, p_value);
}
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
const int size = remote_object_ids.size();
ur->create_action(size == 1 ? vformat(TTR("Set %s"), name) : vformat(TTR("Set %s on %d objects"), name, size), UndoRedo::MERGE_ENDS);
ur->add_do_method(this, SNAME("emit_signal"), SNAME("values_edited"), name, values, p_field);
ur->add_undo_method(this, SNAME("emit_signal"), SNAME("values_edited"), name, old_values, p_field);
ur->commit_action();
return true;
}
bool EditorDebuggerRemoteObjects::_get(const StringName &p_name, Variant &r_ret) const {
String name = p_name;
if (!prop_values.has(name)) {
return false;
}
// Change it back to the real name when fetching.
if (name == "Script") {
name = "script";
} else if (name.begins_with("Metadata/")) {
name = name.replace_first("Metadata/", "metadata/");
}
r_ret = prop_values[p_name][remote_object_ids[0]];
return true;
}
void EditorDebuggerRemoteObjects::_get_property_list(List<PropertyInfo> *p_list) const {
p_list->clear(); // Sorry, don't want any categories.
for (const PropertyInfo &prop : prop_list) {
p_list->push_back(prop);
}
}
void EditorDebuggerRemoteObjects::set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field) {
_set_impl(p_property, p_value, p_field);
}
String EditorDebuggerRemoteObjects::get_title() {
if (!remote_object_ids.is_empty() && ObjectID(remote_object_ids[0].operator uint64_t()).is_valid()) {
const int size = remote_object_ids.size();
return size == 1 ? vformat(TTR("Remote %s: %d"), type_name, remote_object_ids[0]) : vformat(TTR("Remote %s (%d Selected)"), type_name, size);
}
return "<null>";
}
Variant EditorDebuggerRemoteObjects::get_variant(const StringName &p_name) {
Variant var;
_get(p_name, var);
return var;
}
void EditorDebuggerRemoteObjects::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_title"), &EditorDebuggerRemoteObjects::get_title);
ClassDB::bind_method("_hide_script_from_inspector", &EditorDebuggerRemoteObjects::_hide_script_from_inspector);
ClassDB::bind_method("_hide_metadata_from_inspector", &EditorDebuggerRemoteObjects::_hide_metadata_from_inspector);
ADD_SIGNAL(MethodInfo("values_edited", PropertyInfo(Variant::STRING, "property"), PropertyInfo(Variant::DICTIONARY, "values", PROPERTY_HINT_DICTIONARY_TYPE, "uint64_t:Variant"), PropertyInfo(Variant::STRING, "field")));
}
/// EditorDebuggerInspector
EditorDebuggerInspector::EditorDebuggerInspector() {
variables = memnew(EditorDebuggerRemoteObjects);
}
EditorDebuggerInspector::~EditorDebuggerInspector() {
clear_cache();
memdelete(variables);
}
void EditorDebuggerInspector::_bind_methods() {
ADD_SIGNAL(MethodInfo("object_selected", PropertyInfo(Variant::INT, "id")));
ADD_SIGNAL(MethodInfo("objects_edited", PropertyInfo(Variant::ARRAY, "ids"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"), PropertyInfo(Variant::STRING, "field")));
ADD_SIGNAL(MethodInfo("object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
}
void EditorDebuggerInspector::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
connect("object_id_selected", callable_mp(this, &EditorDebuggerInspector::_object_selected));
} break;
case NOTIFICATION_ENTER_TREE: {
variables->remote_object_ids.append(0);
edit(variables);
} break;
}
}
void EditorDebuggerInspector::_objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field) {
emit_signal(SNAME("objects_edited"), p_prop, p_values, p_field);
}
void EditorDebuggerInspector::_object_selected(ObjectID p_object) {
emit_signal(SNAME("object_selected"), p_object);
}
EditorDebuggerRemoteObjects *EditorDebuggerInspector::set_objects(const Array &p_arr) {
ERR_FAIL_COND_V(p_arr.is_empty(), nullptr);
TypedArray<uint64_t> ids;
LocalVector<SceneDebuggerObject> objects;
for (const Array arr : p_arr) {
SceneDebuggerObject obj;
obj.deserialize(arr);
if (obj.id.is_valid()) {
ids.push_back((uint64_t)obj.id);
objects.push_back(obj);
}
}
ERR_FAIL_COND_V(ids.is_empty(), nullptr);
// Sorting is necessary, as selected nodes in the remote tree are ordered by index.
ids.sort();
EditorDebuggerRemoteObjects *remote_objects = nullptr;
for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
if (robjs->remote_object_ids == ids) {
remote_objects = robjs;
break;
}
}
if (!remote_objects) {
remote_objects = memnew(EditorDebuggerRemoteObjects);
remote_objects->remote_object_ids = ids;
remote_objects->remote_object_ids.make_read_only();
remote_objects->connect("values_edited", callable_mp(this, &EditorDebuggerInspector::_objects_edited));
remote_objects_list.push_back(remote_objects);
}
StringName class_name = objects[0].class_name;
if (class_name != SNAME("Object")) {
// Search for the common class between all selected objects.
bool check_type_again = true;
while (check_type_again) {
check_type_again = false;
if (class_name == SNAME("Object") || class_name == StringName()) {
// All objects inherit from Object, so no need to continue checking.
class_name = SNAME("Object");
break;
}
// Check that all objects inherit from type_name.
for (const SceneDebuggerObject &obj : objects) {
if (obj.class_name == class_name || ClassDB::is_parent_class(obj.class_name, class_name)) {
continue; // class_name is the same or a parent of the object's class.
}
// class_name is not a parent of the node's class, so check again with the parent class.
class_name = ClassDB::get_parent_class(class_name);
check_type_again = true;
break;
}
}
}
remote_objects->type_name = class_name;
// Search for properties that are present in all selected objects.
struct UsageData {
int qty = 0;
SceneDebuggerObject::SceneDebuggerProperty prop;
TypedDictionary<uint64_t, Variant> values;
};
HashMap<String, UsageData> usage;
int nc = 0;
for (const SceneDebuggerObject &obj : objects) {
for (const SceneDebuggerObject::SceneDebuggerProperty &prop : obj.properties) {
PropertyInfo pinfo = prop.first;
// Rename those variables, so they don't conflict with the ones from the resource itself.
if (pinfo.name == "script") {
pinfo.name = "Script";
} else if (pinfo.name.begins_with("metadata/")) {
pinfo.name = pinfo.name.replace_first("metadata/", "Metadata/");
}
if (!usage.has(pinfo.name)) {
UsageData usage_dt;
usage_dt.prop = prop;
usage_dt.prop.first.name = pinfo.name;
usage_dt.values[obj.id] = prop.second;
usage[pinfo.name] = usage_dt;
}
// Make sure only properties with the same exact PropertyInfo data will appear.
if (usage[pinfo.name].prop.first == pinfo) {
usage[pinfo.name].qty++;
usage[pinfo.name].values[obj.id] = prop.second;
}
}
nc++;
}
for (HashMap<String, UsageData>::Iterator E = usage.begin(); E;) {
HashMap<String, UsageData>::Iterator next = E;
++next;
UsageData usage_dt = E->value;
if (nc != usage_dt.qty) {
// Doesn't appear on all of them, remove it.
usage.erase(E->key);
}
E = next;
}
int old_prop_size = remote_objects->prop_list.size();
remote_objects->prop_list.clear();
int new_props_added = 0;
HashSet<String> changed;
for (KeyValue<String, UsageData> &KV : usage) {
const PropertyInfo &pinfo = KV.value.prop.first;
Variant var = KV.value.values[remote_objects->remote_object_ids[0]];
if (pinfo.type == Variant::OBJECT && var.is_string()) {
String path = var;
if (path.contains("::")) {
// Built-in resource.
String base_path = path.get_slice("::", 0);
Ref<Resource> dependency = ResourceLoader::load(base_path);
if (dependency.is_valid()) {
remote_dependencies.insert(dependency);
}
}
var = ResourceLoader::load(path);
KV.value.values[remote_objects->remote_object_ids[0]] = var;
}
// Always add the property, since props may have been added or removed.
remote_objects->prop_list.push_back(pinfo);
if (!remote_objects->prop_values.has(pinfo.name)) {
new_props_added++;
} else if (bool(Variant::evaluate(Variant::OP_NOT_EQUAL, remote_objects->prop_values[pinfo.name], var))) {
changed.insert(pinfo.name);
}
remote_objects->prop_values[pinfo.name] = KV.value.values;
}
if (old_prop_size == remote_objects->prop_list.size() && new_props_added == 0) {
// Only some may have changed, if so, then update those, if they exist.
for (const String &E : changed) {
emit_signal(SNAME("object_property_updated"), remote_objects->get_instance_id(), E);
}
} else {
// Full update, because props were added or removed.
remote_objects->update();
}
return remote_objects;
}
void EditorDebuggerInspector::clear_remote_inspector() {
if (remote_objects_list.is_empty()) {
return;
}
const Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
// Check if the inspector holds remote items, and take it out if so.
if (Object::cast_to<EditorDebuggerRemoteObjects>(obj)) {
EditorNode::get_singleton()->push_item(nullptr);
}
}
void EditorDebuggerInspector::clear_cache() {
clear_remote_inspector();
for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
memdelete(robjs);
}
remote_objects_list.clear();
remote_dependencies.clear();
}
void EditorDebuggerInspector::invalidate_selection_from_cache(const TypedArray<uint64_t> &p_ids) {
for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
if (robjs->remote_object_ids == p_ids) {
const Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
if (obj == robjs) {
EditorNode::get_singleton()->push_item(nullptr);
}
remote_objects_list.erase(robjs);
memdelete(robjs);
break;
}
}
}
void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_offset) {
DebuggerMarshalls::ScriptStackVariable var;
var.deserialize(p_array);
String n = var.name;
Variant v = var.value;
PropertyHint h = PROPERTY_HINT_NONE;
String hs;
if (var.var_type == Variant::OBJECT && v) {
v = Object::cast_to<EncodedObjectAsID>(v)->get_object_id();
h = PROPERTY_HINT_OBJECT_ID;
hs = "Object";
}
String type;
switch (var.type) {
case 0:
type = "Locals/";
break;
case 1:
type = "Members/";
break;
case 2:
type = "Globals/";
break;
case 3:
type = "Evaluated/";
break;
default:
type = "Unknown/";
}
PropertyInfo pinfo;
// Encode special characters to avoid issues with expressions in Evaluator.
// Dots are skipped by uri_encode(), but uri_decode() process them correctly when replaced with "%2E".
pinfo.name = type + n.uri_encode().replace(".", "%2E");
pinfo.type = v.get_type();
pinfo.hint = h;
pinfo.hint_string = hs;
if ((p_offset == -1) || variables->prop_list.is_empty()) {
variables->prop_list.push_back(pinfo);
} else {
List<PropertyInfo>::Element *current = variables->prop_list.front();
for (int i = 0; i < p_offset; i++) {
current = current->next();
}
variables->prop_list.insert_before(current, pinfo);
}
variables->prop_values[pinfo.name][0] = v;
variables->update();
edit(variables);
}
void EditorDebuggerInspector::clear_stack_variables() {
variables->clear();
variables->update();
}
String EditorDebuggerInspector::get_stack_variable(const String &p_var) {
for (KeyValue<StringName, TypedDictionary<uint64_t, Variant>> &E : variables->prop_values) {
String v = E.key.operator String();
if (v.get_slicec('/', 1) == p_var) {
return variables->get_variant(v);
}
}
return String();
}

View File

@@ -0,0 +1,100 @@
/**************************************************************************/
/* editor_debugger_inspector.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/variant/typed_dictionary.h"
#include "editor/inspector/editor_inspector.h"
class SceneDebuggerObject;
class EditorDebuggerRemoteObjects : public Object {
GDCLASS(EditorDebuggerRemoteObjects, Object);
private:
bool _set_impl(const StringName &p_name, const Variant &p_value, const String &p_field);
protected:
bool _set(const StringName &p_name, const Variant &p_value);
bool _get(const StringName &p_name, Variant &r_ret) const;
void _get_property_list(List<PropertyInfo> *p_list) const;
static void _bind_methods();
public:
TypedArray<uint64_t> remote_object_ids;
String type_name;
List<PropertyInfo> prop_list;
HashMap<StringName, TypedDictionary<uint64_t, Variant>> prop_values;
bool _hide_script_from_inspector() { return true; }
bool _hide_metadata_from_inspector() { return true; }
void set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field);
String get_title();
Variant get_variant(const StringName &p_name);
void clear() {
prop_list.clear();
prop_values.clear();
}
void update() { notify_property_list_changed(); }
};
class EditorDebuggerInspector : public EditorInspector {
GDCLASS(EditorDebuggerInspector, EditorInspector);
private:
LocalVector<EditorDebuggerRemoteObjects *> remote_objects_list;
HashSet<Ref<Resource>> remote_dependencies;
EditorDebuggerRemoteObjects *variables = nullptr;
void _object_selected(ObjectID p_object);
void _objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field);
protected:
void _notification(int p_what);
static void _bind_methods();
public:
EditorDebuggerInspector();
~EditorDebuggerInspector();
// Remote Object cache
EditorDebuggerRemoteObjects *set_objects(const Array &p_array);
void clear_remote_inspector();
void clear_cache();
void invalidate_selection_from_cache(const TypedArray<uint64_t> &p_ids);
// Stack Dump variables
String get_stack_variable(const String &p_var);
void add_stack_variable(const Array &p_arr, int p_offset = -1);
void clear_stack_variables();
};

View File

@@ -0,0 +1,932 @@
/**************************************************************************/
/* editor_debugger_node.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_debugger_node.h"
#include "core/object/undo_redo.h"
#include "editor/debugger/editor_debugger_plugin.h"
#include "editor/debugger/editor_debugger_tree.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/docks/inspector_dock.h"
#include "editor/docks/scene_tree_dock.h"
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_bottom_panel.h"
#include "editor/run/editor_run_bar.h"
#include "editor/script/script_editor_plugin.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/menu_button.h"
#include "scene/gui/tab_container.h"
#include "scene/resources/packed_scene.h"
template <typename Func>
void _for_all(TabContainer *p_node, const Func &p_func) {
for (int i = 0; i < p_node->get_tab_count(); i++) {
ScriptEditorDebugger *dbg = Object::cast_to<ScriptEditorDebugger>(p_node->get_tab_control(i));
ERR_FAIL_NULL(dbg);
p_func(dbg);
}
}
EditorDebuggerNode *EditorDebuggerNode::singleton = nullptr;
EditorDebuggerNode::EditorDebuggerNode() {
if (!singleton) {
singleton = this;
}
add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_LEFT));
add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_RIGHT));
add_theme_constant_override("margin_bottom", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_BOTTOM));
tabs = memnew(TabContainer);
tabs->set_tabs_visible(false);
tabs->connect("tab_changed", callable_mp(this, &EditorDebuggerNode::_debugger_changed));
add_child(tabs);
Ref<StyleBoxEmpty> empty;
empty.instantiate();
tabs->add_theme_style_override(SceneStringName(panel), empty);
auto_switch_remote_scene_tree = EDITOR_GET("debugger/auto_switch_to_remote_scene_tree");
_add_debugger();
// Remote scene tree
remote_scene_tree = memnew(EditorDebuggerTree);
remote_scene_tree->connect("objects_selected", callable_mp(this, &EditorDebuggerNode::_remote_objects_requested));
remote_scene_tree->connect("selection_cleared", callable_mp(this, &EditorDebuggerNode::_remote_selection_cleared));
remote_scene_tree->connect("save_node", callable_mp(this, &EditorDebuggerNode::_save_node_requested));
remote_scene_tree->connect("button_clicked", callable_mp(this, &EditorDebuggerNode::_remote_tree_button_pressed));
SceneTreeDock::get_singleton()->add_remote_tree_editor(remote_scene_tree);
SceneTreeDock::get_singleton()->connect("remote_tree_selected", callable_mp(this, &EditorDebuggerNode::request_remote_tree));
remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval");
inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
if (Engine::get_singleton()->is_recovery_mode_hint()) {
return;
}
EditorRunBar::get_singleton()->get_pause_button()->connect(SceneStringName(pressed), callable_mp(this, &EditorDebuggerNode::_paused));
}
ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() {
ScriptEditorDebugger *node = memnew(ScriptEditorDebugger);
int id = tabs->get_tab_count();
node->connect("stop_requested", callable_mp(this, &EditorDebuggerNode::_debugger_wants_stop).bind(id));
node->connect("stopped", callable_mp(this, &EditorDebuggerNode::_debugger_stopped).bind(id));
node->connect("stack_frame_selected", callable_mp(this, &EditorDebuggerNode::_stack_frame_selected).bind(id));
node->connect("error_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
node->connect("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
node->connect("debug_data", callable_mp(this, &EditorDebuggerNode::_debug_data).bind(id));
node->connect("remote_tree_select_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_select_requested).bind(id));
node->connect("remote_tree_clear_selection_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_clear_selection_requested).bind(id));
node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id));
node->connect("remote_objects_updated", callable_mp(this, &EditorDebuggerNode::_remote_objects_updated).bind(id));
node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id));
node->connect("remote_objects_requested", callable_mp(this, &EditorDebuggerNode::_remote_objects_requested).bind(id));
node->connect("set_breakpoint", callable_mp(this, &EditorDebuggerNode::_breakpoint_set_in_tree).bind(id));
node->connect("clear_breakpoints", callable_mp(this, &EditorDebuggerNode::_breakpoints_cleared_in_tree).bind(id));
node->connect("errors_cleared", callable_mp(this, &EditorDebuggerNode::_update_errors));
if (tabs->get_tab_count() > 0) {
get_debugger(0)->clear_style();
}
tabs->add_child(node);
node->set_name(vformat(TTR("Session %d"), tabs->get_tab_count()));
if (tabs->get_tab_count() > 1) {
node->clear_style();
tabs->set_tabs_visible(true);
tabs->add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("DebuggerPanel"), EditorStringName(EditorStyles)));
}
if (!debugger_plugins.is_empty()) {
for (Ref<EditorDebuggerPlugin> plugin : debugger_plugins) {
plugin->create_session(node);
}
}
return node;
}
void EditorDebuggerNode::_stack_frame_selected(int p_debugger) {
const ScriptEditorDebugger *dbg = get_debugger(p_debugger);
ERR_FAIL_NULL(dbg);
if (dbg != get_current_debugger()) {
return;
}
_text_editor_stack_goto(dbg);
}
void EditorDebuggerNode::_error_selected(const String &p_file, int p_line, int p_debugger) {
Ref<Script> s = ResourceLoader::load(p_file);
emit_signal(SNAME("goto_script_line"), s, p_line - 1);
}
void EditorDebuggerNode::_text_editor_stack_goto(const ScriptEditorDebugger *p_debugger) {
String file = p_debugger->get_stack_script_file();
if (file.is_empty()) {
return;
}
if (file.is_resource_file()) {
stack_script = ResourceLoader::load(file);
} else {
// If the script is built-in, it can be opened only if the scene is loaded in memory.
int i = file.find("::");
int j = file.rfind_char('(', i);
if (j > -1) { // If the script is named, the string is "name (file)", so we need to extract the path.
file = file.substr(j + 1, file.find_char(')', i) - j - 1);
}
Ref<PackedScene> ps = ResourceLoader::load(file.get_slice("::", 0));
stack_script = ResourceLoader::load(file);
}
const int line = p_debugger->get_stack_script_line() - 1;
emit_signal(SNAME("goto_script_line"), stack_script, line);
emit_signal(SNAME("set_execution"), stack_script, line);
stack_script.unref(); // Why?!?
}
void EditorDebuggerNode::_text_editor_stack_clear(const ScriptEditorDebugger *p_debugger) {
String file = p_debugger->get_stack_script_file();
if (file.is_empty()) {
return;
}
if (file.is_resource_file()) {
stack_script = ResourceLoader::load(file);
} else {
// If the script is built-in, it can be opened only if the scene is loaded in memory.
int i = file.find("::");
int j = file.rfind_char('(', i);
if (j > -1) { // If the script is named, the string is "name (file)", so we need to extract the path.
file = file.substr(j + 1, file.find_char(')', i) - j - 1);
}
Ref<PackedScene> ps = ResourceLoader::load(file.get_slice("::", 0));
stack_script = ResourceLoader::load(file);
}
emit_signal(SNAME("clear_execution"), stack_script);
stack_script.unref(); // Why?!?
}
void EditorDebuggerNode::_bind_methods() {
// LiveDebug.
ClassDB::bind_method("live_debug_create_node", &EditorDebuggerNode::live_debug_create_node);
ClassDB::bind_method("live_debug_instantiate_node", &EditorDebuggerNode::live_debug_instantiate_node);
ClassDB::bind_method("live_debug_remove_node", &EditorDebuggerNode::live_debug_remove_node);
ClassDB::bind_method("live_debug_remove_and_keep_node", &EditorDebuggerNode::live_debug_remove_and_keep_node);
ClassDB::bind_method("live_debug_restore_node", &EditorDebuggerNode::live_debug_restore_node);
ClassDB::bind_method("live_debug_duplicate_node", &EditorDebuggerNode::live_debug_duplicate_node);
ClassDB::bind_method("live_debug_reparent_node", &EditorDebuggerNode::live_debug_reparent_node);
ADD_SIGNAL(MethodInfo("goto_script_line"));
ADD_SIGNAL(MethodInfo("set_execution", PropertyInfo("script"), PropertyInfo(Variant::INT, "line")));
ADD_SIGNAL(MethodInfo("clear_execution", PropertyInfo("script")));
ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug")));
ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::BOOL, "enabled")));
ADD_SIGNAL(MethodInfo("breakpoint_set_in_tree", PropertyInfo("script"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::BOOL, "enabled"), PropertyInfo(Variant::INT, "debugger")));
ADD_SIGNAL(MethodInfo("breakpoints_cleared_in_tree", PropertyInfo(Variant::INT, "debugger")));
}
void EditorDebuggerNode::register_undo_redo(UndoRedo *p_undo_redo) {
p_undo_redo->set_method_notify_callback(_methods_changed, this);
p_undo_redo->set_property_notify_callback(_properties_changed, this);
}
ScriptEditorDebugger *EditorDebuggerNode::get_debugger(int p_id) const {
return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(p_id));
}
ScriptEditorDebugger *EditorDebuggerNode::get_previous_debugger() const {
return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(tabs->get_previous_tab()));
}
ScriptEditorDebugger *EditorDebuggerNode::get_current_debugger() const {
return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(tabs->get_current_tab()));
}
ScriptEditorDebugger *EditorDebuggerNode::get_default_debugger() const {
return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(0));
}
String EditorDebuggerNode::get_server_uri() const {
ERR_FAIL_COND_V(server.is_null(), "");
return server->get_uri();
}
void EditorDebuggerNode::set_keep_open(bool p_keep_open) {
keep_open = p_keep_open;
if (keep_open) {
if (server.is_null() || !server->is_active()) {
start();
}
} else {
bool found = false;
_for_all(tabs, [&](ScriptEditorDebugger *p_debugger) {
if (p_debugger->is_session_active()) {
found = true;
}
});
if (!found) {
stop();
}
}
}
Error EditorDebuggerNode::start(const String &p_uri) {
if (Engine::get_singleton()->is_recovery_mode_hint()) {
return ERR_UNAVAILABLE;
}
ERR_FAIL_COND_V(!p_uri.contains("://"), ERR_INVALID_PARAMETER);
if (keep_open && current_uri == p_uri && server.is_valid()) {
return OK;
}
stop(true);
current_uri = p_uri;
server = Ref<EditorDebuggerServer>(EditorDebuggerServer::create(p_uri.substr(0, p_uri.find("://") + 3)));
const Error err = server->start(p_uri);
if (err != OK) {
return err;
}
set_process(true);
EditorNode::get_log()->add_message("--- Debugging process started ---", EditorLog::MSG_TYPE_EDITOR);
return OK;
}
void EditorDebuggerNode::stop(bool p_force) {
if (keep_open && !p_force) {
return;
}
remote_scene_tree_wait = false;
inspect_edited_object_wait = false;
current_uri.clear();
if (server.is_valid()) {
server->stop();
EditorNode::get_log()->add_message("--- Debugging process stopped ---", EditorLog::MSG_TYPE_EDITOR);
if (EditorRunBar::get_singleton()->is_movie_maker_enabled()) {
// Request attention in case the user was doing something else when movie recording is finished.
DisplayServer::get_singleton()->window_request_attention();
}
server.unref();
}
// Also close all debugging sessions.
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (dbg->is_session_active()) {
dbg->_stop_and_notify();
}
});
_break_state_changed();
breakpoints.clear();
EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::REMOTE_HISTORY, false);
set_process(false);
}
void EditorDebuggerNode::_notification(int p_what) {
switch (p_what) {
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (!EditorThemeManager::is_generated_theme_outdated()) {
return;
}
if (tabs->get_tab_count() > 1) {
tabs->add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("DebuggerPanel"), EditorStringName(EditorStyles)));
}
add_theme_constant_override("margin_left", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_LEFT));
add_theme_constant_override("margin_right", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_RIGHT));
add_theme_constant_override("margin_bottom", -EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanelDebuggerOverride"), EditorStringName(EditorStyles))->get_margin(SIDE_BOTTOM));
remote_scene_tree->update_icon_max_width();
} break;
case NOTIFICATION_READY: {
_update_debug_options();
initializing = false;
} break;
case NOTIFICATION_PROCESS: {
if (server.is_null()) {
return;
}
if (!server->is_active()) {
stop();
return;
}
server->poll();
_update_errors();
// Remote scene tree update.
if (!remote_scene_tree_wait) {
remote_scene_tree_timeout -= get_process_delta_time();
if (remote_scene_tree_timeout < 0) {
remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval");
if (remote_scene_tree->is_visible_in_tree()) {
remote_scene_tree_wait = true;
get_current_debugger()->request_remote_tree();
}
}
}
// Remote inspector update.
if (!inspect_edited_object_wait) {
inspect_edited_object_timeout -= get_process_delta_time();
if (inspect_edited_object_timeout < 0) {
inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
if (EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(InspectorDock::get_inspector_singleton()->get_edited_object())) {
inspect_edited_object_wait = true;
get_current_debugger()->request_remote_objects(robjs->remote_object_ids, false);
}
}
}
// Take connections.
if (server->is_connection_available()) {
ScriptEditorDebugger *debugger = nullptr;
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (debugger || dbg->is_session_active()) {
return;
}
debugger = dbg;
});
if (debugger == nullptr) {
if (tabs->get_tab_count() <= 4) { // Max 4 debugging sessions active.
debugger = _add_debugger();
} else {
// We already have too many sessions, disconnecting new clients to prevent them from hanging.
server->take_connection()->close();
return; // Can't add, stop here.
}
}
EditorRunBar::get_singleton()->get_pause_button()->set_disabled(false);
// Switch to remote tree view if so desired.
auto_switch_remote_scene_tree = (bool)EDITOR_GET("debugger/auto_switch_to_remote_scene_tree");
if (auto_switch_remote_scene_tree) {
SceneTreeDock::get_singleton()->show_remote_tree();
}
// Good to go.
SceneTreeDock::get_singleton()->show_tab_buttons();
debugger->set_editor_remote_tree(remote_scene_tree);
debugger->start(server->take_connection());
// Send breakpoints.
for (const KeyValue<Breakpoint, bool> &E : breakpoints) {
const Breakpoint &bp = E.key;
debugger->set_breakpoint(bp.source, bp.line, E.value);
} // Will arrive too late, how does the regular run work?
debugger->update_live_edit_root();
}
} break;
}
}
void EditorDebuggerNode::_update_errors() {
int error_count = 0;
int warning_count = 0;
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
error_count += dbg->get_error_count();
warning_count += dbg->get_warning_count();
});
if (error_count != last_error_count || warning_count != last_warning_count) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->update_tabs();
});
if (error_count == 0 && warning_count == 0) {
debugger_button->set_text(TTR("Debugger"));
debugger_button->remove_theme_color_override(SceneStringName(font_color));
debugger_button->set_button_icon(Ref<Texture2D>());
} else {
debugger_button->set_text(TTR("Debugger") + " (" + itos(error_count + warning_count) + ")");
if (error_count >= 1 && warning_count >= 1) {
debugger_button->set_button_icon(get_editor_theme_icon(SNAME("ErrorWarning")));
// Use error color to represent the highest level of severity reported.
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
} else if (error_count >= 1) {
debugger_button->set_button_icon(get_editor_theme_icon(SNAME("Error")));
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
} else {
debugger_button->set_button_icon(get_editor_theme_icon(SNAME("Warning")));
debugger_button->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
}
}
last_error_count = error_count;
last_warning_count = warning_count;
}
}
void EditorDebuggerNode::_debugger_stopped(int p_id) {
ScriptEditorDebugger *dbg = get_debugger(p_id);
ERR_FAIL_NULL(dbg);
bool found = false;
_for_all(tabs, [&](ScriptEditorDebugger *p_debugger) {
if (p_debugger->is_session_active()) {
found = true;
}
});
if (!found) {
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(false);
EditorRunBar::get_singleton()->get_pause_button()->set_disabled(true);
SceneTreeDock *dock = SceneTreeDock::get_singleton();
if (dock->is_inside_tree()) {
dock->hide_remote_tree();
dock->hide_tab_buttons();
}
EditorNode::get_singleton()->notify_all_debug_sessions_exited();
}
}
void EditorDebuggerNode::_debugger_wants_stop(int p_id) {
// Ask editor to kill PID.
if (int pid = get_debugger(p_id)->get_remote_pid()) {
callable_mp(EditorNode::get_singleton(), &EditorNode::stop_child_process).call_deferred(pid);
}
}
void EditorDebuggerNode::_debugger_changed(int p_tab) {
remote_scene_tree_wait = false;
inspect_edited_object_wait = false;
if (Object *robjs = InspectorDock::get_inspector_singleton()->get_edited_object()) {
if (Object::cast_to<EditorDebuggerRemoteObjects>(robjs)) {
// Clear inspected object, you can only inspect objects in selected debugger.
// Hopefully, in the future, we will have one inspector per debugger.
EditorNode::get_singleton()->push_item(nullptr);
}
}
if (ScriptEditorDebugger *prev_debug = get_previous_debugger()) {
prev_debug->clear_inspector();
_text_editor_stack_clear(prev_debug);
}
if (remote_scene_tree->is_visible_in_tree()) {
get_current_debugger()->request_remote_tree();
}
if (get_current_debugger()->is_breaked()) {
_text_editor_stack_goto(get_current_debugger());
}
_break_state_changed();
}
void EditorDebuggerNode::_debug_data(const String &p_msg, const Array &p_data, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
if (p_msg == "scene:scene_tree") {
remote_scene_tree_wait = false;
} else if (p_msg == "scene:inspect_objects") {
inspect_edited_object_wait = false;
}
}
void EditorDebuggerNode::set_script_debug_button(MenuButton *p_button) {
script_menu = p_button;
script_menu->set_text(TTRC("Debug"));
script_menu->set_switch_on_hover(true);
PopupMenu *p = script_menu->get_popup();
p->add_shortcut(ED_GET_SHORTCUT("debugger/step_into"), DEBUG_STEP);
p->add_shortcut(ED_GET_SHORTCUT("debugger/step_over"), DEBUG_NEXT);
p->add_separator();
p->add_shortcut(ED_GET_SHORTCUT("debugger/break"), DEBUG_BREAK);
p->add_shortcut(ED_GET_SHORTCUT("debugger/continue"), DEBUG_CONTINUE);
p->add_separator();
p->add_check_shortcut(ED_GET_SHORTCUT("debugger/debug_with_external_editor"), DEBUG_WITH_EXTERNAL_EDITOR);
p->connect(SceneStringName(id_pressed), callable_mp(this, &EditorDebuggerNode::_menu_option));
_break_state_changed();
script_menu->show();
}
void EditorDebuggerNode::_break_state_changed() {
const bool breaked = get_current_debugger()->is_breaked();
const bool can_debug = get_current_debugger()->is_debuggable();
if (breaked) { // Show debugger.
EditorNode::get_bottom_panel()->make_item_visible(this);
}
// Update script menu.
if (!script_menu) {
return;
}
PopupMenu *p = script_menu->get_popup();
p->set_item_disabled(p->get_item_index(DEBUG_NEXT), !(breaked && can_debug));
p->set_item_disabled(p->get_item_index(DEBUG_STEP), !(breaked && can_debug));
p->set_item_disabled(p->get_item_index(DEBUG_BREAK), breaked);
p->set_item_disabled(p->get_item_index(DEBUG_CONTINUE), !breaked);
}
void EditorDebuggerNode::_menu_option(int p_id) {
switch (p_id) {
case DEBUG_NEXT: {
debug_next();
} break;
case DEBUG_STEP: {
debug_step();
} break;
case DEBUG_BREAK: {
debug_break();
} break;
case DEBUG_CONTINUE: {
debug_continue();
} break;
case DEBUG_WITH_EXTERNAL_EDITOR: {
bool ischecked = script_menu->get_popup()->is_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR));
debug_with_external_editor = !ischecked;
script_menu->get_popup()->set_item_checked(script_menu->get_popup()->get_item_index(DEBUG_WITH_EXTERNAL_EDITOR), !ischecked);
if (!initializing) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "debug_with_external_editor", !ischecked);
}
} break;
}
}
void EditorDebuggerNode::_update_debug_options() {
if (EditorSettings::get_singleton()->get_project_metadata("debug_options", "debug_with_external_editor", false).operator bool()) {
_menu_option(DEBUG_WITH_EXTERNAL_EDITOR);
}
}
void EditorDebuggerNode::_paused() {
const bool paused = EditorRunBar::get_singleton()->get_pause_button()->is_pressed();
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
if (paused && !dbg->is_breaked()) {
dbg->debug_break();
} else if (!paused && dbg->is_breaked()) {
dbg->debug_continue();
}
});
}
void EditorDebuggerNode::_breaked(bool p_breaked, bool p_can_debug, const String &p_message, bool p_has_stackdump, int p_debugger) {
if (get_current_debugger() != get_debugger(p_debugger)) {
if (!p_breaked) {
return;
}
tabs->set_current_tab(p_debugger);
}
_break_state_changed();
EditorRunBar::get_singleton()->get_pause_button()->set_pressed(p_breaked);
emit_signal(SNAME("breaked"), p_breaked, p_can_debug);
}
bool EditorDebuggerNode::is_skip_breakpoints() const {
return get_current_debugger()->is_skip_breakpoints();
}
bool EditorDebuggerNode::is_ignore_error_breaks() const {
return get_default_debugger()->is_ignore_error_breaks();
}
void EditorDebuggerNode::set_breakpoint(const String &p_path, int p_line, bool p_enabled) {
breakpoints[Breakpoint(p_path, p_line)] = p_enabled;
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->set_breakpoint(p_path, p_line, p_enabled);
});
emit_signal(SNAME("breakpoint_toggled"), p_path, p_line, p_enabled);
}
void EditorDebuggerNode::set_breakpoints(const String &p_path, const Array &p_lines) {
for (int i = 0; i < p_lines.size(); i++) {
set_breakpoint(p_path, p_lines[i], true);
}
for (const KeyValue<Breakpoint, bool> &E : breakpoints) {
Breakpoint b = E.key;
if (b.source == p_path && !p_lines.has(b.line)) {
set_breakpoint(p_path, b.line, false);
}
}
}
void EditorDebuggerNode::reload_all_scripts() {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->reload_all_scripts();
});
}
void EditorDebuggerNode::reload_scripts(const Vector<String> &p_script_paths) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->reload_scripts(p_script_paths);
});
}
void EditorDebuggerNode::debug_next() {
get_current_debugger()->debug_next();
}
void EditorDebuggerNode::debug_step() {
get_current_debugger()->debug_step();
}
void EditorDebuggerNode::debug_break() {
get_current_debugger()->debug_break();
}
void EditorDebuggerNode::debug_continue() {
get_current_debugger()->debug_continue();
}
String EditorDebuggerNode::get_var_value(const String &p_var) const {
return get_current_debugger()->get_var_value(p_var);
}
// LiveEdit/Inspector
void EditorDebuggerNode::request_remote_tree() {
get_current_debugger()->request_remote_tree();
}
void EditorDebuggerNode::set_remote_selection(const TypedArray<int64_t> &p_ids) {
stop_waiting_inspection();
get_current_debugger()->request_remote_objects(p_ids);
}
void EditorDebuggerNode::clear_remote_tree_selection() {
remote_scene_tree->clear_selection();
get_current_debugger()->clear_inspector(remote_scene_tree_clear_msg);
}
void EditorDebuggerNode::stop_waiting_inspection() {
inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
inspect_edited_object_wait = false;
}
bool EditorDebuggerNode::match_remote_selection(const TypedArray<uint64_t> &p_ids) const {
return p_ids == remote_scene_tree->get_selection();
}
void EditorDebuggerNode::_remote_tree_select_requested(const TypedArray<int64_t> &p_ids, int p_debugger) {
if (p_debugger == tabs->get_current_tab()) {
remote_scene_tree->select_nodes(p_ids);
}
}
void EditorDebuggerNode::_remote_tree_clear_selection_requested(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
remote_scene_tree->clear_selection();
remote_scene_tree_clear_msg = false;
get_current_debugger()->clear_inspector(false);
remote_scene_tree_clear_msg = true;
}
void EditorDebuggerNode::_remote_tree_updated(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
remote_scene_tree->clear();
remote_scene_tree->update_scene_tree(get_current_debugger()->get_remote_tree(), p_debugger);
}
void EditorDebuggerNode::_remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
if (p_button != MouseButton::LEFT) {
return;
}
TreeItem *item = Object::cast_to<TreeItem>(p_item);
ERR_FAIL_NULL(item);
if (p_id == EditorDebuggerTree::BUTTON_SUBSCENE) {
remote_scene_tree->emit_signal(SNAME("open"), item->get_meta("scene_file_path"));
} else if (p_id == EditorDebuggerTree::BUTTON_VISIBILITY) {
ObjectID obj_id = item->get_metadata(0);
ERR_FAIL_COND(obj_id.is_null());
get_current_debugger()->update_remote_object(obj_id, "visible", !item->get_meta("visible"));
get_current_debugger()->request_remote_tree();
}
}
void EditorDebuggerNode::_remote_objects_updated(EditorDebuggerRemoteObjects *p_objs, int p_debugger) {
if (p_debugger == tabs->get_current_tab() && p_objs != InspectorDock::get_inspector_singleton()->get_edited_object()) {
EditorNode::get_singleton()->push_item(p_objs);
}
}
void EditorDebuggerNode::_remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
if (obj && obj->get_instance_id() == p_id) {
InspectorDock::get_inspector_singleton()->update_property(p_property);
}
}
void EditorDebuggerNode::_remote_objects_requested(const TypedArray<uint64_t> &p_ids, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
stop_waiting_inspection();
get_current_debugger()->request_remote_objects(p_ids);
}
void EditorDebuggerNode::_remote_selection_cleared(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
stop_waiting_inspection();
get_current_debugger()->clear_inspector();
}
void EditorDebuggerNode::_save_node_requested(ObjectID p_id, const String &p_file, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
get_current_debugger()->save_node(p_id, p_file);
}
void EditorDebuggerNode::_breakpoint_set_in_tree(Ref<RefCounted> p_script, int p_line, bool p_enabled, int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
emit_signal(SNAME("breakpoint_set_in_tree"), p_script, p_line, p_enabled);
}
void EditorDebuggerNode::_breakpoints_cleared_in_tree(int p_debugger) {
if (p_debugger != tabs->get_current_tab()) {
return;
}
emit_signal(SNAME("breakpoints_cleared_in_tree"));
}
// Remote inspector/edit.
void EditorDebuggerNode::_methods_changed(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount) {
if (!singleton) {
return;
}
_for_all(singleton->tabs, [&](ScriptEditorDebugger *dbg) {
dbg->_method_changed(p_base, p_name, p_args, p_argcount);
});
}
void EditorDebuggerNode::_properties_changed(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value) {
if (!singleton) {
return;
}
_for_all(singleton->tabs, [&](ScriptEditorDebugger *dbg) {
dbg->_property_changed(p_base, p_property, p_value);
});
}
// LiveDebug
void EditorDebuggerNode::set_live_debugging(bool p_enabled) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->set_live_debugging(p_enabled);
});
}
void EditorDebuggerNode::update_live_edit_root() {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->update_live_edit_root();
});
}
void EditorDebuggerNode::live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_create_node(p_parent, p_type, p_name);
});
}
void EditorDebuggerNode::live_debug_instantiate_node(const NodePath &p_parent, const String &p_path, const String &p_name) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_instantiate_node(p_parent, p_path, p_name);
});
}
void EditorDebuggerNode::live_debug_remove_node(const NodePath &p_at) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_remove_node(p_at);
});
}
void EditorDebuggerNode::live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_remove_and_keep_node(p_at, p_keep_id);
});
}
void EditorDebuggerNode::live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_restore_node(p_id, p_at, p_at_pos);
});
}
void EditorDebuggerNode::live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_duplicate_node(p_at, p_new_name);
});
}
void EditorDebuggerNode::live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->live_debug_reparent_node(p_at, p_new_place, p_new_name, p_at_pos);
});
}
void EditorDebuggerNode::set_debug_mute_audio(bool p_mute) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->set_debug_mute_audio(p_mute);
});
debug_mute_audio = p_mute;
}
bool EditorDebuggerNode::get_debug_mute_audio() const {
return debug_mute_audio;
}
void EditorDebuggerNode::set_camera_override(CameraOverride p_override) {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
dbg->set_camera_override(p_override);
});
camera_override = p_override;
}
EditorDebuggerNode::CameraOverride EditorDebuggerNode::get_camera_override() {
return camera_override;
}
void EditorDebuggerNode::add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
ERR_FAIL_COND_MSG(p_plugin.is_null(), "Debugger plugin is null.");
ERR_FAIL_COND_MSG(debugger_plugins.has(p_plugin), "Debugger plugin already exists.");
debugger_plugins.insert(p_plugin);
Ref<EditorDebuggerPlugin> plugin = p_plugin;
for (int i = 0; get_debugger(i); i++) {
plugin->create_session(get_debugger(i));
}
}
void EditorDebuggerNode::remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
ERR_FAIL_COND_MSG(p_plugin.is_null(), "Debugger plugin is null.");
ERR_FAIL_COND_MSG(!debugger_plugins.has(p_plugin), "Debugger plugin doesn't exists.");
debugger_plugins.erase(p_plugin);
Ref<EditorDebuggerPlugin>(p_plugin)->clear();
}
bool EditorDebuggerNode::plugins_capture(ScriptEditorDebugger *p_debugger, const String &p_message, const Array &p_data) {
int session_index = tabs->get_tab_idx_from_control(p_debugger);
ERR_FAIL_COND_V(session_index < 0, false);
int colon_index = p_message.find_char(':');
ERR_FAIL_COND_V_MSG(colon_index < 1, false, "Invalid message received.");
const String cap = p_message.substr(0, colon_index);
bool parsed = false;
for (Ref<EditorDebuggerPlugin> plugin : debugger_plugins) {
if (plugin->has_capture(cap)) {
parsed |= plugin->capture(p_message, p_data, session_index);
}
}
return parsed;
}

View File

@@ -0,0 +1,234 @@
/**************************************************************************/
/* editor_debugger_node.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/script_language.h"
#include "editor/debugger/editor_debugger_server.h"
#include "scene/gui/margin_container.h"
class Button;
class DebugAdapterParser;
class EditorDebuggerPlugin;
class EditorDebuggerTree;
class EditorDebuggerRemoteObjects;
class MenuButton;
class ScriptEditorDebugger;
class TabContainer;
class UndoRedo;
class EditorDebuggerNode : public MarginContainer {
GDCLASS(EditorDebuggerNode, MarginContainer);
public:
enum CameraOverride {
OVERRIDE_NONE,
OVERRIDE_INGAME,
OVERRIDE_EDITORS,
};
private:
enum Options {
DEBUG_NEXT,
DEBUG_STEP,
DEBUG_BREAK,
DEBUG_CONTINUE,
DEBUG_WITH_EXTERNAL_EDITOR,
};
class Breakpoint {
public:
String source;
int line = 0;
static uint32_t hash(const Breakpoint &p_val) {
uint32_t h = HashMapHasherDefault::hash(p_val.source);
return hash_murmur3_one_32(p_val.line, h);
}
bool operator==(const Breakpoint &p_b) const {
return (line == p_b.line && source == p_b.source);
}
bool operator<(const Breakpoint &p_b) const {
if (line == p_b.line) {
return source < p_b.source;
}
return line < p_b.line;
}
Breakpoint() {}
Breakpoint(const String &p_source, int p_line) {
line = p_line;
source = p_source;
}
};
Ref<EditorDebuggerServer> server;
TabContainer *tabs = nullptr;
Button *debugger_button = nullptr;
MenuButton *script_menu = nullptr;
Ref<Script> stack_script; // Why?!?
bool initializing = true;
int last_error_count = 0;
int last_warning_count = 0;
bool inspect_edited_object_wait = false;
float inspect_edited_object_timeout = 0;
EditorDebuggerTree *remote_scene_tree = nullptr;
bool remote_scene_tree_wait = false;
float remote_scene_tree_timeout = 0.0;
bool remote_scene_tree_clear_msg = true;
bool auto_switch_remote_scene_tree = false;
bool debug_with_external_editor = false;
bool keep_open = false;
String current_uri;
bool debug_mute_audio = false;
CameraOverride camera_override = OVERRIDE_NONE;
HashMap<Breakpoint, bool, Breakpoint> breakpoints;
HashSet<Ref<EditorDebuggerPlugin>> debugger_plugins;
ScriptEditorDebugger *_add_debugger();
void _update_errors();
friend class DebuggerEditorPlugin;
friend class DebugAdapterParser;
static EditorDebuggerNode *singleton;
EditorDebuggerNode();
protected:
void _debugger_stopped(int p_id);
void _debugger_wants_stop(int p_id);
void _debugger_changed(int p_tab);
void _debug_data(const String &p_msg, const Array &p_data, int p_debugger);
void _remote_tree_select_requested(const TypedArray<int64_t> &p_ids, int p_debugger);
void _remote_tree_clear_selection_requested(int p_debugger);
void _remote_tree_updated(int p_debugger);
void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
void _remote_objects_updated(EditorDebuggerRemoteObjects *p_objs, int p_debugger);
void _remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger);
void _remote_objects_requested(const TypedArray<uint64_t> &p_ids, int p_debugger);
void _remote_selection_cleared(int p_debugger);
void _save_node_requested(ObjectID p_id, const String &p_file, int p_debugger);
void _breakpoint_set_in_tree(Ref<RefCounted> p_script, int p_line, bool p_enabled, int p_debugger);
void _breakpoints_cleared_in_tree(int p_debugger);
void _clear_execution(Ref<RefCounted> p_script) {
emit_signal(SNAME("clear_execution"), p_script);
}
void _text_editor_stack_goto(const ScriptEditorDebugger *p_debugger);
void _text_editor_stack_clear(const ScriptEditorDebugger *p_debugger);
void _stack_frame_selected(int p_debugger);
void _error_selected(const String &p_file, int p_line, int p_debugger);
void _breaked(bool p_breaked, bool p_can_debug, const String &p_message, bool p_has_stackdump, int p_debugger);
void _paused();
void _break_state_changed();
void _menu_option(int p_id);
void _update_debug_options();
protected:
void _notification(int p_what);
static void _bind_methods();
public:
static EditorDebuggerNode *get_singleton() { return singleton; }
void register_undo_redo(UndoRedo *p_undo_redo);
ScriptEditorDebugger *get_previous_debugger() const;
ScriptEditorDebugger *get_current_debugger() const;
ScriptEditorDebugger *get_default_debugger() const;
ScriptEditorDebugger *get_debugger(int p_debugger) const;
void debug_next();
void debug_step();
void debug_break();
void debug_continue();
void set_script_debug_button(MenuButton *p_button);
void set_tool_button(Button *p_button) {
debugger_button = p_button;
}
String get_var_value(const String &p_var) const;
Ref<Script> get_dump_stack_script() const { return stack_script; } // Why do we need this?
bool get_debug_with_external_editor() { return debug_with_external_editor; }
bool is_skip_breakpoints() const;
bool is_ignore_error_breaks() const;
void set_breakpoint(const String &p_path, int p_line, bool p_enabled);
void set_breakpoints(const String &p_path, const Array &p_lines);
void reload_all_scripts();
void reload_scripts(const Vector<String> &p_script_paths);
// Remote inspector/edit.
void request_remote_tree();
void set_remote_selection(const TypedArray<int64_t> &p_ids);
void clear_remote_tree_selection();
void stop_waiting_inspection();
bool match_remote_selection(const TypedArray<uint64_t> &p_ids) const;
static void _methods_changed(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount);
static void _properties_changed(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value);
// LiveDebug
void set_live_debugging(bool p_enabled);
void update_live_edit_root();
void live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name);
void live_debug_instantiate_node(const NodePath &p_parent, const String &p_path, const String &p_name);
void live_debug_remove_node(const NodePath &p_at);
void live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id);
void live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos);
void live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name);
void live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos);
void set_debug_mute_audio(bool p_mute);
bool get_debug_mute_audio() const;
void set_camera_override(CameraOverride p_override);
CameraOverride get_camera_override();
String get_server_uri() const;
void set_keep_open(bool p_keep_open);
Error start(const String &p_uri = "tcp://");
void stop(bool p_force = false);
bool plugins_capture(ScriptEditorDebugger *p_debugger, const String &p_message, const Array &p_data);
void add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
void remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
};

View File

@@ -0,0 +1,220 @@
/**************************************************************************/
/* editor_debugger_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_debugger_plugin.h"
#include "editor/debugger/script_editor_debugger.h"
void EditorDebuggerSession::_breaked(bool p_really_did, bool p_can_debug, const String &p_message, bool p_has_stackdump) {
if (p_really_did) {
emit_signal(SNAME("breaked"), p_can_debug);
} else {
emit_signal(SNAME("continued"));
}
}
void EditorDebuggerSession::_started() {
emit_signal(SNAME("started"));
}
void EditorDebuggerSession::_stopped() {
emit_signal(SNAME("stopped"));
}
void EditorDebuggerSession::_bind_methods() {
ClassDB::bind_method(D_METHOD("send_message", "message", "data"), &EditorDebuggerSession::send_message, DEFVAL(Array()));
ClassDB::bind_method(D_METHOD("toggle_profiler", "profiler", "enable", "data"), &EditorDebuggerSession::toggle_profiler, DEFVAL(Array()));
ClassDB::bind_method(D_METHOD("is_breaked"), &EditorDebuggerSession::is_breaked);
ClassDB::bind_method(D_METHOD("is_debuggable"), &EditorDebuggerSession::is_debuggable);
ClassDB::bind_method(D_METHOD("is_active"), &EditorDebuggerSession::is_active);
ClassDB::bind_method(D_METHOD("add_session_tab", "control"), &EditorDebuggerSession::add_session_tab);
ClassDB::bind_method(D_METHOD("remove_session_tab", "control"), &EditorDebuggerSession::remove_session_tab);
ClassDB::bind_method(D_METHOD("set_breakpoint", "path", "line", "enabled"), &EditorDebuggerSession::set_breakpoint);
ADD_SIGNAL(MethodInfo("started"));
ADD_SIGNAL(MethodInfo("stopped"));
ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "can_debug")));
ADD_SIGNAL(MethodInfo("continued"));
}
void EditorDebuggerSession::add_session_tab(Control *p_tab) {
ERR_FAIL_COND(!p_tab || !debugger);
debugger->add_debugger_tab(p_tab);
tabs.insert(p_tab);
}
void EditorDebuggerSession::remove_session_tab(Control *p_tab) {
ERR_FAIL_COND(!p_tab || !debugger);
debugger->remove_debugger_tab(p_tab);
tabs.erase(p_tab);
}
void EditorDebuggerSession::send_message(const String &p_message, const Array &p_args) {
ERR_FAIL_NULL_MSG(debugger, "Plugin is not attached to debugger.");
debugger->send_message(p_message, p_args);
}
void EditorDebuggerSession::toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data) {
ERR_FAIL_NULL_MSG(debugger, "Plugin is not attached to debugger.");
debugger->toggle_profiler(p_profiler, p_enable, p_data);
}
bool EditorDebuggerSession::is_breaked() {
ERR_FAIL_NULL_V_MSG(debugger, false, "Plugin is not attached to debugger.");
return debugger->is_breaked();
}
bool EditorDebuggerSession::is_debuggable() {
ERR_FAIL_NULL_V_MSG(debugger, false, "Plugin is not attached to debugger.");
return debugger->is_debuggable();
}
bool EditorDebuggerSession::is_active() {
ERR_FAIL_NULL_V_MSG(debugger, false, "Plugin is not attached to debugger.");
return debugger->is_session_active();
}
void EditorDebuggerSession::set_breakpoint(const String &p_path, int p_line, bool p_enabled) {
ERR_FAIL_NULL_MSG(debugger, "Plugin is not attached to debugger.");
debugger->set_breakpoint(p_path, p_line, p_enabled);
}
void EditorDebuggerSession::detach_debugger() {
if (!debugger) {
return;
}
debugger->disconnect("started", callable_mp(this, &EditorDebuggerSession::_started));
debugger->disconnect("stopped", callable_mp(this, &EditorDebuggerSession::_stopped));
debugger->disconnect("breaked", callable_mp(this, &EditorDebuggerSession::_breaked));
debugger->disconnect(SceneStringName(tree_exited), callable_mp(this, &EditorDebuggerSession::_debugger_gone_away));
for (Control *tab : tabs) {
debugger->remove_debugger_tab(tab);
}
tabs.clear();
debugger = nullptr;
}
void EditorDebuggerSession::_debugger_gone_away() {
debugger = nullptr;
tabs.clear();
}
EditorDebuggerSession::EditorDebuggerSession(ScriptEditorDebugger *p_debugger) {
ERR_FAIL_NULL(p_debugger);
debugger = p_debugger;
debugger->connect("started", callable_mp(this, &EditorDebuggerSession::_started));
debugger->connect("stopped", callable_mp(this, &EditorDebuggerSession::_stopped));
debugger->connect("breaked", callable_mp(this, &EditorDebuggerSession::_breaked));
debugger->connect(SceneStringName(tree_exited), callable_mp(this, &EditorDebuggerSession::_debugger_gone_away), CONNECT_ONE_SHOT);
}
EditorDebuggerSession::~EditorDebuggerSession() {
detach_debugger();
}
/// EditorDebuggerPlugin
EditorDebuggerPlugin::~EditorDebuggerPlugin() {
clear();
}
void EditorDebuggerPlugin::clear() {
for (Ref<EditorDebuggerSession> &session : sessions) {
session->detach_debugger();
}
sessions.clear();
}
void EditorDebuggerPlugin::create_session(ScriptEditorDebugger *p_debugger) {
sessions.push_back(Ref<EditorDebuggerSession>(memnew(EditorDebuggerSession(p_debugger))));
setup_session(sessions.size() - 1);
}
void EditorDebuggerPlugin::setup_session(int p_idx) {
GDVIRTUAL_CALL(_setup_session, p_idx);
}
Ref<EditorDebuggerSession> EditorDebuggerPlugin::get_session(int p_idx) {
ERR_FAIL_INDEX_V(p_idx, sessions.size(), nullptr);
return sessions.get(p_idx);
}
Array EditorDebuggerPlugin::get_sessions() {
Array ret;
for (const Ref<EditorDebuggerSession> &session : sessions) {
ret.push_back(session);
}
return ret;
}
bool EditorDebuggerPlugin::has_capture(const String &p_message) const {
bool ret = false;
if (GDVIRTUAL_CALL(_has_capture, p_message, ret)) {
return ret;
}
return false;
}
bool EditorDebuggerPlugin::capture(const String &p_message, const Array &p_data, int p_session_id) {
bool ret = false;
if (GDVIRTUAL_CALL(_capture, p_message, p_data, p_session_id, ret)) {
return ret;
}
return false;
}
void EditorDebuggerPlugin::goto_script_line(const Ref<Script> &p_script, int p_line) {
GDVIRTUAL_CALL(_goto_script_line, p_script, p_line);
}
void EditorDebuggerPlugin::breakpoints_cleared_in_tree() {
GDVIRTUAL_CALL(_breakpoints_cleared_in_tree);
}
void EditorDebuggerPlugin::breakpoint_set_in_tree(const Ref<Script> &p_script, int p_line, bool p_enabled) {
GDVIRTUAL_CALL(_breakpoint_set_in_tree, p_script, p_line, p_enabled);
}
void EditorDebuggerPlugin::_bind_methods() {
GDVIRTUAL_BIND(_setup_session, "session_id");
GDVIRTUAL_BIND(_has_capture, "capture");
GDVIRTUAL_BIND(_capture, "message", "data", "session_id");
GDVIRTUAL_BIND(_goto_script_line, "script", "line");
GDVIRTUAL_BIND(_breakpoints_cleared_in_tree);
GDVIRTUAL_BIND(_breakpoint_set_in_tree, "script", "line", "enabled");
ClassDB::bind_method(D_METHOD("get_session", "id"), &EditorDebuggerPlugin::get_session);
ClassDB::bind_method(D_METHOD("get_sessions"), &EditorDebuggerPlugin::get_sessions);
}
EditorDebuggerPlugin::EditorDebuggerPlugin() {
EditorDebuggerNode::get_singleton()->connect("goto_script_line", callable_mp(this, &EditorDebuggerPlugin::goto_script_line));
EditorDebuggerNode::get_singleton()->connect("breakpoints_cleared_in_tree", callable_mp(this, &EditorDebuggerPlugin::breakpoints_cleared_in_tree));
EditorDebuggerNode::get_singleton()->connect("breakpoint_set_in_tree", callable_mp(this, &EditorDebuggerPlugin::breakpoint_set_in_tree));
}

View File

@@ -0,0 +1,104 @@
/**************************************************************************/
/* editor_debugger_plugin.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/control.h"
class ScriptEditorDebugger;
class EditorDebuggerSession : public RefCounted {
GDCLASS(EditorDebuggerSession, RefCounted);
private:
HashSet<Control *> tabs;
ScriptEditorDebugger *debugger = nullptr;
void _breaked(bool p_really_did, bool p_can_debug, const String &p_message, bool p_has_stackdump);
void _started();
void _stopped();
void _debugger_gone_away();
protected:
static void _bind_methods();
public:
void detach_debugger();
void add_session_tab(Control *p_tab);
void remove_session_tab(Control *p_tab);
void send_message(const String &p_message, const Array &p_args = Array());
void toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data = Array());
bool is_breaked();
bool is_debuggable();
bool is_active();
void set_breakpoint(const String &p_path, int p_line, bool p_enabled);
EditorDebuggerSession(ScriptEditorDebugger *p_debugger);
~EditorDebuggerSession();
};
class EditorDebuggerPlugin : public RefCounted {
GDCLASS(EditorDebuggerPlugin, RefCounted);
private:
List<Ref<EditorDebuggerSession>> sessions;
protected:
static void _bind_methods();
public:
void create_session(ScriptEditorDebugger *p_debugger);
void clear();
virtual void setup_session(int p_idx);
virtual bool capture(const String &p_message, const Array &p_data, int p_session);
virtual bool has_capture(const String &p_capture) const;
Ref<EditorDebuggerSession> get_session(int p_session_id);
Array get_sessions();
GDVIRTUAL3R(bool, _capture, const String &, const Array &, int);
GDVIRTUAL1RC(bool, _has_capture, const String &);
GDVIRTUAL1(_setup_session, int);
virtual void goto_script_line(const Ref<Script> &p_script, int p_line);
virtual void breakpoints_cleared_in_tree();
virtual void breakpoint_set_in_tree(const Ref<Script> &p_script, int p_line, bool p_enabled);
GDVIRTUAL2(_goto_script_line, const Ref<Script> &, int);
GDVIRTUAL0(_breakpoints_cleared_in_tree);
GDVIRTUAL3(_breakpoint_set_in_tree, const Ref<Script> &, int, bool);
EditorDebuggerPlugin();
~EditorDebuggerPlugin();
};

View File

@@ -0,0 +1,141 @@
/**************************************************************************/
/* editor_debugger_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_debugger_server.h"
#include "core/io/tcp_server.h"
#include "core/os/thread.h"
#include "editor/editor_log.h"
#include "editor/editor_node.h"
#include "editor/settings/editor_settings.h"
class EditorDebuggerServerTCP : public EditorDebuggerServer {
private:
Ref<TCPServer> server;
String endpoint;
public:
static EditorDebuggerServer *create(const String &p_protocol);
virtual void poll() override {}
virtual String get_uri() const override;
virtual Error start(const String &p_uri) override;
virtual void stop() override;
virtual bool is_active() const override;
virtual bool is_connection_available() const override;
virtual Ref<RemoteDebuggerPeer> take_connection() override;
EditorDebuggerServerTCP();
};
EditorDebuggerServer *EditorDebuggerServerTCP::create(const String &p_protocol) {
ERR_FAIL_COND_V(p_protocol != "tcp://", nullptr);
return memnew(EditorDebuggerServerTCP);
}
EditorDebuggerServerTCP::EditorDebuggerServerTCP() {
server.instantiate();
}
String EditorDebuggerServerTCP::get_uri() const {
return endpoint;
}
Error EditorDebuggerServerTCP::start(const String &p_uri) {
// Default host and port
String bind_host = (String)EDITOR_GET("network/debug/remote_host");
int bind_port = (int)EDITOR_GET("network/debug/remote_port");
// Optionally override
if (!p_uri.is_empty() && p_uri != "tcp://") {
String scheme, path, fragment;
Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment);
ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER);
}
// Try listening on ports
const int max_attempts = 5;
for (int attempt = 1;; ++attempt) {
const Error err = server->listen(bind_port, bind_host);
if (err == OK) {
break;
}
if (attempt >= max_attempts) {
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, remote debugging unavailable.", bind_port), EditorLog::MSG_TYPE_ERROR);
return err;
}
int last_port = bind_port++;
EditorNode::get_log()->add_message(vformat("Cannot listen on port %d, trying %d instead.", last_port, bind_port), EditorLog::MSG_TYPE_WARNING);
}
// Endpoint that the client should connect to
endpoint = vformat("tcp://%s:%d", bind_host, bind_port);
return OK;
}
void EditorDebuggerServerTCP::stop() {
server->stop();
}
bool EditorDebuggerServerTCP::is_active() const {
return server->is_listening();
}
bool EditorDebuggerServerTCP::is_connection_available() const {
return server->is_listening() && server->is_connection_available();
}
Ref<RemoteDebuggerPeer> EditorDebuggerServerTCP::take_connection() {
ERR_FAIL_COND_V(!is_connection_available(), Ref<RemoteDebuggerPeer>());
return memnew(RemoteDebuggerPeerTCP(server->take_connection()));
}
/// EditorDebuggerServer
HashMap<StringName, EditorDebuggerServer::CreateServerFunc> EditorDebuggerServer::protocols;
EditorDebuggerServer *EditorDebuggerServer::create(const String &p_protocol) {
ERR_FAIL_COND_V(!protocols.has(p_protocol), nullptr);
return protocols[p_protocol](p_protocol);
}
void EditorDebuggerServer::register_protocol_handler(const String &p_protocol, CreateServerFunc p_func) {
ERR_FAIL_COND(protocols.has(p_protocol));
protocols[p_protocol] = p_func;
}
void EditorDebuggerServer::initialize() {
register_protocol_handler("tcp://", EditorDebuggerServerTCP::create);
}
void EditorDebuggerServer::deinitialize() {
protocols.clear();
}

View File

@@ -0,0 +1,57 @@
/**************************************************************************/
/* editor_debugger_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/debugger/remote_debugger_peer.h"
#include "core/object/ref_counted.h"
class EditorDebuggerServer : public RefCounted {
public:
typedef EditorDebuggerServer *(*CreateServerFunc)(const String &p_uri);
private:
static HashMap<StringName, CreateServerFunc> protocols;
public:
static void initialize();
static void deinitialize();
static void register_protocol_handler(const String &p_protocol, CreateServerFunc p_func);
static EditorDebuggerServer *create(const String &p_protocol);
virtual String get_uri() const = 0;
virtual void poll() = 0;
virtual Error start(const String &p_uri) = 0;
virtual void stop() = 0;
virtual bool is_active() const = 0;
virtual bool is_connection_available() const = 0;
virtual Ref<RemoteDebuggerPeer> take_connection() = 0;
};

View File

@@ -0,0 +1,514 @@
/**************************************************************************/
/* editor_debugger_tree.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_debugger_tree.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/docks/scene_tree_dock.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/gui/editor_toaster.h"
#include "editor/settings/editor_settings.h"
#include "scene/debugger/scene_debugger.h"
#include "scene/gui/texture_rect.h"
#include "scene/resources/packed_scene.h"
#include "servers/display_server.h"
EditorDebuggerTree::EditorDebuggerTree() {
set_v_size_flags(SIZE_EXPAND_FILL);
set_allow_rmb_select(true);
set_select_mode(SELECT_MULTI);
// Popup
item_menu = memnew(PopupMenu);
item_menu->connect(SceneStringName(id_pressed), callable_mp(this, &EditorDebuggerTree::_item_menu_id_pressed));
add_child(item_menu);
// File Dialog
file_dialog = memnew(EditorFileDialog);
file_dialog->connect("file_selected", callable_mp(this, &EditorDebuggerTree::_file_selected));
add_child(file_dialog);
accept = memnew(AcceptDialog);
add_child(accept);
}
void EditorDebuggerTree::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE: {
set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
connect("multi_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selection_changed));
connect("nothing_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_nothing_selected));
connect("item_collapsed", callable_mp(this, &EditorDebuggerTree::_scene_tree_folded));
connect("item_mouse_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_rmb_selected));
} break;
case NOTIFICATION_ENTER_TREE: {
update_icon_max_width();
} break;
}
}
void EditorDebuggerTree::_bind_methods() {
ADD_SIGNAL(MethodInfo("objects_selected", PropertyInfo(Variant::ARRAY, "object_ids"), PropertyInfo(Variant::INT, "debugger")));
ADD_SIGNAL(MethodInfo("selection_cleared", PropertyInfo(Variant::INT, "debugger")));
ADD_SIGNAL(MethodInfo("save_node", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "filename"), PropertyInfo(Variant::INT, "debugger")));
ADD_SIGNAL(MethodInfo("open"));
}
void EditorDebuggerTree::_scene_tree_selection_changed(TreeItem *p_item, int p_column, bool p_selected) {
if (updating_scene_tree || !p_item) {
return;
}
uint64_t id = uint64_t(p_item->get_metadata(0));
if (p_selected) {
if (inspected_object_ids.size() == (int)EDITOR_GET("debugger/max_node_selection")) {
selection_surpassed_limit = true;
p_item->deselect(0);
} else if (!inspected_object_ids.has(id)) {
inspected_object_ids.append(id);
}
} else if (inspected_object_ids.has(id)) {
inspected_object_ids.erase(id);
}
if (!notify_selection_queued) {
callable_mp(this, &EditorDebuggerTree::_notify_selection_changed).call_deferred();
notify_selection_queued = true;
}
}
void EditorDebuggerTree::_scene_tree_nothing_selected() {
deselect_all();
inspected_object_ids.clear();
emit_signal(SNAME("selection_cleared"), debugger_id);
}
void EditorDebuggerTree::_notify_selection_changed() {
notify_selection_queued = false;
if (inspected_object_ids.is_empty()) {
emit_signal(SNAME("selection_cleared"), debugger_id);
} else {
emit_signal(SNAME("objects_selected"), inspected_object_ids.duplicate(), debugger_id);
}
if (selection_surpassed_limit) {
selection_surpassed_limit = false;
EditorToaster::get_singleton()->popup_str(vformat(TTR("Some remote nodes were not selected, as the configured maximum selection is %d. This can be changed at \"debugger/max_node_selection\" in the Editor Settings."), EDITOR_GET("debugger/max_node_selection")), EditorToaster::SEVERITY_WARNING);
}
}
void EditorDebuggerTree::_scene_tree_folded(Object *p_obj) {
if (updating_scene_tree) {
return;
}
TreeItem *item = Object::cast_to<TreeItem>(p_obj);
if (!item) {
return;
}
ObjectID id = ObjectID(uint64_t(item->get_metadata(0)));
if (unfold_cache.has(id)) {
unfold_cache.erase(id);
} else {
unfold_cache.insert(id);
}
}
void EditorDebuggerTree::_scene_tree_rmb_selected(const Vector2 &p_position, MouseButton p_button) {
if (p_button != MouseButton::RIGHT) {
return;
}
TreeItem *item = get_item_at_position(p_position);
if (!item) {
return;
}
item->select(0);
item_menu->clear();
item_menu->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTR("Save Branch as Scene..."), ITEM_MENU_SAVE_REMOTE_NODE);
item_menu->add_icon_item(get_editor_theme_icon(SNAME("CopyNodePath")), TTR("Copy Node Path"), ITEM_MENU_COPY_NODE_PATH);
item_menu->add_icon_item(get_editor_theme_icon(SNAME("Collapse")), TTR("Expand/Collapse Branch"), ITEM_MENU_EXPAND_COLLAPSE);
item_menu->set_position(get_screen_position() + get_local_mouse_position());
item_menu->reset_size();
item_menu->popup();
}
/// Populates inspect_scene_tree given data in nodes as a flat list, encoded depth first.
///
/// Given a nodes array like [R,A,B,C,D,E] the following Tree will be generated, assuming
/// filter is an empty String, R and A child count are 2, B is 1 and C, D and E are 0.
///
/// R
/// |-A
/// | |-B
/// | | |-C
/// | |
/// | |-D
/// |
/// |-E
///
void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger) {
set_hide_root(false);
updating_scene_tree = true;
const String last_path = get_selected_path();
const String filter = SceneTreeDock::get_singleton()->get_filter();
LocalVector<TreeItem *> select_items;
bool hide_filtered_out_parents = EDITOR_GET("docks/scene_tree/hide_filtered_out_parents");
bool should_scroll = scrolling_to_item || filter != last_filter;
scrolling_to_item = false;
TreeItem *scroll_item = nullptr;
TypedArray<uint64_t> ids_present;
// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
List<ParentItem> parents;
for (const SceneDebuggerTree::RemoteNode &node : p_tree->nodes) {
TreeItem *parent = nullptr;
Pair<TreeItem *, TreeItem *> move_from_to;
if (parents.size()) { // Find last parent.
ParentItem &p = parents.front()->get();
parent = p.tree_item;
if (!(--p.child_count)) { // If no child left, remove it.
parents.pop_front();
if (hide_filtered_out_parents && !filter.is_subsequence_ofn(parent->get_text(0))) {
if (parent == get_root()) {
set_hide_root(true);
} else {
move_from_to.first = parent;
// Find the closest ancestor that matches the filter.
for (const ParentItem p2 : parents) {
move_from_to.second = p2.tree_item;
if (p2.matches_filter || move_from_to.second == get_root()) {
break;
}
}
if (!move_from_to.second) {
move_from_to.second = get_root();
}
}
}
}
}
// Add this node.
TreeItem *item = create_item(parent);
item->set_text(0, node.name);
if (node.scene_file_path.is_empty()) {
item->set_tooltip_text(0, node.name + "\n" + TTR("Type:") + " " + node.type_name);
} else {
item->set_tooltip_text(0, node.name + "\n" + TTR("Instance:") + " " + node.scene_file_path + "\n" + TTR("Type:") + " " + node.type_name);
}
Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(node.type_name, "");
if (icon.is_valid()) {
item->set_icon(0, icon);
}
item->set_metadata(0, node.id);
String current_path;
if (parent) {
current_path += (String)parent->get_meta("node_path");
// Set current item as collapsed if necessary (root is never collapsed).
if (!unfold_cache.has(node.id)) {
item->set_collapsed(true);
}
}
item->set_meta("node_path", current_path + "/" + item->get_text(0));
// Select previously selected nodes.
if (debugger_id == p_debugger) { // Can use remote id.
if (inspected_object_ids.has(uint64_t(node.id))) {
ids_present.append(node.id);
if (selection_uncollapse_all) {
selection_uncollapse_all = false;
// Temporarily set to `false`, to allow caching the unfolds.
updating_scene_tree = false;
item->uncollapse_tree();
updating_scene_tree = true;
}
select_items.push_back(item);
if (should_scroll) {
scroll_item = item;
}
}
} else if (last_path == (String)item->get_meta("node_path")) { // Must use path.
updating_scene_tree = false; // Force emission of new selections.
select_items.push_back(item);
if (should_scroll) {
scroll_item = item;
}
updating_scene_tree = true;
}
// Add buttons.
const Color remote_button_color = Color(1, 1, 1, 0.8);
if (!node.scene_file_path.is_empty()) {
String node_scene_file_path = node.scene_file_path;
Ref<Texture2D> button_icon = get_editor_theme_icon(SNAME("InstanceOptions"));
String tooltip = vformat(TTR("This node has been instantiated from a PackedScene file:\n%s\nClick to open the original file in the Editor."), node_scene_file_path);
item->set_meta("scene_file_path", node_scene_file_path);
item->add_button(0, button_icon, BUTTON_SUBSCENE, false, tooltip);
item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color);
}
if (node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_HAS_VISIBLE_METHOD) {
bool node_visible = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE;
bool node_visible_in_tree = node.view_flags & SceneDebuggerTree::RemoteNode::VIEW_VISIBLE_IN_TREE;
Ref<Texture2D> button_icon = get_editor_theme_icon(node_visible ? SNAME("GuiVisibilityVisible") : SNAME("GuiVisibilityHidden"));
String tooltip = TTR("Toggle Visibility");
item->set_meta("visible", node_visible);
item->add_button(0, button_icon, BUTTON_VISIBILITY, false, tooltip);
if (ClassDB::is_parent_class(node.type_name, "CanvasItem") || ClassDB::is_parent_class(node.type_name, "Node3D")) {
item->set_button_color(0, item->get_button_count(0) - 1, node_visible_in_tree ? remote_button_color : Color(1, 1, 1, 0.6));
} else {
item->set_button_color(0, item->get_button_count(0) - 1, remote_button_color);
}
}
// Add in front of the parents stack if children are expected.
if (node.child_count) {
parents.push_front(ParentItem(item, node.child_count, filter.is_subsequence_ofn(item->get_text(0))));
} else {
// Apply filters.
while (parent) {
const bool had_siblings = item->get_prev() || item->get_next();
if (filter.is_subsequence_ofn(item->get_text(0))) {
break; // Filter matches, must survive.
}
if (select_items.has(item) || scroll_item == item) {
select_items.resize(select_items.size() - 1);
scroll_item = nullptr;
}
parent->remove_child(item);
memdelete(item);
if (had_siblings) {
break; // Parent must survive.
}
item = parent;
parent = item->get_parent();
// Check if parent expects more children.
for (ParentItem &pair : parents) {
if (pair.tree_item == item) {
parent = nullptr;
break; // Might have more children.
}
}
}
}
// Move all children to the ancestor that matches the filter, if picked.
if (move_from_to.first) {
TreeItem *from = move_from_to.first;
TypedArray<TreeItem> children = from->get_children();
if (!children.is_empty()) {
for (Variant &c : children) {
TreeItem *ti = Object::cast_to<TreeItem>(c);
from->remove_child(ti);
move_from_to.second->add_child(ti);
}
from->get_parent()->remove_child(from);
memdelete(from);
if (select_items.has(from) || scroll_item == from) {
select_items.erase(from);
scroll_item = nullptr;
}
}
}
}
inspected_object_ids = ids_present;
debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree.
for (TreeItem *item : select_items) {
item->select(0);
}
if (scroll_item) {
scroll_to_item(scroll_item, false);
}
last_filter = filter;
updating_scene_tree = false;
}
void EditorDebuggerTree::select_nodes(const TypedArray<int64_t> &p_ids) {
// Manually select, as the tree control may be out-of-date for some reason (e.g. not shown yet).
selection_uncollapse_all = true;
inspected_object_ids = p_ids;
scrolling_to_item = true;
if (!updating_scene_tree) {
// Request a tree refresh.
EditorDebuggerNode::get_singleton()->request_remote_tree();
}
// Set the value immediately, so no update flooding happens and causes a crash.
updating_scene_tree = true;
}
void EditorDebuggerTree::clear_selection() {
inspected_object_ids.clear();
if (!updating_scene_tree) {
// Request a tree refresh.
EditorDebuggerNode::get_singleton()->request_remote_tree();
}
// Set the value immediately, so no update flooding happens and causes a crash.
updating_scene_tree = true;
}
Variant EditorDebuggerTree::get_drag_data(const Point2 &p_point) {
if (get_button_id_at_position(p_point) != -1) {
return Variant();
}
TreeItem *selected = get_selected();
if (!selected) {
return Variant();
}
String path = selected->get_text(0);
const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
HBoxContainer *hb = memnew(HBoxContainer);
TextureRect *tf = memnew(TextureRect);
tf->set_texture(selected->get_icon(0));
tf->set_custom_minimum_size(Size2(icon_size, icon_size));
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
tf->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
hb->add_child(tf);
Label *label = memnew(Label(path));
hb->add_child(label);
set_drag_preview(hb);
if (!selected->get_parent() || !selected->get_parent()->get_parent()) {
path = ".";
} else {
while (selected->get_parent()->get_parent() != get_root()) {
selected = selected->get_parent();
path = selected->get_text(0) + "/" + path;
}
}
return vformat("\"%s\"", path);
}
void EditorDebuggerTree::update_icon_max_width() {
add_theme_constant_override("icon_max_width", get_theme_constant("class_icon_size", EditorStringName(Editor)));
}
String EditorDebuggerTree::get_selected_path() {
if (!get_selected()) {
return "";
}
return get_selected()->get_meta("node_path");
}
void EditorDebuggerTree::_item_menu_id_pressed(int p_option) {
switch (p_option) {
case ITEM_MENU_SAVE_REMOTE_NODE: {
file_dialog->set_access(EditorFileDialog::ACCESS_RESOURCES);
file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
List<String> extensions;
Ref<PackedScene> sd = memnew(PackedScene);
ResourceSaver::get_recognized_extensions(sd, &extensions);
file_dialog->clear_filters();
for (const String &extension : extensions) {
file_dialog->add_filter("*." + extension, extension.to_upper());
}
String filename = get_selected_path().get_file() + "." + extensions.front()->get().to_lower();
file_dialog->set_current_path(filename);
file_dialog->popup_file_dialog();
} break;
case ITEM_MENU_COPY_NODE_PATH: {
String text = get_selected_path();
if (text.is_empty()) {
return;
} else if (text == "/root") {
text = ".";
} else {
text = text.replace("/root/", "");
int slash = text.find_char('/');
if (slash < 0) {
text = ".";
} else {
text = text.substr(slash + 1);
}
}
DisplayServer::get_singleton()->clipboard_set(text);
} break;
case ITEM_MENU_EXPAND_COLLAPSE: {
TreeItem *s_item = get_selected();
if (!s_item) {
s_item = get_root();
if (!s_item) {
break;
}
}
bool collapsed = s_item->is_any_collapsed();
s_item->set_collapsed_recursive(!collapsed);
ensure_cursor_is_visible();
}
}
}
void EditorDebuggerTree::_file_selected(const String &p_file) {
if (inspected_object_ids.size() != 1) {
accept->set_text(vformat(TTR("Saving the branch as a scene requires selecting only one node, but you have selected %d nodes."), inspected_object_ids.size()));
accept->popup_centered();
return;
}
emit_signal(SNAME("save_node"), inspected_object_ids[0], p_file, debugger_id);
}

View File

@@ -0,0 +1,104 @@
/**************************************************************************/
/* editor_debugger_tree.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/tree.h"
class AcceptDialog;
class SceneDebuggerTree;
class EditorFileDialog;
class EditorDebuggerTree : public Tree {
GDCLASS(EditorDebuggerTree, Tree);
private:
struct ParentItem {
TreeItem *tree_item;
int child_count;
bool matches_filter;
ParentItem(TreeItem *p_tree_item = nullptr, int p_child_count = 0, bool p_matches_filter = false) {
tree_item = p_tree_item;
child_count = p_child_count;
matches_filter = p_matches_filter;
}
};
enum ItemMenu {
ITEM_MENU_SAVE_REMOTE_NODE,
ITEM_MENU_COPY_NODE_PATH,
ITEM_MENU_EXPAND_COLLAPSE,
};
TypedArray<uint64_t> inspected_object_ids;
int debugger_id = 0;
bool updating_scene_tree = false;
bool scrolling_to_item = false;
bool notify_selection_queued = false;
bool selection_surpassed_limit = false;
bool selection_uncollapse_all = false;
HashSet<ObjectID> unfold_cache;
PopupMenu *item_menu = nullptr;
EditorFileDialog *file_dialog = nullptr;
AcceptDialog *accept = nullptr;
String last_filter;
void _scene_tree_folded(Object *p_obj);
void _scene_tree_selection_changed(TreeItem *p_item, int p_column, bool p_selected);
void _scene_tree_nothing_selected();
void _notify_selection_changed();
void _scene_tree_rmb_selected(const Vector2 &p_position, MouseButton p_button);
void _item_menu_id_pressed(int p_option);
void _file_selected(const String &p_file);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
enum Button {
BUTTON_SUBSCENE = 0,
BUTTON_VISIBILITY = 1,
};
virtual Variant get_drag_data(const Point2 &p_point) override;
void update_icon_max_width();
String get_selected_path();
ObjectID get_selected_object();
int get_current_debugger(); // Would love to have one tree for every debugger.
inline TypedArray<uint64_t> get_selection() const { return inspected_object_ids.duplicate(); }
void update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger);
void select_nodes(const TypedArray<int64_t> &p_ids);
void clear_selection();
EditorDebuggerTree();
};

View File

@@ -0,0 +1,148 @@
/**************************************************************************/
/* editor_expression_evaluator.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_expression_evaluator.h"
#include "editor/debugger/editor_debugger_inspector.h"
#include "editor/debugger/script_editor_debugger.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/line_edit.h"
void EditorExpressionEvaluator::on_start() {
expression_input->set_editable(false);
evaluate_btn->set_disabled(true);
if (clear_on_run_checkbox->is_pressed()) {
inspector->clear_stack_variables();
}
}
void EditorExpressionEvaluator::set_editor_debugger(ScriptEditorDebugger *p_editor_debugger) {
editor_debugger = p_editor_debugger;
}
void EditorExpressionEvaluator::add_value(const Array &p_array) {
inspector->add_stack_variable(p_array, 0);
inspector->set_v_scroll(0);
inspector->set_h_scroll(0);
}
void EditorExpressionEvaluator::_evaluate() {
const String &expression = expression_input->get_text();
if (expression.is_empty()) {
return;
}
if (!editor_debugger->is_session_active()) {
return;
}
editor_debugger->request_remote_evaluate(expression, editor_debugger->get_stack_script_frame());
expression_input->clear();
}
void EditorExpressionEvaluator::_clear() {
inspector->clear_stack_variables();
}
void EditorExpressionEvaluator::_remote_object_selected(ObjectID p_id) {
Array arr = { p_id };
editor_debugger->emit_signal(SNAME("remote_objects_requested"), arr);
}
void EditorExpressionEvaluator::_on_expression_input_changed(const String &p_expression) {
evaluate_btn->set_disabled(p_expression.is_empty());
}
void EditorExpressionEvaluator::_on_debugger_breaked(bool p_breaked, bool p_can_debug) {
expression_input->set_editable(p_breaked);
evaluate_btn->set_disabled(!p_breaked);
}
void EditorExpressionEvaluator::_on_debugger_clear_execution(Ref<Script> p_stack_script) {
expression_input->set_editable(false);
evaluate_btn->set_disabled(true);
}
void EditorExpressionEvaluator::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
EditorDebuggerNode::get_singleton()->connect("breaked", callable_mp(this, &EditorExpressionEvaluator::_on_debugger_breaked));
EditorDebuggerNode::get_singleton()->connect("clear_execution", callable_mp(this, &EditorExpressionEvaluator::_on_debugger_clear_execution));
} break;
}
}
EditorExpressionEvaluator::EditorExpressionEvaluator() {
set_h_size_flags(SIZE_EXPAND_FILL);
HBoxContainer *hb = memnew(HBoxContainer);
add_child(hb);
expression_input = memnew(LineEdit);
expression_input->set_h_size_flags(Control::SIZE_EXPAND_FILL);
expression_input->set_placeholder(TTR("Expression to evaluate"));
expression_input->set_accessibility_name(TTRC("Expression to evaluate"));
expression_input->set_clear_button_enabled(true);
expression_input->connect(SceneStringName(text_submitted), callable_mp(this, &EditorExpressionEvaluator::_evaluate).unbind(1));
expression_input->connect(SceneStringName(text_changed), callable_mp(this, &EditorExpressionEvaluator::_on_expression_input_changed));
hb->add_child(expression_input);
clear_on_run_checkbox = memnew(CheckBox);
clear_on_run_checkbox->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
clear_on_run_checkbox->set_text(TTR("Clear on Run"));
clear_on_run_checkbox->set_pressed(true);
hb->add_child(clear_on_run_checkbox);
evaluate_btn = memnew(Button);
evaluate_btn->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
evaluate_btn->set_text(TTR("Evaluate"));
evaluate_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorExpressionEvaluator::_evaluate));
hb->add_child(evaluate_btn);
clear_btn = memnew(Button);
clear_btn->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
clear_btn->set_text(TTR("Clear"));
clear_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorExpressionEvaluator::_clear));
hb->add_child(clear_btn);
inspector = memnew(EditorDebuggerInspector);
inspector->set_v_size_flags(SIZE_EXPAND_FILL);
inspector->set_property_name_style(EditorPropertyNameProcessor::STYLE_RAW);
inspector->set_read_only(true);
inspector->connect("object_selected", callable_mp(this, &EditorExpressionEvaluator::_remote_object_selected));
inspector->set_use_filter(true);
add_child(inspector);
expression_input->set_editable(false);
evaluate_btn->set_disabled(true);
}

View File

@@ -0,0 +1,74 @@
/**************************************************************************/
/* editor_expression_evaluator.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/box_container.h"
class Button;
class CheckBox;
class EditorDebuggerInspector;
class LineEdit;
class RemoteDebuggerPeer;
class ScriptEditorDebugger;
class EditorExpressionEvaluator : public VBoxContainer {
GDCLASS(EditorExpressionEvaluator, VBoxContainer)
private:
Ref<RemoteDebuggerPeer> peer;
LineEdit *expression_input = nullptr;
CheckBox *clear_on_run_checkbox = nullptr;
Button *evaluate_btn = nullptr;
Button *clear_btn = nullptr;
EditorDebuggerInspector *inspector = nullptr;
void _evaluate();
void _clear();
void _remote_object_selected(ObjectID p_id);
void _on_expression_input_changed(const String &p_expression);
void _on_debugger_breaked(bool p_breaked, bool p_can_debug);
void _on_debugger_clear_execution(Ref<Script> p_stack_script);
protected:
ScriptEditorDebugger *editor_debugger = nullptr;
void _notification(int p_what);
public:
void on_start();
void set_editor_debugger(ScriptEditorDebugger *p_editor_debugger);
void add_value(const Array &p_array);
EditorExpressionEvaluator();
};

View File

@@ -0,0 +1,277 @@
/**************************************************************************/
/* editor_file_server.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_file_server.h"
#include "editor/editor_node.h"
#include "editor/export/editor_export_platform.h"
#include "editor/settings/editor_settings.h"
#define FILESYSTEM_PROTOCOL_VERSION 1
#define PASSWORD_LENGTH 32
#define MAX_FILE_BUFFER_SIZE 100 * 1024 * 1024 // 100mb max file buffer size (description of files to update, compressed).
static void _add_file(String f, const uint64_t &p_modified_time, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
f = f.replace_first("res://", ""); // remove res://
const uint64_t *cached_mt = cached_files.getptr(f);
if (cached_mt && *cached_mt == p_modified_time) {
// File is good, skip it.
cached_files.erase(f); // Erase to mark this file as existing. Remaining files not added to files_to_send will be considered erased here, so they need to be erased in the client too.
return;
}
files_to_send.insert(f, p_modified_time);
}
void EditorFileServer::_scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
for (int i = 0; i < efd->get_file_count(); i++) {
String f = efd->get_file_path(i);
if (FileAccess::exists(f + ".import")) {
// is imported, determine what to do
// Todo the modified times of remapped files should most likely be kept in EditorFileSystem to speed this up in the future.
Ref<ConfigFile> cf;
cf.instantiate();
Error err = cf->load(f + ".import");
ERR_CONTINUE(err != OK);
{
uint64_t mt = FileAccess::get_modified_time(f + ".import");
_add_file(f + ".import", mt, files_to_send, cached_files);
}
if (!cf->has_section("remap")) {
continue;
}
Vector<String> remaps = cf->get_section_keys("remap");
for (const String &remap : remaps) {
if (remap == "path") {
String remapped_path = cf->get_value("remap", remap);
uint64_t mt = FileAccess::get_modified_time(remapped_path);
_add_file(remapped_path, mt, files_to_send, cached_files);
} else if (remap.begins_with("path.")) {
String feature = remap.get_slicec('.', 1);
if (p_tags.has(feature)) {
String remapped_path = cf->get_value("remap", remap);
uint64_t mt = FileAccess::get_modified_time(remapped_path);
_add_file(remapped_path, mt, files_to_send, cached_files);
}
}
}
} else {
uint64_t mt = efd->get_file_modified_time(i);
_add_file(f, mt, files_to_send, cached_files);
}
}
for (int i = 0; i < efd->get_subdir_count(); i++) {
_scan_files_changed(efd->get_subdir(i), p_tags, files_to_send, cached_files);
}
}
static void _add_custom_file(const String &f, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {
if (!FileAccess::exists(f)) {
return;
}
_add_file(f, FileAccess::get_modified_time(f), files_to_send, cached_files);
}
void EditorFileServer::poll() {
if (!active) {
return;
}
if (!server->is_connection_available()) {
return;
}
Ref<StreamPeerTCP> tcp_peer = server->take_connection();
ERR_FAIL_COND(tcp_peer.is_null());
// Got a connection!
EditorProgress pr("updating_remote_file_system", TTR("Updating assets on target device:"), 105);
pr.step(TTR("Syncing headers"), 0, true);
print_verbose("EFS: Connecting taken!");
char header[4];
Error err = tcp_peer->get_data((uint8_t *)&header, 4);
ERR_FAIL_COND(err != OK);
ERR_FAIL_COND(header[0] != 'G');
ERR_FAIL_COND(header[1] != 'R');
ERR_FAIL_COND(header[2] != 'F');
ERR_FAIL_COND(header[3] != 'S');
uint32_t protocol_version = tcp_peer->get_u32();
ERR_FAIL_COND(protocol_version != FILESYSTEM_PROTOCOL_VERSION);
char cpassword[PASSWORD_LENGTH + 1];
err = tcp_peer->get_data((uint8_t *)cpassword, PASSWORD_LENGTH);
cpassword[PASSWORD_LENGTH] = 0;
ERR_FAIL_COND(err != OK);
print_verbose("EFS: Got password: " + String(cpassword));
ERR_FAIL_COND_MSG(password != cpassword, "Client disconnected because password mismatch.");
uint32_t tag_count = tcp_peer->get_u32();
print_verbose("EFS: Getting tags: " + itos(tag_count));
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
Vector<String> tags;
for (uint32_t i = 0; i < tag_count; i++) {
String tag = tcp_peer->get_utf8_string();
print_verbose("EFS: tag #" + itos(i) + ": " + tag);
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
tags.push_back(tag);
}
uint32_t file_buffer_decompressed_size = tcp_peer->get_32();
HashMap<String, uint64_t> cached_files;
if (file_buffer_decompressed_size > 0) {
pr.step(TTR("Getting remote file system"), 1, true);
// Got files cached by client.
uint32_t file_buffer_size = tcp_peer->get_32();
print_verbose("EFS: Getting file buffer: compressed - " + String::humanize_size(file_buffer_size) + " decompressed: " + String::humanize_size(file_buffer_decompressed_size));
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
ERR_FAIL_COND(file_buffer_size > MAX_FILE_BUFFER_SIZE);
LocalVector<uint8_t> file_buffer;
file_buffer.resize(file_buffer_size);
LocalVector<uint8_t> file_buffer_decompressed;
file_buffer_decompressed.resize(file_buffer_decompressed_size);
err = tcp_peer->get_data(file_buffer.ptr(), file_buffer_size);
pr.step(TTR("Decompressing remote file system"), 2, true);
ERR_FAIL_COND(err != OK);
// Decompress the text with all the files
const int64_t decompressed_size = Compression::decompress(file_buffer_decompressed.ptr(), file_buffer_decompressed.size(), file_buffer.ptr(), file_buffer.size(), Compression::MODE_ZSTD);
ERR_FAIL_COND_MSG(decompressed_size != file_buffer_decompressed.size(), "Error decompressing file buffer. Decompressed size did not match the expected size.");
String files_text = String::utf8((const char *)file_buffer_decompressed.ptr(), file_buffer_decompressed.size());
Vector<String> files = files_text.split("\n");
print_verbose("EFS: Total cached files received: " + itos(files.size()));
for (int i = 0; i < files.size(); i++) {
if (files[i].get_slice_count("::") != 2) {
continue;
}
String file = files[i].get_slice("::", 0);
uint64_t modified_time = files[i].get_slice("::", 1).to_int();
cached_files.insert(file, modified_time);
}
} else {
// Client does not have any files stored.
}
pr.step(TTR("Scanning for local changes"), 3, true);
print_verbose("EFS: Scanning changes:");
HashMap<String, uint64_t> files_to_send;
// Scan files to send.
_scan_files_changed(EditorFileSystem::get_singleton()->get_filesystem(), tags, files_to_send, cached_files);
// Add forced export files
Vector<String> forced_export = EditorExportPlatform::get_forced_export_files(Ref<EditorExportPreset>());
for (int i = 0; i < forced_export.size(); i++) {
_add_custom_file(forced_export[i], files_to_send, cached_files);
}
_add_custom_file("res://project.godot", files_to_send, cached_files);
// Check which files were removed and also add them
for (KeyValue<String, uint64_t> K : cached_files) {
if (!files_to_send.has(K.key)) {
files_to_send.insert(K.key, 0); //0 means removed
}
}
tcp_peer->put_32(files_to_send.size());
print_verbose("EFS: Sending list of changed files.");
pr.step(TTR("Sending list of changed files:"), 4, true);
// Send list of changed files first, to ensure that if connecting breaks, the client is not found in a broken state.
for (KeyValue<String, uint64_t> K : files_to_send) {
tcp_peer->put_utf8_string(K.key);
tcp_peer->put_64(K.value);
}
print_verbose("EFS: Sending " + itos(files_to_send.size()) + " files.");
int idx = 0;
for (KeyValue<String, uint64_t> K : files_to_send) {
pr.step(TTR("Sending file:") + " " + K.key.get_file(), 5 + idx * 100 / files_to_send.size(), false);
idx++;
if (K.value == 0 || !FileAccess::exists("res://" + K.key)) { // File was removed
continue;
}
Vector<uint8_t> array = FileAccess::_get_file_as_bytes("res://" + K.key);
tcp_peer->put_64(array.size());
tcp_peer->put_data(array.ptr(), array.size());
ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);
}
tcp_peer->put_data((const uint8_t *)"GEND", 4); // End marker.
print_verbose("EFS: Done.");
}
void EditorFileServer::start() {
if (active) {
stop();
}
port = EDITOR_GET("filesystem/file_server/port");
password = EDITOR_GET("filesystem/file_server/password");
Error err = server->listen(port);
ERR_FAIL_COND_MSG(err != OK, "EditorFileServer: Unable to listen on port " + itos(port));
active = true;
}
bool EditorFileServer::is_active() const {
return active;
}
void EditorFileServer::stop() {
if (active) {
server->stop();
active = false;
}
}
EditorFileServer::EditorFileServer() {
server.instantiate();
}
EditorFileServer::~EditorFileServer() {
stop();
}

View File

@@ -0,0 +1,56 @@
/**************************************************************************/
/* editor_file_server.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/io/tcp_server.h"
#include "core/os/thread.h"
#include "editor/file_system/editor_file_system.h"
class EditorFileServer : public Object {
GDCLASS(EditorFileServer, Object);
Ref<TCPServer> server;
String password;
int port = 0;
bool active = false;
void _scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files);
public:
void poll();
void start();
void stop();
bool is_active() const;
EditorFileServer();
~EditorFileServer();
};

View File

@@ -0,0 +1,438 @@
/**************************************************************************/
/* editor_performance_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_performance_profiler.h"
#include "editor/editor_string_names.h"
#include "editor/inspector/editor_property_name_processor.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
#include "main/performance.h"
EditorPerformanceProfiler::Monitor::Monitor(const String &p_name, const String &p_base, int p_frame_index, Performance::MonitorType p_type, TreeItem *p_item) {
type = p_type;
item = p_item;
frame_index = p_frame_index;
name = p_name;
base = p_base;
}
void EditorPerformanceProfiler::Monitor::update_value(float p_value) {
ERR_FAIL_NULL(item);
String label = EditorPerformanceProfiler::_create_label(p_value, type);
String tooltip = label;
switch (type) {
case Performance::MONITOR_TYPE_MEMORY: {
tooltip = label;
} break;
case Performance::MONITOR_TYPE_TIME: {
tooltip = label;
} break;
default: {
tooltip += " " + item->get_text(0);
} break;
}
item->set_text(1, label);
item->set_tooltip_text(1, tooltip);
if (p_value > max) {
max = p_value;
}
}
void EditorPerformanceProfiler::Monitor::reset() {
history.clear();
max = 0.0f;
if (item) {
item->set_text(1, "");
item->set_tooltip_text(1, "");
}
}
String EditorPerformanceProfiler::_create_label(float p_value, Performance::MonitorType p_type) {
switch (p_type) {
case Performance::MONITOR_TYPE_QUANTITY: {
return TS->format_number(itos(p_value));
}
case Performance::MONITOR_TYPE_MEMORY: {
return String::humanize_size(p_value);
}
case Performance::MONITOR_TYPE_TIME: {
return TS->format_number(rtos(p_value * 1000).pad_decimals(2)) + " " + TTR("ms");
}
default: {
return TS->format_number(rtos(p_value));
}
}
}
void EditorPerformanceProfiler::_monitor_select() {
monitor_draw->queue_redraw();
}
void EditorPerformanceProfiler::_monitor_draw() {
Vector<StringName> active;
for (const KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item->is_checked(0)) {
active.push_back(E.key);
}
}
if (active.is_empty()) {
info_message->show();
return;
}
info_message->hide();
Ref<StyleBox> graph_style_box = get_theme_stylebox(CoreStringName(normal), SNAME("TextEdit"));
Ref<Font> graph_font = get_theme_font(SceneStringName(font), SNAME("TextEdit"));
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("TextEdit"));
int columns = int(Math::ceil(Math::sqrt(float(active.size()))));
int rows = int(Math::ceil(float(active.size()) / float(columns)));
if (active.size() == 1) {
rows = 1;
}
Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows);
float spacing = float(POINT_SEPARATION) / float(columns);
float value_multiplier = EditorThemeManager::is_dark_theme() ? 1.4f : 0.55f;
float hue_shift = 1.0f / float(monitors.size());
for (int i = 0; i < active.size(); i++) {
Monitor &current = monitors[active[i]];
Rect2i rect(Point2i(i % columns, i / columns) * cell_size + Point2i(MARGIN, MARGIN), cell_size - Point2i(MARGIN, MARGIN) * 2);
monitor_draw->draw_style_box(graph_style_box, rect);
rect.position += graph_style_box->get_offset();
rect.size -= graph_style_box->get_minimum_size();
Color draw_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
draw_color.set_hsv(Math::fmod(hue_shift * float(current.frame_index), 0.9f), draw_color.get_s() * 0.9f, draw_color.get_v() * value_multiplier, 0.6f);
monitor_draw->draw_string(graph_font, rect.position + Point2(0, graph_font->get_ascent(font_size)), current.item->get_text(0), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, draw_color);
draw_color.a = 0.9f;
float value_position = rect.size.width - graph_font->get_string_size(current.item->get_text(1), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width;
if (value_position < 0) {
value_position = 0;
}
monitor_draw->draw_string(graph_font, rect.position + Point2(value_position, graph_font->get_ascent(font_size)), current.item->get_text(1), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, draw_color);
rect.position.y += graph_font->get_height(font_size);
rect.size.height -= graph_font->get_height(font_size);
int line_count = rect.size.height / (graph_font->get_height(font_size) * 2);
if (line_count > 5) {
line_count = 5;
}
if (line_count > 0) {
Color horizontal_line_color;
horizontal_line_color.set_hsv(draw_color.get_h(), draw_color.get_s() * 0.5f, draw_color.get_v() * 0.5f, 0.3f);
monitor_draw->draw_line(rect.position, rect.position + Vector2(rect.size.width, 0), horizontal_line_color, Math::round(EDSCALE));
monitor_draw->draw_string(graph_font, rect.position + Vector2(0, graph_font->get_ascent(font_size)), _create_label(current.max, current.type), HORIZONTAL_ALIGNMENT_LEFT, rect.size.width, font_size, horizontal_line_color);
for (int j = 0; j < line_count; j++) {
Vector2 y_offset = Vector2(0, rect.size.height * (1.0f - float(j) / float(line_count)));
monitor_draw->draw_line(rect.position + y_offset, rect.position + Vector2(rect.size.width, 0) + y_offset, horizontal_line_color, Math::round(EDSCALE));
monitor_draw->draw_string(graph_font, rect.position - Vector2(0, graph_font->get_descent(font_size)) + y_offset, _create_label(current.max * float(j) / float(line_count), current.type), HORIZONTAL_ALIGNMENT_LEFT, rect.size.width, font_size, horizontal_line_color);
}
}
float from = rect.size.width;
float prev = -1.0f;
int count = 0;
List<float>::Element *e = current.history.front();
while (from >= 0 && e) {
float m = current.max;
float h2 = 0;
if (m != 0) {
h2 = (e->get() / m);
}
h2 = (1.0f - h2) * float(rect.size.y);
if (e != current.history.front()) {
monitor_draw->draw_line(rect.position + Point2(from, h2), rect.position + Point2(from + spacing, prev), draw_color, Math::round(EDSCALE));
}
if (marker_key == active[i] && count == marker_frame) {
Color line_color;
line_color.set_hsv(draw_color.get_h(), draw_color.get_s() * 0.8f, draw_color.get_v(), 0.5f);
monitor_draw->draw_line(rect.position + Point2(from, 0), rect.position + Point2(from, rect.size.y), line_color, Math::round(EDSCALE));
String label = _create_label(e->get(), current.type);
Size2 size = graph_font->get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size);
Vector2 text_top_left_position = Vector2(from, h2) - (size + Vector2(MARKER_MARGIN, MARKER_MARGIN));
if (text_top_left_position.x < 0) {
text_top_left_position.x = from + MARKER_MARGIN;
}
if (text_top_left_position.y < 0) {
text_top_left_position.y = h2 + MARKER_MARGIN;
}
monitor_draw->draw_string(graph_font, rect.position + text_top_left_position + Point2(0, graph_font->get_ascent(font_size)), label, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x, font_size, line_color);
}
prev = h2;
e = e->next();
from -= spacing;
count++;
}
}
}
void EditorPerformanceProfiler::_build_monitor_tree() {
HashSet<StringName> monitor_checked;
for (KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item && E.value.item->is_checked(0)) {
monitor_checked.insert(E.key);
}
}
base_map.clear();
monitor_tree->get_root()->clear_children();
for (KeyValue<StringName, Monitor> &E : monitors) {
TreeItem *base = _get_monitor_base(E.value.base);
TreeItem *item = _create_monitor_item(E.value.name, base);
item->set_checked(0, monitor_checked.has(E.key));
E.value.item = item;
if (!E.value.history.is_empty()) {
E.value.update_value(E.value.history.front()->get());
}
}
}
TreeItem *EditorPerformanceProfiler::_get_monitor_base(const StringName &p_base_name) {
if (base_map.has(p_base_name)) {
return base_map[p_base_name];
}
TreeItem *base = monitor_tree->create_item(monitor_tree->get_root());
base->set_text(0, EditorPropertyNameProcessor::get_singleton()->process_name(p_base_name, EditorPropertyNameProcessor::get_settings_style()));
base->set_editable(0, false);
base->set_selectable(0, false);
base->set_expand_right(0, true);
if (is_inside_tree()) {
base->set_custom_font(0, get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
}
base_map.insert(p_base_name, base);
return base;
}
TreeItem *EditorPerformanceProfiler::_create_monitor_item(const StringName &p_monitor_name, TreeItem *p_base) {
TreeItem *item = monitor_tree->create_item(p_base);
item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
item->set_editable(0, true);
item->set_selectable(0, false);
item->set_selectable(1, false);
item->set_text(0, EditorPropertyNameProcessor::get_singleton()->process_name(p_monitor_name, EditorPropertyNameProcessor::get_settings_style()));
return item;
}
void EditorPerformanceProfiler::_marker_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
Vector<StringName> active;
for (KeyValue<StringName, Monitor> &E : monitors) {
if (E.value.item->is_checked(0)) {
active.push_back(E.key);
}
}
if (active.size() > 0) {
int columns = int(Math::ceil(Math::sqrt(float(active.size()))));
int rows = int(Math::ceil(float(active.size()) / float(columns)));
if (active.size() == 1) {
rows = 1;
}
Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows);
Vector2i index = mb->get_position() / cell_size;
Rect2i rect(index * cell_size + Point2i(MARGIN, MARGIN), cell_size - Point2i(MARGIN, MARGIN) * 2);
if (rect.has_point(mb->get_position())) {
if (index.x + index.y * columns < active.size()) {
marker_key = active[index.x + index.y * columns];
} else {
marker_key = "";
}
Ref<StyleBox> graph_style_box = get_theme_stylebox(CoreStringName(normal), SNAME("TextEdit"));
rect.position += graph_style_box->get_offset();
rect.size -= graph_style_box->get_minimum_size();
Vector2 point = mb->get_position() - rect.position;
if (point.x >= rect.size.x) {
marker_frame = 0;
} else {
int point_sep = 5;
float spacing = float(point_sep) / float(columns);
marker_frame = (rect.size.x - point.x) / spacing;
}
monitor_draw->queue_redraw();
return;
}
}
marker_key = "";
monitor_draw->queue_redraw();
}
}
void EditorPerformanceProfiler::reset() {
HashMap<StringName, Monitor>::Iterator E = monitors.begin();
while (E != monitors.end()) {
HashMap<StringName, Monitor>::Iterator N = E;
++N;
if (String(E->key).begins_with("custom:")) {
monitors.remove(E);
} else {
E->value.reset();
}
E = N;
}
_build_monitor_tree();
marker_key = "";
marker_frame = 0;
monitor_draw->queue_redraw();
}
void EditorPerformanceProfiler::update_monitors(const Vector<StringName> &p_names) {
HashMap<StringName, int> names;
for (int i = 0; i < p_names.size(); i++) {
names.insert("custom:" + p_names[i], Performance::MONITOR_MAX + i);
}
{
HashMap<StringName, Monitor>::Iterator E = monitors.begin();
while (E != monitors.end()) {
HashMap<StringName, Monitor>::Iterator N = E;
++N;
if (String(E->key).begins_with("custom:")) {
if (!names.has(E->key)) {
monitors.remove(E);
} else {
E->value.frame_index = names[E->key];
names.erase(E->key);
}
}
E = N;
}
}
for (const KeyValue<StringName, int> &E : names) {
String name = String(E.key).replace_first("custom:", "");
String base = "Custom";
if (name.get_slice_count("/") == 2) {
base = name.get_slicec('/', 0);
name = name.get_slicec('/', 1);
}
monitors.insert(E.key, Monitor(name, base, E.value, Performance::MONITOR_TYPE_QUANTITY, nullptr));
}
_build_monitor_tree();
}
void EditorPerformanceProfiler::add_profile_frame(const Vector<float> &p_values) {
for (KeyValue<StringName, Monitor> &E : monitors) {
float value = 0.0f;
if (E.value.frame_index >= 0 && E.value.frame_index < p_values.size()) {
value = p_values[E.value.frame_index];
}
E.value.history.push_front(value);
E.value.update_value(value);
}
marker_frame++;
monitor_draw->queue_redraw();
}
List<float> *EditorPerformanceProfiler::get_monitor_data(const StringName &p_name) {
if (monitors.has(p_name)) {
return &monitors[p_name].history;
}
return nullptr;
}
void EditorPerformanceProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
for (KeyValue<StringName, TreeItem *> &E : base_map) {
E.value->set_custom_font(0, get_theme_font(SNAME("bold"), EditorStringName(EditorFonts)));
}
} break;
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/localize_settings")) {
_build_monitor_tree();
}
} break;
}
}
EditorPerformanceProfiler::EditorPerformanceProfiler() {
set_name(TTR("Monitors"));
set_split_offset(340 * EDSCALE);
monitor_tree = memnew(Tree);
monitor_tree->set_custom_minimum_size(Size2(300, 0) * EDSCALE);
monitor_tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
monitor_tree->set_columns(2);
monitor_tree->set_column_title(0, TTR("Monitor"));
monitor_tree->set_column_expand(0, true);
monitor_tree->set_column_title(1, TTR("Value"));
monitor_tree->set_column_custom_minimum_width(1, 100 * EDSCALE);
monitor_tree->set_column_expand(1, false);
monitor_tree->set_column_titles_visible(true);
monitor_tree->connect("item_edited", callable_mp(this, &EditorPerformanceProfiler::_monitor_select));
monitor_tree->create_item();
monitor_tree->set_hide_root(true);
monitor_tree->set_theme_type_variation("TreeSecondary");
add_child(monitor_tree);
monitor_draw = memnew(Control);
monitor_draw->set_custom_minimum_size(Size2(300, 0) * EDSCALE);
monitor_draw->set_clip_contents(true);
monitor_draw->connect(SceneStringName(draw), callable_mp(this, &EditorPerformanceProfiler::_monitor_draw));
monitor_draw->connect(SceneStringName(gui_input), callable_mp(this, &EditorPerformanceProfiler::_marker_input));
add_child(monitor_draw);
info_message = memnew(Label);
info_message->set_focus_mode(FOCUS_ACCESSIBILITY);
info_message->set_text(TTR("Pick one or more items from the list to display the graph."));
info_message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
info_message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
info_message->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
info_message->set_anchors_and_offsets_preset(PRESET_FULL_RECT, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);
monitor_draw->add_child(info_message);
for (int i = 0; i < Performance::MONITOR_MAX; i++) {
const Performance::Monitor monitor = Performance::Monitor(i);
const String path = Performance::get_singleton()->get_monitor_name(monitor);
const String base = path.get_slicec('/', 0);
const String name = path.get_slicec('/', 1);
monitors.insert(path, Monitor(name, base, i, Performance::get_singleton()->get_monitor_type(monitor), nullptr));
}
_build_monitor_tree();
}

View File

@@ -0,0 +1,89 @@
/**************************************************************************/
/* editor_performance_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/templates/hash_map.h"
#include "main/performance.h"
#include "scene/gui/control.h"
#include "scene/gui/label.h"
#include "scene/gui/split_container.h"
#include "scene/gui/tree.h"
class EditorPerformanceProfiler : public HSplitContainer {
GDCLASS(EditorPerformanceProfiler, HSplitContainer);
private:
class Monitor {
public:
String name;
String base;
List<float> history;
float max = 0.0f;
TreeItem *item = nullptr;
Performance::MonitorType type = Performance::MONITOR_TYPE_QUANTITY;
int frame_index = 0;
Monitor() {}
Monitor(const String &p_name, const String &p_base, int p_frame_index, Performance::MonitorType p_type, TreeItem *p_item);
void update_value(float p_value);
void reset();
};
HashMap<StringName, Monitor> monitors;
HashMap<StringName, TreeItem *> base_map;
Tree *monitor_tree = nullptr;
Control *monitor_draw = nullptr;
Label *info_message = nullptr;
StringName marker_key;
int marker_frame = 0;
const int MARGIN = 4;
const int POINT_SEPARATION = 5;
const int MARKER_MARGIN = 2;
static String _create_label(float p_value, Performance::MonitorType p_type);
void _monitor_select();
void _monitor_draw();
void _build_monitor_tree();
TreeItem *_get_monitor_base(const StringName &p_base_name);
TreeItem *_create_monitor_item(const StringName &p_monitor_name, TreeItem *p_base);
void _marker_input(const Ref<InputEvent> &p_event);
protected:
void _notification(int p_what);
public:
void reset();
void update_monitors(const Vector<StringName> &p_names);
void add_profile_frame(const Vector<float> &p_values);
List<float> *get_monitor_data(const StringName &p_name);
EditorPerformanceProfiler();
};

View File

@@ -0,0 +1,797 @@
/**************************************************************************/
/* editor_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_profiler.h"
#include "core/io/image.h"
#include "editor/editor_string_names.h"
#include "editor/run/editor_run_bar.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/check_box.h"
#include "scene/gui/flow_container.h"
#include "scene/resources/image_texture.h"
void EditorProfiler::_make_metric_ptrs(Metric &m) {
for (int i = 0; i < m.categories.size(); i++) {
m.category_ptrs[m.categories[i].signature] = &m.categories.write[i];
for (int j = 0; j < m.categories[i].items.size(); j++) {
m.item_ptrs[m.categories[i].items[j].signature] = &m.categories.write[i].items.write[j];
}
}
}
EditorProfiler::Metric EditorProfiler::_get_frame_metric(int index) {
return frame_metrics[(frame_metrics.size() + last_metric - (total_metrics - 1) + index) % frame_metrics.size()];
}
void EditorProfiler::add_frame_metric(const Metric &p_metric, bool p_final) {
++last_metric;
if (last_metric >= frame_metrics.size()) {
last_metric = 0;
}
total_metrics++;
if (total_metrics > frame_metrics.size()) {
total_metrics = frame_metrics.size();
}
frame_metrics.write[last_metric] = p_metric;
_make_metric_ptrs(frame_metrics.write[last_metric]);
updating_frame = true;
clear_button->set_disabled(false);
cursor_metric_edit->set_editable(true);
cursor_metric_edit->set_max(p_metric.frame_number);
cursor_metric_edit->set_min(_get_frame_metric(0).frame_number);
if (!seeking) {
cursor_metric_edit->set_value(p_metric.frame_number);
}
updating_frame = false;
if (frame_delay->is_stopped()) {
frame_delay->set_wait_time(p_final ? 0.1 : 1);
frame_delay->start();
}
if (plot_delay->is_stopped()) {
plot_delay->set_wait_time(0.1);
plot_delay->start();
}
}
void EditorProfiler::clear() {
int metric_size = EDITOR_GET("debugger/profiler_frame_history_size");
metric_size = CLAMP(metric_size, 60, 10000);
frame_metrics.clear();
frame_metrics.resize(metric_size);
total_metrics = 0;
last_metric = -1;
variables->clear();
plot_sigs.clear();
plot_sigs.insert("physics_frame_time");
plot_sigs.insert("category_frame_time");
display_internal_profiles->set_visible(EDITOR_GET("debugger/profile_native_calls"));
updating_frame = true;
cursor_metric_edit->set_min(0);
cursor_metric_edit->set_max(100); // Doesn't make much sense, but we can't have min == max. Doesn't hurt.
cursor_metric_edit->set_value(0);
cursor_metric_edit->set_editable(false);
updating_frame = false;
hover_metric = -1;
seeking = false;
// Ensure button text (start, stop) is correct
_update_button_text();
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
static String _get_percent_txt(float p_value, float p_total) {
if (p_total == 0) {
p_total = 0.00001;
}
return TS->format_number(String::num((p_value / p_total) * 100, 1)) + TS->percent_sign();
}
String EditorProfiler::_get_time_as_text(const Metric &m, float p_time, int p_calls) {
const int dmode = display_mode->get_selected();
if (dmode == DISPLAY_FRAME_TIME) {
return TS->format_number(rtos(p_time * 1000).pad_decimals(2)) + " " + TTR("ms");
} else if (dmode == DISPLAY_AVERAGE_TIME) {
if (p_calls == 0) {
return TS->format_number("0.00") + " " + TTR("ms");
} else {
return TS->format_number(rtos((p_time / p_calls) * 1000).pad_decimals(2)) + " " + TTR("ms");
}
} else if (dmode == DISPLAY_FRAME_PERCENT) {
return _get_percent_txt(p_time, m.frame_time);
} else if (dmode == DISPLAY_PHYSICS_FRAME_PERCENT) {
return _get_percent_txt(p_time, m.physics_frame_time);
}
return "err";
}
Color EditorProfiler::_get_color_from_signature(const StringName &p_signature) const {
Color bc = get_theme_color(SNAME("error_color"), EditorStringName(Editor));
double rot = Math::abs(double(p_signature.hash()) / double(0x7FFFFFFF));
Color c;
c.set_hsv(rot, bc.get_s(), bc.get_v());
return c.lerp(get_theme_color(SNAME("base_color"), EditorStringName(Editor)), 0.07);
}
int EditorProfiler::_get_zoom_left_border() const {
const int max_profiles_shown = frame_metrics.size() / Math::exp(graph_zoom);
return CLAMP(zoom_center - max_profiles_shown / 2, 0, frame_metrics.size() - max_profiles_shown);
}
void EditorProfiler::_item_edited() {
if (updating_frame) {
return;
}
TreeItem *item = variables->get_edited();
if (!item) {
return;
}
StringName signature = item->get_metadata(0);
bool checked = item->is_checked(0);
if (checked) {
plot_sigs.insert(signature);
} else {
plot_sigs.erase(signature);
}
if (!frame_delay->is_processing()) {
frame_delay->set_wait_time(0.1);
frame_delay->start();
}
_update_plot();
}
void EditorProfiler::_update_plot() {
const int w = MAX(1, graph->get_size().width); // Clamp to 1 to prevent from crashing when profiler is autostarted.
const int h = MAX(1, graph->get_size().height);
bool reset_texture = false;
const int desired_len = w * h * 4;
if (graph_image.size() != desired_len) {
reset_texture = true;
graph_image.resize(desired_len);
}
uint8_t *wr = graph_image.ptrw();
const Color background_color = get_theme_color(SNAME("dark_color_2"), EditorStringName(Editor));
// Clear the previous frame and set the background color.
for (int i = 0; i < desired_len; i += 4) {
wr[i + 0] = Math::fast_ftoi(background_color.r * 255);
wr[i + 1] = Math::fast_ftoi(background_color.g * 255);
wr[i + 2] = Math::fast_ftoi(background_color.b * 255);
wr[i + 3] = 255;
}
//find highest value
const bool use_self = display_time->get_selected() == DISPLAY_SELF_TIME;
float highest = 0;
for (int i = 0; i < total_metrics; i++) {
const Metric &m = _get_frame_metric(i);
for (const StringName &E : plot_sigs) {
HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E);
if (F) {
highest = MAX(F->value->total_time, highest);
}
HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E);
if (G) {
if (use_self) {
highest = MAX(G->value->self, highest);
} else {
highest = MAX(G->value->total, highest);
}
}
}
}
if (highest > 0) {
//means some data exists..
highest *= 1.2; //leave some upper room
graph_height = highest;
Vector<int> columnv;
columnv.resize(h * 4);
int *column = columnv.ptrw();
HashMap<StringName, int> prev_plots;
const int max_profiles_shown = frame_metrics.size() / Math::exp(graph_zoom);
const int left_border = _get_zoom_left_border();
const int profiles_drawn = CLAMP(total_metrics - left_border, 0, max_profiles_shown);
const int pixel_cols = (profiles_drawn * w) / max_profiles_shown - 1;
for (int i = 0; i < pixel_cols; i++) {
for (int j = 0; j < h * 4; j++) {
column[j] = 0;
}
int current = (i * max_profiles_shown / w) + left_border;
for (const StringName &E : plot_sigs) {
const Metric &m = _get_frame_metric(current);
float value = 0;
HashMap<StringName, Metric::Category *>::ConstIterator F = m.category_ptrs.find(E);
if (F) {
value = F->value->total_time;
}
HashMap<StringName, Metric::Category::Item *>::ConstIterator G = m.item_ptrs.find(E);
if (G) {
if (use_self) {
value = G->value->self;
} else {
value = G->value->total;
}
}
int plot_pos = CLAMP(int(value * h / highest), 0, h - 1);
int prev_plot = plot_pos;
HashMap<StringName, int>::Iterator H = prev_plots.find(E);
if (H) {
prev_plot = H->value;
H->value = plot_pos;
} else {
prev_plots[E] = plot_pos;
}
plot_pos = h - plot_pos - 1;
prev_plot = h - prev_plot - 1;
if (prev_plot > plot_pos) {
SWAP(prev_plot, plot_pos);
}
Color col = _get_color_from_signature(E);
for (int j = prev_plot; j <= plot_pos; j++) {
column[j * 4 + 0] += Math::fast_ftoi(CLAMP(col.r * 255, 0, 255));
column[j * 4 + 1] += Math::fast_ftoi(CLAMP(col.g * 255, 0, 255));
column[j * 4 + 2] += Math::fast_ftoi(CLAMP(col.b * 255, 0, 255));
column[j * 4 + 3] += 1;
}
}
for (int j = 0; j < h * 4; j += 4) {
const int a = column[j + 3];
if (a > 0) {
column[j + 0] /= a;
column[j + 1] /= a;
column[j + 2] /= a;
}
const uint8_t red = uint8_t(column[j + 0]);
const uint8_t green = uint8_t(column[j + 1]);
const uint8_t blue = uint8_t(column[j + 2]);
const bool is_filled = red >= 1 || green >= 1 || blue >= 1;
const int widx = ((j >> 2) * w + i) * 4;
// If the pixel isn't filled by any profiler line, apply the background color instead.
wr[widx + 0] = is_filled ? red : Math::fast_ftoi(background_color.r * 255);
wr[widx + 1] = is_filled ? green : Math::fast_ftoi(background_color.g * 255);
wr[widx + 2] = is_filled ? blue : Math::fast_ftoi(background_color.b * 255);
wr[widx + 3] = 255;
}
}
}
Ref<Image> img = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, graph_image);
if (reset_texture) {
if (graph_texture.is_null()) {
graph_texture.instantiate();
}
graph_texture->set_image(img);
}
graph_texture->update(img);
graph->set_texture(graph_texture);
graph->queue_redraw();
}
void EditorProfiler::_update_frame() {
int cursor_metric = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number;
updating_frame = true;
variables->clear();
TreeItem *root = variables->create_item();
const Metric &m = _get_frame_metric(cursor_metric);
int dtime = display_time->get_selected();
for (int i = 0; i < m.categories.size(); i++) {
TreeItem *category = variables->create_item(root);
category->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
category->set_editable(0, true);
category->set_metadata(0, m.categories[i].signature);
category->set_text(0, String(m.categories[i].name));
category->set_text(1, _get_time_as_text(m, m.categories[i].total_time, 1));
if (plot_sigs.has(m.categories[i].signature)) {
category->set_checked(0, true);
category->set_custom_color(0, _get_color_from_signature(m.categories[i].signature));
}
for (int j = 0; j < m.categories[i].items.size(); j++) {
const Metric::Category::Item &it = m.categories[i].items[j];
if (it.internal == it.total && !display_internal_profiles->is_pressed() && m.categories[i].name == "Script Functions") {
continue;
}
TreeItem *item = variables->create_item(category);
item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
item->set_editable(0, true);
item->set_text(0, it.name);
item->set_metadata(0, it.signature);
item->set_metadata(1, it.script);
item->set_metadata(2, it.line);
item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_RIGHT);
item->set_tooltip_text(0, it.name + "\n" + it.script + ":" + itos(it.line));
float time = dtime == DISPLAY_SELF_TIME ? it.self : it.total;
if (dtime == DISPLAY_SELF_TIME && !display_internal_profiles->is_pressed()) {
time += it.internal;
}
item->set_text(1, _get_time_as_text(m, time, it.calls));
item->set_text(2, itos(it.calls));
if (plot_sigs.has(it.signature)) {
item->set_checked(0, true);
item->set_custom_color(0, _get_color_from_signature(it.signature));
}
}
}
updating_frame = false;
}
void EditorProfiler::_update_button_text() {
if (activate->is_pressed()) {
activate->set_button_icon(get_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
} else {
activate->set_button_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
}
void EditorProfiler::_activate_pressed() {
_update_button_text();
if (activate->is_pressed()) {
_clear_pressed();
}
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
void EditorProfiler::_clear_pressed() {
clear_button->set_disabled(true);
clear();
_update_plot();
}
void EditorProfiler::_internal_profiles_pressed() {
_combo_changed(0);
}
void EditorProfiler::_autostart_toggled(bool p_toggled_on) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "autostart_profiler", p_toggled_on);
EditorRunBar::get_singleton()->update_profiler_autostart_indicator();
}
void EditorProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
activate->set_button_icon(get_editor_theme_icon(SNAME("Play")));
clear_button->set_button_icon(get_editor_theme_icon(SNAME("Clear")));
theme_cache.seek_line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
theme_cache.seek_line_color.a = 0.8;
theme_cache.seek_line_hover_color = theme_cache.seek_line_color;
theme_cache.seek_line_hover_color.a = 0.4;
if (total_metrics > 0) {
_update_plot();
}
} break;
}
}
void EditorProfiler::_graph_tex_draw() {
if (total_metrics == 0) {
return;
}
if (seeking) {
int frame = cursor_metric_edit->get_value() - _get_frame_metric(0).frame_number;
frame = frame - _get_zoom_left_border() + 1;
int cur_x = (frame * graph->get_size().width * Math::exp(graph_zoom)) / frame_metrics.size();
cur_x = CLAMP(cur_x, 0, graph->get_size().width);
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), theme_cache.seek_line_color);
}
if (hover_metric > -1) {
int cur_x = (2 * hover_metric + 1) * graph->get_size().x / (2 * frame_metrics.size()) + 1;
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), theme_cache.seek_line_hover_color);
}
}
void EditorProfiler::_graph_tex_mouse_exit() {
hover_metric = -1;
graph->queue_redraw();
}
void EditorProfiler::_cursor_metric_changed(double) {
if (updating_frame) {
return;
}
graph->queue_redraw();
_update_frame();
}
void EditorProfiler::_graph_tex_input(const Ref<InputEvent> &p_ev) {
if (last_metric < 0) {
return;
}
Ref<InputEventMouse> me = p_ev;
Ref<InputEventMouseButton> mb = p_ev;
Ref<InputEventMouseMotion> mm = p_ev;
MouseButton button_idx = mb.is_valid() ? mb->get_button_index() : MouseButton();
if (
(mb.is_valid() && button_idx == MouseButton::LEFT && mb->is_pressed()) ||
(mm.is_valid())) {
int x = me->get_position().x - 1;
hover_metric = x * frame_metrics.size() / graph->get_size().width;
x = x * frame_metrics.size() / graph->get_size().width;
x = x / Math::exp(graph_zoom) + _get_zoom_left_border();
x = CLAMP(x, 0, frame_metrics.size() - 1);
if (mb.is_valid() || (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) {
updating_frame = true;
if (x < total_metrics) {
cursor_metric_edit->set_value(_get_frame_metric(x).frame_number);
}
updating_frame = false;
if (activate->is_pressed()) {
if (!seeking) {
emit_signal(SNAME("break_request"));
}
}
seeking = true;
if (!frame_delay->is_processing()) {
frame_delay->set_wait_time(0.1);
frame_delay->start();
}
}
}
if (graph_zoom > 0 && mm.is_valid() && (mm->get_button_mask().has_flag(MouseButtonMask::MIDDLE) || mm->get_button_mask().has_flag(MouseButtonMask::RIGHT))) {
// Panning.
const int max_profiles_shown = frame_metrics.size() / Math::exp(graph_zoom);
pan_accumulator += (float)mm->get_relative().x * max_profiles_shown / graph->get_size().width;
if (Math::abs(pan_accumulator) > 1) {
zoom_center = CLAMP(zoom_center - (int)pan_accumulator, max_profiles_shown / 2, frame_metrics.size() - max_profiles_shown / 2);
pan_accumulator -= (int)pan_accumulator;
_update_plot();
}
}
if (button_idx == MouseButton::WHEEL_DOWN) {
// Zooming.
graph_zoom = MAX(-0.05 + graph_zoom, 0);
_update_plot();
} else if (button_idx == MouseButton::WHEEL_UP) {
if (graph_zoom == 0) {
zoom_center = me->get_position().x;
zoom_center = zoom_center * frame_metrics.size() / graph->get_size().width;
}
graph_zoom = MIN(0.05 + graph_zoom, 2);
_update_plot();
}
graph->queue_redraw();
}
void EditorProfiler::disable_seeking() {
seeking = false;
graph->queue_redraw();
}
void EditorProfiler::_combo_changed(int) {
_update_frame();
_update_plot();
}
void EditorProfiler::_bind_methods() {
ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable")));
ADD_SIGNAL(MethodInfo("break_request"));
}
void EditorProfiler::set_enabled(bool p_enable, bool p_clear) {
activate->set_disabled(!p_enable);
if (p_clear) {
clear();
}
}
void EditorProfiler::set_profiling(bool p_pressed) {
activate->set_pressed(p_pressed);
_update_button_text();
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
bool EditorProfiler::is_profiling() {
return activate->is_pressed();
}
Vector<Vector<String>> EditorProfiler::get_data_as_csv() const {
Vector<Vector<String>> res;
if (frame_metrics.is_empty()) {
return res;
}
// Different metrics may contain different number of categories.
HashSet<StringName> possible_signatures;
for (int i = 0; i < frame_metrics.size(); i++) {
const Metric &m = frame_metrics[i];
if (!m.valid) {
continue;
}
for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) {
possible_signatures.insert(E.key);
}
for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) {
possible_signatures.insert(E.key);
}
}
// Generate CSV header and cache indices.
HashMap<StringName, int> sig_map;
Vector<String> signatures;
signatures.resize(possible_signatures.size());
int sig_index = 0;
for (const StringName &E : possible_signatures) {
signatures.write[sig_index] = E;
sig_map[E] = sig_index;
sig_index++;
}
res.push_back(signatures);
// values
Vector<String> values;
int index = last_metric;
for (int i = 0; i < frame_metrics.size(); i++) {
++index;
if (index >= frame_metrics.size()) {
index = 0;
}
const Metric &m = frame_metrics[index];
if (!m.valid) {
continue;
}
// Don't keep old values since there may be empty cells.
values.clear();
values.resize(possible_signatures.size());
for (const KeyValue<StringName, Metric::Category *> &E : m.category_ptrs) {
values.write[sig_map[E.key]] = String::num_real(E.value->total_time);
}
for (const KeyValue<StringName, Metric::Category::Item *> &E : m.item_ptrs) {
values.write[sig_map[E.key]] = String::num_real(E.value->total);
}
res.push_back(values);
}
return res;
}
EditorProfiler::EditorProfiler() {
HBoxContainer *hb = memnew(HBoxContainer);
hb->add_theme_constant_override(SNAME("separation"), 8 * EDSCALE);
add_child(hb);
FlowContainer *container = memnew(FlowContainer);
container->set_h_size_flags(SIZE_EXPAND_FILL);
container->add_theme_constant_override(SNAME("h_separation"), 8 * EDSCALE);
container->add_theme_constant_override(SNAME("v_separation"), 2 * EDSCALE);
hb->add_child(container);
activate = memnew(Button);
activate->set_toggle_mode(true);
activate->set_disabled(true);
activate->set_text(TTR("Start"));
activate->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_activate_pressed));
container->add_child(activate);
clear_button = memnew(Button);
clear_button->set_text(TTR("Clear"));
clear_button->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_clear_pressed));
clear_button->set_disabled(true);
container->add_child(clear_button);
CheckBox *autostart_checkbox = memnew(CheckBox);
autostart_checkbox->set_text(TTR("Autostart"));
autostart_checkbox->set_pressed(EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_profiler", false));
autostart_checkbox->connect(SceneStringName(toggled), callable_mp(this, &EditorProfiler::_autostart_toggled));
container->add_child(autostart_checkbox);
HBoxContainer *hb_measure = memnew(HBoxContainer);
hb_measure->add_theme_constant_override(SNAME("separation"), 2 * EDSCALE);
container->add_child(hb_measure);
hb_measure->add_child(memnew(Label(TTR("Measure:"))));
display_mode = memnew(OptionButton);
display_mode->set_accessibility_name(TTRC("Measure:"));
display_mode->add_item(TTR("Frame Time (ms)"));
display_mode->add_item(TTR("Average Time (ms)"));
display_mode->add_item(TTR("Frame %"));
display_mode->add_item(TTR("Physics Frame %"));
display_mode->connect(SceneStringName(item_selected), callable_mp(this, &EditorProfiler::_combo_changed));
hb_measure->add_child(display_mode);
HBoxContainer *hb_time = memnew(HBoxContainer);
hb_time->add_theme_constant_override(SNAME("separation"), 2 * EDSCALE);
container->add_child(hb_time);
hb_time->add_child(memnew(Label(TTR("Time:"))));
display_time = memnew(OptionButton);
display_time->set_accessibility_name(TTRC("Time:"));
// TRANSLATORS: This is an option in the profiler to display the time spent in a function, including the time spent in other functions called by that function.
display_time->add_item(TTR("Inclusive"));
// TRANSLATORS: This is an option in the profiler to display the time spent in a function, exincluding the time spent in other functions called by that function.
display_time->add_item(TTR("Self"));
display_time->set_tooltip_text(TTR("Inclusive: Includes time from other functions called by this function.\nUse this to spot bottlenecks.\n\nSelf: Only count the time spent in the function itself, not in other functions called by that function.\nUse this to find individual functions to optimize."));
display_time->connect(SceneStringName(item_selected), callable_mp(this, &EditorProfiler::_combo_changed));
hb_time->add_child(display_time);
display_internal_profiles = memnew(CheckButton(TTR("Display internal functions")));
display_internal_profiles->set_visible(EDITOR_GET("debugger/profile_native_calls"));
display_internal_profiles->set_pressed(false);
display_internal_profiles->connect(SceneStringName(pressed), callable_mp(this, &EditorProfiler::_internal_profiles_pressed));
container->add_child(display_internal_profiles);
HBoxContainer *hb_frame = memnew(HBoxContainer);
hb_frame->add_theme_constant_override(SNAME("separation"), 2 * EDSCALE);
hb_frame->set_v_size_flags(SIZE_SHRINK_BEGIN);
hb->add_child(hb_frame);
hb_frame->add_child(memnew(Label(TTR("Frame #:"))));
cursor_metric_edit = memnew(SpinBox);
cursor_metric_edit->set_accessibility_name(TTRC("Frame #:"));
cursor_metric_edit->set_h_size_flags(SIZE_FILL);
cursor_metric_edit->set_value(0);
cursor_metric_edit->set_editable(false);
hb_frame->add_child(cursor_metric_edit);
cursor_metric_edit->connect(SceneStringName(value_changed), callable_mp(this, &EditorProfiler::_cursor_metric_changed));
h_split = memnew(HSplitContainer);
add_child(h_split);
h_split->set_v_size_flags(SIZE_EXPAND_FILL);
variables = memnew(Tree);
variables->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
variables->set_custom_minimum_size(Size2(320, 0) * EDSCALE);
variables->set_hide_folding(true);
h_split->add_child(variables);
variables->set_hide_root(true);
variables->set_columns(3);
variables->set_column_titles_visible(true);
variables->set_column_title(0, TTR("Name"));
variables->set_column_expand(0, true);
variables->set_column_clip_content(0, true);
variables->set_column_custom_minimum_width(0, 60);
variables->set_column_title(1, TTR("Time"));
variables->set_column_expand(1, false);
variables->set_column_clip_content(1, true);
variables->set_column_custom_minimum_width(1, 75 * EDSCALE);
variables->set_column_title(2, TTR("Calls"));
variables->set_column_expand(2, false);
variables->set_column_clip_content(2, true);
variables->set_column_custom_minimum_width(2, 50 * EDSCALE);
variables->set_theme_type_variation("TreeSecondary");
variables->connect("item_edited", callable_mp(this, &EditorProfiler::_item_edited));
graph = memnew(TextureRect);
graph->set_custom_minimum_size(Size2(250 * EDSCALE, 0));
graph->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
graph->set_mouse_filter(MOUSE_FILTER_STOP);
graph->connect(SceneStringName(draw), callable_mp(this, &EditorProfiler::_graph_tex_draw));
graph->connect(SceneStringName(gui_input), callable_mp(this, &EditorProfiler::_graph_tex_input));
graph->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorProfiler::_graph_tex_mouse_exit));
h_split->add_child(graph);
graph->set_h_size_flags(SIZE_EXPAND_FILL);
int metric_size = CLAMP(int(EDITOR_GET("debugger/profiler_frame_history_size")), 60, 10000);
frame_metrics.resize(metric_size);
frame_delay = memnew(Timer);
frame_delay->set_wait_time(0.1);
frame_delay->set_one_shot(true);
add_child(frame_delay);
frame_delay->connect("timeout", callable_mp(this, &EditorProfiler::_update_frame));
plot_delay = memnew(Timer);
plot_delay->set_wait_time(0.1);
plot_delay->set_one_shot(true);
add_child(plot_delay);
plot_delay->connect("timeout", callable_mp(this, &EditorProfiler::_update_plot));
plot_sigs.insert("physics_frame_time");
plot_sigs.insert("category_frame_time");
}

View File

@@ -0,0 +1,187 @@
/**************************************************************************/
/* editor_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/check_button.h"
#include "scene/gui/label.h"
#include "scene/gui/option_button.h"
#include "scene/gui/spin_box.h"
#include "scene/gui/split_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/tree.h"
class ImageTexture;
class EditorProfiler : public VBoxContainer {
GDCLASS(EditorProfiler, VBoxContainer);
public:
struct Metric {
bool valid = false;
int frame_number = 0;
float frame_time = 0;
float process_time = 0;
float physics_time = 0;
float physics_frame_time = 0;
struct Category {
StringName signature;
String name;
float total_time = 0; //total for category
struct Item {
StringName signature;
String name;
String script;
int line = 0;
float self = 0;
float total = 0;
float internal = 0;
int calls = 0;
};
Vector<Item> items;
};
Vector<Category> categories;
HashMap<StringName, Category *> category_ptrs;
HashMap<StringName, Category::Item *> item_ptrs;
};
enum DisplayMode {
DISPLAY_FRAME_TIME,
DISPLAY_AVERAGE_TIME,
DISPLAY_FRAME_PERCENT,
DISPLAY_PHYSICS_FRAME_PERCENT,
};
enum DisplayTime {
DISPLAY_TOTAL_TIME,
DISPLAY_SELF_TIME,
};
private:
struct ThemeCache {
Color seek_line_color;
Color seek_line_hover_color;
} theme_cache;
Button *activate = nullptr;
Button *clear_button = nullptr;
TextureRect *graph = nullptr;
Ref<ImageTexture> graph_texture;
Vector<uint8_t> graph_image;
float graph_zoom = 0.0f;
float pan_accumulator = 0.0f;
int zoom_center = -1;
Tree *variables = nullptr;
HSplitContainer *h_split = nullptr;
HashSet<StringName> plot_sigs;
OptionButton *display_mode = nullptr;
OptionButton *display_time = nullptr;
CheckButton *display_internal_profiles = nullptr;
SpinBox *cursor_metric_edit = nullptr;
Vector<Metric> frame_metrics;
int total_metrics = 0;
int last_metric = -1;
int max_functions = 0;
bool updating_frame = false;
int hover_metric = -1;
float graph_height = 1.0f;
bool seeking = false;
Timer *frame_delay = nullptr;
Timer *plot_delay = nullptr;
void _update_button_text();
void _update_frame();
void _activate_pressed();
void _clear_pressed();
void _autostart_toggled(bool p_toggled_on);
void _internal_profiles_pressed();
String _get_time_as_text(const Metric &m, float p_time, int p_calls);
void _make_metric_ptrs(Metric &m);
void _item_edited();
void _update_plot();
void _graph_tex_mouse_exit();
void _graph_tex_draw();
void _graph_tex_input(const Ref<InputEvent> &p_ev);
Color _get_color_from_signature(const StringName &p_signature) const;
int _get_zoom_left_border() const;
void _cursor_metric_changed(double);
void _combo_changed(int);
Metric _get_frame_metric(int index);
protected:
void _notification(int p_what);
static void _bind_methods();
public:
void add_frame_metric(const Metric &p_metric, bool p_final = false);
void set_enabled(bool p_enable, bool p_clear = true);
void set_profiling(bool p_pressed);
bool is_profiling();
bool is_seeking() { return seeking; }
void disable_seeking();
void clear();
Vector<Vector<String>> get_data_as_csv() const;
EditorProfiler();
};

View File

@@ -0,0 +1,862 @@
/**************************************************************************/
/* editor_visual_profiler.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "editor_visual_profiler.h"
#include "core/io/image.h"
#include "editor/editor_string_names.h"
#include "editor/run/editor_run_bar.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/flow_container.h"
#include "scene/resources/image_texture.h"
void EditorVisualProfiler::set_hardware_info(const String &p_cpu_name, const String &p_gpu_name) {
cpu_name = p_cpu_name;
gpu_name = p_gpu_name;
queue_redraw();
}
void EditorVisualProfiler::add_frame_metric(const Metric &p_metric) {
++last_metric;
if (last_metric >= frame_metrics.size()) {
last_metric = 0;
}
frame_metrics.write[last_metric] = p_metric;
List<String> stack;
for (int i = 0; i < frame_metrics[last_metric].areas.size(); i++) {
String name = frame_metrics[last_metric].areas[i].name;
frame_metrics.write[last_metric].areas.write[i].color_cache = _get_color_from_signature(name);
String full_name;
if (name[0] == '<') {
stack.pop_back();
}
if (stack.size()) {
full_name = stack.back()->get() + name;
} else {
full_name = name;
}
if (name[0] == '>') {
stack.push_back(full_name + "/");
}
frame_metrics.write[last_metric].areas.write[i].fullpath_cache = full_name;
}
updating_frame = true;
clear_button->set_disabled(false);
cursor_metric_edit->set_max(frame_metrics[last_metric].frame_number);
cursor_metric_edit->set_min(MAX(int64_t(frame_metrics[last_metric].frame_number) - frame_metrics.size(), 0));
if (!seeking) {
cursor_metric_edit->set_value(frame_metrics[last_metric].frame_number);
if (hover_metric != -1) {
hover_metric++;
if (hover_metric >= frame_metrics.size()) {
hover_metric = 0;
}
}
}
updating_frame = false;
if (frame_delay->is_stopped()) {
frame_delay->set_wait_time(0.1);
frame_delay->start();
}
if (plot_delay->is_stopped()) {
plot_delay->set_wait_time(0.1);
plot_delay->start();
}
}
void EditorVisualProfiler::clear() {
int metric_size = EDITOR_GET("debugger/profiler_frame_history_size");
metric_size = CLAMP(metric_size, 60, 10000);
frame_metrics.clear();
frame_metrics.resize(metric_size);
last_metric = -1;
variables->clear();
//activate->set_pressed(false);
graph_limit = 1000.0f / CLAMP(int(EDITOR_GET("debugger/profiler_target_fps")), 1, 1000);
updating_frame = true;
cursor_metric_edit->set_min(0);
cursor_metric_edit->set_max(0);
cursor_metric_edit->set_value(0);
updating_frame = false;
hover_metric = -1;
seeking = false;
}
String EditorVisualProfiler::_get_time_as_text(float p_time) {
int dmode = display_mode->get_selected();
if (dmode == DISPLAY_FRAME_TIME) {
return TS->format_number(String::num(p_time, 2)) + " " + TTR("ms");
} else if (dmode == DISPLAY_FRAME_PERCENT) {
return TS->format_number(String::num(p_time * 100 / graph_limit, 2)) + " " + TS->percent_sign();
}
return "err";
}
Color EditorVisualProfiler::_get_color_from_signature(const StringName &p_signature) const {
Color bc = get_theme_color(SNAME("error_color"), EditorStringName(Editor));
double rot = Math::abs(double(p_signature.hash()) / double(0x7FFFFFFF));
Color c;
c.set_hsv(rot, bc.get_s(), bc.get_v());
return c.lerp(get_theme_color(SNAME("base_color"), EditorStringName(Editor)), 0.07);
}
void EditorVisualProfiler::_item_selected() {
if (updating_frame) {
return;
}
TreeItem *item = variables->get_selected();
if (!item) {
return;
}
selected_area = item->get_metadata(0);
_update_plot();
}
void EditorVisualProfiler::_update_plot() {
const int w = graph->get_size().width + 1; // `+1` is to prevent from crashing when visual profiler is auto started.
const int h = graph->get_size().height + 1;
bool reset_texture = false;
const int desired_len = w * h * 4;
if (graph_image.size() != desired_len) {
reset_texture = true;
graph_image.resize(desired_len);
}
uint8_t *wr = graph_image.ptrw();
const Color background_color = get_theme_color("dark_color_2", EditorStringName(Editor));
// Clear the previous frame and set the background color.
for (int i = 0; i < desired_len; i += 4) {
wr[i + 0] = Math::fast_ftoi(background_color.r * 255);
wr[i + 1] = Math::fast_ftoi(background_color.g * 255);
wr[i + 2] = Math::fast_ftoi(background_color.b * 255);
wr[i + 3] = 255;
}
//find highest value
float highest_cpu = 0;
float highest_gpu = 0;
for (int i = 0; i < frame_metrics.size(); i++) {
const Metric &m = frame_metrics[i];
if (!m.valid) {
continue;
}
if (m.areas.size()) {
highest_cpu = MAX(highest_cpu, m.areas[m.areas.size() - 1].cpu_time);
highest_gpu = MAX(highest_gpu, m.areas[m.areas.size() - 1].gpu_time);
}
}
if (highest_cpu > 0 || highest_gpu > 0) {
if (frame_relative->is_pressed()) {
highest_cpu = MAX(graph_limit, highest_cpu);
highest_gpu = MAX(graph_limit, highest_gpu);
}
if (linked->is_pressed()) {
float highest = MAX(highest_cpu, highest_gpu);
highest_cpu = highest_gpu = highest;
}
//means some data exists..
highest_cpu *= 1.2; //leave some upper room
highest_gpu *= 1.2; //leave some upper room
graph_height_cpu = highest_cpu;
graph_height_gpu = highest_gpu;
Vector<Color> columnv_cpu;
columnv_cpu.resize(h);
Color *column_cpu = columnv_cpu.ptrw();
Vector<Color> columnv_gpu;
columnv_gpu.resize(h);
Color *column_gpu = columnv_gpu.ptrw();
int half_w = w / 2;
for (int i = 0; i < half_w; i++) {
for (int j = 0; j < h; j++) {
column_cpu[j] = Color(0, 0, 0, 0);
column_gpu[j] = Color(0, 0, 0, 0);
}
int current = i * frame_metrics.size() / half_w;
int next = (i + 1) * frame_metrics.size() / half_w;
if (next > frame_metrics.size()) {
next = frame_metrics.size();
}
if (next == current) {
next = current + 1; //just because for loop must work
}
for (int j = current; j < next; j++) {
//wrap
int idx = last_metric + 1 + j;
while (idx >= frame_metrics.size()) {
idx -= frame_metrics.size();
}
int area_count = frame_metrics[idx].areas.size();
const Metric::Area *areas = frame_metrics[idx].areas.ptr();
int prev_cpu = 0;
int prev_gpu = 0;
for (int k = 1; k < area_count; k++) {
int ofs_cpu = int(areas[k].cpu_time * h / highest_cpu);
ofs_cpu = CLAMP(ofs_cpu, 0, h - 1);
Color color = selected_area == areas[k - 1].fullpath_cache ? Color(1, 1, 1, 1) : areas[k - 1].color_cache;
for (int l = prev_cpu; l < ofs_cpu; l++) {
column_cpu[h - l - 1] += color;
}
prev_cpu = ofs_cpu;
int ofs_gpu = int(areas[k].gpu_time * h / highest_gpu);
ofs_gpu = CLAMP(ofs_gpu, 0, h - 1);
for (int l = prev_gpu; l < ofs_gpu; l++) {
column_gpu[h - l - 1] += color;
}
prev_gpu = ofs_gpu;
}
}
//plot CPU
for (int j = 0; j < h; j++) {
uint8_t r, g, b;
if (column_cpu[j].a == 0) {
r = Math::fast_ftoi(background_color.r * 255);
g = Math::fast_ftoi(background_color.g * 255);
b = Math::fast_ftoi(background_color.b * 255);
} else {
r = CLAMP((column_cpu[j].r / column_cpu[j].a) * 255.0, 0, 255);
g = CLAMP((column_cpu[j].g / column_cpu[j].a) * 255.0, 0, 255);
b = CLAMP((column_cpu[j].b / column_cpu[j].a) * 255.0, 0, 255);
}
int widx = (j * w + i) * 4;
wr[widx + 0] = r;
wr[widx + 1] = g;
wr[widx + 2] = b;
wr[widx + 3] = 255;
}
//plot GPU
for (int j = 0; j < h; j++) {
uint8_t r, g, b;
if (column_gpu[j].a == 0) {
r = Math::fast_ftoi(background_color.r * 255);
g = Math::fast_ftoi(background_color.g * 255);
b = Math::fast_ftoi(background_color.b * 255);
} else {
r = CLAMP((column_gpu[j].r / column_gpu[j].a) * 255.0, 0, 255);
g = CLAMP((column_gpu[j].g / column_gpu[j].a) * 255.0, 0, 255);
b = CLAMP((column_gpu[j].b / column_gpu[j].a) * 255.0, 0, 255);
}
int widx = (j * w + w / 2 + i) * 4;
wr[widx + 0] = r;
wr[widx + 1] = g;
wr[widx + 2] = b;
wr[widx + 3] = 255;
}
}
}
Ref<Image> img = Image::create_from_data(w, h, false, Image::FORMAT_RGBA8, graph_image);
if (reset_texture) {
if (graph_texture.is_null()) {
graph_texture.instantiate();
}
graph_texture->set_image(img);
}
graph_texture->update(img);
graph->set_texture(graph_texture);
graph->queue_redraw();
}
void EditorVisualProfiler::_update_frame(bool p_focus_selected) {
int cursor_metric = _get_cursor_index();
Ref<Texture> track_icon = get_editor_theme_icon(SNAME("TrackColor"));
ERR_FAIL_INDEX(cursor_metric, frame_metrics.size());
updating_frame = true;
variables->clear();
TreeItem *root = variables->create_item();
const Metric &m = frame_metrics[cursor_metric];
List<TreeItem *> stack;
List<TreeItem *> categories;
TreeItem *ensure_selected = nullptr;
for (int i = 1; i < m.areas.size() - 1; i++) {
TreeItem *parent = stack.size() ? stack.back()->get() : root;
String name = m.areas[i].name;
float cpu_time = m.areas[i].cpu_time;
float gpu_time = m.areas[i].gpu_time;
if (i < m.areas.size() - 1) {
cpu_time = m.areas[i + 1].cpu_time - cpu_time;
gpu_time = m.areas[i + 1].gpu_time - gpu_time;
}
if (name.begins_with(">")) {
TreeItem *category = variables->create_item(parent);
stack.push_back(category);
categories.push_back(category);
name = name.substr(1);
category->set_text(0, name);
category->set_metadata(1, cpu_time);
category->set_metadata(2, gpu_time);
continue;
}
if (name.begins_with("<")) {
stack.pop_back();
continue;
}
TreeItem *category = variables->create_item(parent);
for (TreeItem *E : stack) {
float total_cpu = E->get_metadata(1);
float total_gpu = E->get_metadata(2);
total_cpu += cpu_time;
total_gpu += gpu_time;
E->set_metadata(1, total_cpu);
E->set_metadata(2, total_gpu);
}
category->set_icon(0, track_icon);
category->set_icon_modulate(0, m.areas[i].color_cache);
category->set_selectable(0, true);
category->set_metadata(0, m.areas[i].fullpath_cache);
category->set_text(0, m.areas[i].name);
category->set_text(1, _get_time_as_text(cpu_time));
category->set_metadata(1, m.areas[i].cpu_time);
category->set_text(2, _get_time_as_text(gpu_time));
category->set_metadata(2, m.areas[i].gpu_time);
if (selected_area == m.areas[i].fullpath_cache) {
category->select(0);
if (p_focus_selected) {
ensure_selected = category;
}
}
}
for (TreeItem *E : categories) {
float total_cpu = E->get_metadata(1);
float total_gpu = E->get_metadata(2);
E->set_text(1, _get_time_as_text(total_cpu));
E->set_text(2, _get_time_as_text(total_gpu));
}
if (ensure_selected) {
variables->ensure_cursor_is_visible();
}
updating_frame = false;
}
void EditorVisualProfiler::_activate_pressed() {
if (activate->is_pressed()) {
activate->set_button_icon(get_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
_clear_pressed(); //always clear on start
clear_button->set_disabled(false);
} else {
activate->set_button_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
void EditorVisualProfiler::_clear_pressed() {
clear_button->set_disabled(true);
clear();
_update_plot();
}
void EditorVisualProfiler::_autostart_toggled(bool p_toggled_on) {
EditorSettings::get_singleton()->set_project_metadata("debug_options", "autostart_visual_profiler", p_toggled_on);
EditorRunBar::get_singleton()->update_profiler_autostart_indicator();
}
void EditorVisualProfiler::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
activate->set_button_icon(get_editor_theme_icon(SNAME("Play")));
clear_button->set_button_icon(get_editor_theme_icon(SNAME("Clear")));
} break;
}
}
void EditorVisualProfiler::_graph_tex_draw() {
if (last_metric < 0) {
return;
}
Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));
const Color color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
if (seeking) {
int max_frames = frame_metrics.size();
int frame = cursor_metric_edit->get_value() - (frame_metrics[last_metric].frame_number - max_frames + 1);
if (frame < 0) {
frame = 0;
}
int half_width = graph->get_size().x / 2;
int cur_x = frame * half_width / max_frames;
graph->draw_line(Vector2(cur_x, 0), Vector2(cur_x, graph->get_size().y), color * Color(1, 1, 1));
graph->draw_line(Vector2(cur_x + half_width, 0), Vector2(cur_x + half_width, graph->get_size().y), color * Color(1, 1, 1));
}
if (graph_height_cpu > 0) {
int frame_y = graph->get_size().y - graph_limit * graph->get_size().y / graph_height_cpu - 1;
int half_width = graph->get_size().x / 2;
graph->draw_line(Vector2(0, frame_y), Vector2(half_width, frame_y), color * Color(1, 1, 1, 0.5));
const String limit_str = String::num(graph_limit, 2) + " ms";
graph->draw_string(font, Vector2(half_width - font->get_string_size(limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 2, frame_y - 2), limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
}
if (graph_height_gpu > 0) {
int frame_y = graph->get_size().y - graph_limit * graph->get_size().y / graph_height_gpu - 1;
int half_width = graph->get_size().x / 2;
graph->draw_line(Vector2(half_width, frame_y), Vector2(graph->get_size().x, frame_y), color * Color(1, 1, 1, 0.5));
const String limit_str = String::num(graph_limit, 2) + " ms";
graph->draw_string(font, Vector2(half_width * 2 - font->get_string_size(limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 2, frame_y - 2), limit_str, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
}
graph->draw_string(font, Vector2(font->get_string_size("X", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x, font->get_ascent(font_size) + 2), "CPU: " + cpu_name, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
graph->draw_string(font, Vector2(font->get_string_size("X", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + graph->get_size().width / 2, font->get_ascent(font_size) + 2), "GPU: " + gpu_name, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color * Color(1, 1, 1, 0.75));
}
void EditorVisualProfiler::_graph_tex_mouse_exit() {
hover_metric = -1;
graph->queue_redraw();
}
void EditorVisualProfiler::_cursor_metric_changed(double) {
if (updating_frame) {
return;
}
graph->queue_redraw();
_update_frame();
}
void EditorVisualProfiler::_graph_tex_input(const Ref<InputEvent> &p_ev) {
if (last_metric < 0) {
return;
}
Ref<InputEventMouse> me = p_ev;
Ref<InputEventMouseButton> mb = p_ev;
Ref<InputEventMouseMotion> mm = p_ev;
if (
(mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) ||
(mm.is_valid())) {
int half_w = graph->get_size().width / 2;
int x = me->get_position().x;
if (x > half_w) {
x -= half_w;
}
x = x * frame_metrics.size() / half_w;
bool show_hover = x >= 0 && x < frame_metrics.size();
if (x < 0) {
x = 0;
}
if (x >= frame_metrics.size()) {
x = frame_metrics.size() - 1;
}
int metric = frame_metrics.size() - x - 1;
metric = last_metric - metric;
while (metric < 0) {
metric += frame_metrics.size();
}
if (show_hover) {
hover_metric = metric;
} else {
hover_metric = -1;
}
if (mb.is_valid() || mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {
//cursor_metric=x;
updating_frame = true;
//metric may be invalid, so look for closest metric that is valid, this makes snap feel better
bool valid = false;
for (int i = 0; i < frame_metrics.size(); i++) {
if (frame_metrics[metric].valid) {
valid = true;
break;
}
metric++;
if (metric >= frame_metrics.size()) {
metric = 0;
}
}
if (!valid) {
return;
}
cursor_metric_edit->set_value(frame_metrics[metric].frame_number);
updating_frame = false;
if (activate->is_pressed()) {
if (!seeking) {
// Break request is not required, just stop profiling
}
}
seeking = true;
if (!frame_delay->is_processing()) {
frame_delay->set_wait_time(0.1);
frame_delay->start();
}
bool touched_cpu = me->get_position().x < graph->get_size().width * 0.5;
const Metric::Area *areas = frame_metrics[metric].areas.ptr();
int area_count = frame_metrics[metric].areas.size();
float posy = (1.0 - (me->get_position().y / graph->get_size().height)) * (touched_cpu ? graph_height_cpu : graph_height_gpu);
int last_valid = -1;
bool found = false;
for (int i = 0; i < area_count - 1; i++) {
if (areas[i].name[0] != '<' && areas[i].name[0] != '>') {
last_valid = i;
}
float h = touched_cpu ? areas[i + 1].cpu_time : areas[i + 1].gpu_time;
if (h > posy) {
found = true;
break;
}
}
StringName area_found;
if (found && last_valid != -1) {
area_found = areas[last_valid].fullpath_cache;
}
if (area_found != selected_area) {
selected_area = area_found;
_update_frame(true);
_update_plot();
}
}
graph->queue_redraw();
}
}
int EditorVisualProfiler::_get_cursor_index() const {
if (last_metric < 0) {
return 0;
}
if (!frame_metrics[last_metric].valid) {
return 0;
}
int diff = (frame_metrics[last_metric].frame_number - cursor_metric_edit->get_value());
int idx = last_metric - diff;
while (idx < 0) {
idx += frame_metrics.size();
}
return idx;
}
void EditorVisualProfiler::disable_seeking() {
seeking = false;
graph->queue_redraw();
}
void EditorVisualProfiler::_combo_changed(int) {
_update_frame();
_update_plot();
}
void EditorVisualProfiler::_bind_methods() {
ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable")));
}
void EditorVisualProfiler::_update_button_text() {
if (activate->is_pressed()) {
activate->set_button_icon(get_editor_theme_icon(SNAME("Stop")));
activate->set_text(TTR("Stop"));
} else {
activate->set_button_icon(get_editor_theme_icon(SNAME("Play")));
activate->set_text(TTR("Start"));
}
}
void EditorVisualProfiler::set_enabled(bool p_enable) {
activate->set_disabled(!p_enable);
}
void EditorVisualProfiler::set_profiling(bool p_profiling) {
activate->set_pressed(p_profiling);
_update_button_text();
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
}
bool EditorVisualProfiler::is_profiling() {
return activate->is_pressed();
}
Vector<Vector<String>> EditorVisualProfiler::get_data_as_csv() const {
Vector<Vector<String>> res;
#if 0
if (frame_metrics.is_empty()) {
return res;
}
// signatures
Vector<String> signatures;
const Vector<EditorFrameProfiler::Metric::Category> &categories = frame_metrics[0].categories;
for (int j = 0; j < categories.size(); j++) {
const EditorFrameProfiler::Metric::Category &c = categories[j];
signatures.push_back(c.signature);
for (int k = 0; k < c.items.size(); k++) {
signatures.push_back(c.items[k].signature);
}
}
res.push_back(signatures);
// values
Vector<String> values;
values.resize(signatures.size());
int index = last_metric;
for (int i = 0; i < frame_metrics.size(); i++) {
++index;
if (index >= frame_metrics.size()) {
index = 0;
}
if (!frame_metrics[index].valid) {
continue;
}
int it = 0;
const Vector<EditorFrameProfiler::Metric::Category> &frame_cat = frame_metrics[index].categories;
for (int j = 0; j < frame_cat.size(); j++) {
const EditorFrameProfiler::Metric::Category &c = frame_cat[j];
values.write[it++] = String::num_real(c.total_time);
for (int k = 0; k < c.items.size(); k++) {
values.write[it++] = String::num_real(c.items[k].total);
}
}
res.push_back(values);
}
#endif
return res;
}
EditorVisualProfiler::EditorVisualProfiler() {
HBoxContainer *hb = memnew(HBoxContainer);
hb->add_theme_constant_override(SNAME("separation"), 8 * EDSCALE);
add_child(hb);
FlowContainer *container = memnew(FlowContainer);
container->set_h_size_flags(SIZE_EXPAND_FILL);
container->add_theme_constant_override(SNAME("h_separation"), 8 * EDSCALE);
container->add_theme_constant_override(SNAME("v_separation"), 2 * EDSCALE);
hb->add_child(container);
activate = memnew(Button);
activate->set_toggle_mode(true);
activate->set_disabled(true);
activate->set_text(TTR("Start"));
activate->connect(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_activate_pressed));
container->add_child(activate);
clear_button = memnew(Button);
clear_button->set_text(TTR("Clear"));
clear_button->set_disabled(true);
clear_button->connect(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_clear_pressed));
container->add_child(clear_button);
CheckBox *autostart_checkbox = memnew(CheckBox);
autostart_checkbox->set_text(TTR("Autostart"));
autostart_checkbox->set_pressed(EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_visual_profiler", false));
autostart_checkbox->connect(SceneStringName(toggled), callable_mp(this, &EditorVisualProfiler::_autostart_toggled));
container->add_child(autostart_checkbox);
HBoxContainer *hb_measure = memnew(HBoxContainer);
hb_measure->add_theme_constant_override(SNAME("separation"), 2 * EDSCALE);
container->add_child(hb_measure);
hb_measure->add_child(memnew(Label(TTR("Measure:"))));
display_mode = memnew(OptionButton);
display_mode->set_accessibility_name(TTRC("Measure:"));
display_mode->add_item(TTR("Frame Time (ms)"));
display_mode->add_item(TTR("Frame %"));
display_mode->connect(SceneStringName(item_selected), callable_mp(this, &EditorVisualProfiler::_combo_changed));
hb_measure->add_child(display_mode);
frame_relative = memnew(CheckBox(TTR("Fit to Frame")));
frame_relative->set_pressed(true);
container->add_child(frame_relative);
frame_relative->connect(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_update_plot));
linked = memnew(CheckBox(TTR("Linked")));
linked->set_pressed(true);
container->add_child(linked);
linked->connect(SceneStringName(pressed), callable_mp(this, &EditorVisualProfiler::_update_plot));
HBoxContainer *hb_frame = memnew(HBoxContainer);
hb_frame->add_theme_constant_override(SNAME("separation"), 2 * EDSCALE);
hb_frame->set_v_size_flags(SIZE_SHRINK_BEGIN);
hb->add_child(hb_frame);
hb_frame->add_child(memnew(Label(TTR("Frame #:"))));
cursor_metric_edit = memnew(SpinBox);
cursor_metric_edit->set_accessibility_name(TTRC("Frame #:"));
cursor_metric_edit->set_h_size_flags(SIZE_FILL);
hb_frame->add_child(cursor_metric_edit);
cursor_metric_edit->connect(SceneStringName(value_changed), callable_mp(this, &EditorVisualProfiler::_cursor_metric_changed));
h_split = memnew(HSplitContainer);
add_child(h_split);
h_split->set_v_size_flags(SIZE_EXPAND_FILL);
variables = memnew(Tree);
variables->set_custom_minimum_size(Size2(300, 0) * EDSCALE);
variables->set_hide_folding(true);
h_split->add_child(variables);
variables->set_hide_root(true);
variables->set_columns(3);
variables->set_column_titles_visible(true);
variables->set_column_title(0, TTR("Name"));
variables->set_column_expand(0, true);
variables->set_column_clip_content(0, true);
variables->set_column_custom_minimum_width(0, 60);
variables->set_column_title(1, TTR("CPU"));
variables->set_column_expand(1, false);
variables->set_column_clip_content(1, true);
variables->set_column_custom_minimum_width(1, 75 * EDSCALE);
variables->set_column_title(2, TTR("GPU"));
variables->set_column_expand(2, false);
variables->set_column_clip_content(2, true);
variables->set_column_custom_minimum_width(2, 75 * EDSCALE);
variables->set_theme_type_variation("TreeSecondary");
variables->connect("cell_selected", callable_mp(this, &EditorVisualProfiler::_item_selected));
graph = memnew(TextureRect);
graph->set_custom_minimum_size(Size2(250 * EDSCALE, 0));
graph->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
graph->set_mouse_filter(MOUSE_FILTER_STOP);
graph->connect(SceneStringName(draw), callable_mp(this, &EditorVisualProfiler::_graph_tex_draw));
graph->connect(SceneStringName(gui_input), callable_mp(this, &EditorVisualProfiler::_graph_tex_input));
graph->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorVisualProfiler::_graph_tex_mouse_exit));
h_split->add_child(graph);
graph->set_h_size_flags(SIZE_EXPAND_FILL);
int metric_size = CLAMP(int(EDITOR_GET("debugger/profiler_frame_history_size")), 60, 10000);
frame_metrics.resize(metric_size);
graph_limit = 1000.0f / CLAMP(int(EDITOR_GET("debugger/profiler_target_fps")), 1, 1000);
frame_delay = memnew(Timer);
frame_delay->set_wait_time(0.1);
frame_delay->set_one_shot(true);
add_child(frame_delay);
frame_delay->connect("timeout", callable_mp(this, &EditorVisualProfiler::_update_frame).bind(false));
plot_delay = memnew(Timer);
plot_delay->set_wait_time(0.1);
plot_delay->set_one_shot(true);
add_child(plot_delay);
plot_delay->connect("timeout", callable_mp(this, &EditorVisualProfiler::_update_plot));
}

View File

@@ -0,0 +1,154 @@
/**************************************************************************/
/* editor_visual_profiler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/label.h"
#include "scene/gui/option_button.h"
#include "scene/gui/spin_box.h"
#include "scene/gui/split_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/tree.h"
class ImageTexture;
class EditorVisualProfiler : public VBoxContainer {
GDCLASS(EditorVisualProfiler, VBoxContainer);
public:
struct Metric {
bool valid = false;
uint64_t frame_number = 0;
struct Area {
String name;
Color color_cache;
StringName fullpath_cache;
float cpu_time = 0;
float gpu_time = 0;
};
Vector<Area> areas;
};
enum DisplayTimeMode {
DISPLAY_FRAME_TIME,
DISPLAY_FRAME_PERCENT,
};
private:
Button *activate = nullptr;
Button *clear_button = nullptr;
TextureRect *graph = nullptr;
Ref<ImageTexture> graph_texture;
Vector<uint8_t> graph_image;
Tree *variables = nullptr;
HSplitContainer *h_split = nullptr;
CheckBox *frame_relative = nullptr;
CheckBox *linked = nullptr;
OptionButton *display_mode = nullptr;
SpinBox *cursor_metric_edit = nullptr;
Vector<Metric> frame_metrics;
int last_metric = -1;
int hover_metric = -1;
StringName selected_area;
bool updating_frame = false;
float graph_height_cpu = 1.0f;
float graph_height_gpu = 1.0f;
float graph_limit = 1000.0f / 60;
String cpu_name;
String gpu_name;
bool seeking = false;
Timer *frame_delay = nullptr;
Timer *plot_delay = nullptr;
void _update_button_text();
void _update_frame(bool p_focus_selected = false);
void _activate_pressed();
void _clear_pressed();
void _autostart_toggled(bool p_toggled_on);
String _get_time_as_text(float p_time);
//void _make_metric_ptrs(Metric &m);
void _item_selected();
void _update_plot();
void _graph_tex_mouse_exit();
void _graph_tex_draw();
void _graph_tex_input(const Ref<InputEvent> &p_ev);
int _get_cursor_index() const;
Color _get_color_from_signature(const StringName &p_signature) const;
void _cursor_metric_changed(double);
void _combo_changed(int);
protected:
void _notification(int p_what);
static void _bind_methods();
public:
void set_hardware_info(const String &p_cpu_name, const String &p_gpu_name);
void add_frame_metric(const Metric &p_metric);
void set_enabled(bool p_enable);
void set_profiling(bool p_profiling);
bool is_profiling();
bool is_seeking() { return seeking; }
void disable_seeking();
void clear();
Vector<Vector<String>> get_data_as_csv() const;
EditorVisualProfiler();
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
/**************************************************************************/
/* script_editor_debugger.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/object/script_language.h"
#include "core/os/os.h"
#include "editor/debugger/editor_debugger_inspector.h"
#include "editor/debugger/editor_debugger_node.h"
#include "scene/gui/margin_container.h"
class Button;
class Tree;
class LineEdit;
class TabContainer;
class RichTextLabel;
class TextureButton;
class AcceptDialog;
class TreeItem;
class HSplitContainer;
class ItemList;
class EditorProfiler;
class EditorFileDialog;
class EditorVisualProfiler;
class EditorPerformanceProfiler;
class SceneDebuggerTree;
class EditorDebuggerPlugin;
class DebugAdapterProtocol;
class DebugAdapterParser;
class EditorExpressionEvaluator;
class ScriptEditorDebugger : public MarginContainer {
GDCLASS(ScriptEditorDebugger, MarginContainer);
friend class EditorDebuggerNode;
friend class DebugAdapterProtocol;
friend class DebugAdapterParser;
private:
enum MessageType {
MESSAGE_ERROR,
MESSAGE_WARNING,
MESSAGE_SUCCESS,
};
enum ProfilerType {
PROFILER_VISUAL,
PROFILER_SCRIPTS_SERVERS
};
enum Actions {
ACTION_COPY_ERROR,
ACTION_OPEN_SOURCE,
ACTION_DELETE_BREAKPOINT,
ACTION_DELETE_BREAKPOINTS_IN_FILE,
ACTION_DELETE_ALL_BREAKPOINTS,
};
AcceptDialog *msgdialog = nullptr;
LineEdit *clicked_ctrl = nullptr;
LineEdit *clicked_ctrl_type = nullptr;
LineEdit *live_edit_root = nullptr;
Button *le_set = nullptr;
Button *le_clear = nullptr;
Button *export_csv = nullptr;
VBoxContainer *errors_tab = nullptr;
Tree *error_tree = nullptr;
Button *expand_all_button = nullptr;
Button *collapse_all_button = nullptr;
Button *clear_button = nullptr;
PopupMenu *item_menu = nullptr;
Tree *breakpoints_tree = nullptr;
PopupMenu *breakpoints_menu = nullptr;
EditorFileDialog *file_dialog = nullptr;
enum FileDialogPurpose {
SAVE_MONITORS_CSV,
SAVE_VRAM_CSV,
};
FileDialogPurpose file_dialog_purpose;
int error_count;
int warning_count;
bool skip_breakpoints_value = false;
bool ignore_error_breaks_value = false;
Ref<Script> stack_script;
TabContainer *tabs = nullptr;
RichTextLabel *reason = nullptr;
Button *skip_breakpoints = nullptr;
Button *ignore_error_breaks = nullptr;
Button *copy = nullptr;
Button *step = nullptr;
Button *next = nullptr;
Button *dobreak = nullptr;
Button *docontinue = nullptr;
// Reference to "Remote" tab in scene tree. Needed by _live_edit_set and buttons state.
// Each debugger should have it's tree in the future I guess.
const Tree *editor_remote_tree = nullptr;
HashMap<int, String> profiler_signature;
Tree *vmem_tree = nullptr;
Button *vmem_refresh = nullptr;
Button *vmem_export = nullptr;
LineEdit *vmem_total = nullptr;
TextureRect *vmem_notice_icon = nullptr;
Tree *stack_dump = nullptr;
LineEdit *search = nullptr;
OptionButton *threads = nullptr;
EditorDebuggerInspector *inspector = nullptr;
SceneDebuggerTree *scene_tree = nullptr;
Ref<RemoteDebuggerPeer> peer;
HashMap<NodePath, int> node_path_cache;
int last_path_id = 0;
HashMap<String, int> res_path_cache;
EditorProfiler *profiler = nullptr;
EditorVisualProfiler *visual_profiler = nullptr;
EditorPerformanceProfiler *performance_profiler = nullptr;
EditorExpressionEvaluator *expression_evaluator = nullptr;
OS::ProcessID remote_pid = 0;
bool move_to_foreground = true;
bool can_request_idle_draw = false;
bool live_debug = true;
uint64_t debugging_thread_id = Thread::UNASSIGNED_ID;
struct ThreadDebugged {
String name;
String error;
bool can_debug = false;
bool has_stackdump = false;
uint32_t debug_order = 0;
uint64_t thread_id = Thread::UNASSIGNED_ID; // for order
};
struct ThreadSort {
bool operator()(const ThreadDebugged *a, const ThreadDebugged *b) const {
return a->debug_order < b->debug_order;
}
};
HashMap<uint64_t, ThreadDebugged> threads_debugged;
bool thread_list_updating = false;
void _select_thread(int p_index);
bool debug_mute_audio = false;
EditorDebuggerNode::CameraOverride camera_override;
void _stack_dump_frame_selected();
void _file_selected(const String &p_file);
/// Message handler function for _parse_message.
typedef void (ScriptEditorDebugger::*ParseMessageFunc)(uint64_t p_thread_id, const Array &p_data);
static HashMap<String, ParseMessageFunc> parse_message_handlers;
static void _init_parse_message_handlers();
void _msg_debug_enter(uint64_t p_thread_id, const Array &p_data);
void _msg_debug_exit(uint64_t p_thread_id, const Array &p_data);
void _msg_set_pid(uint64_t p_thread_id, const Array &p_data);
void _msg_scene_click_ctrl(uint64_t p_thread_id, const Array &p_data);
void _msg_scene_scene_tree(uint64_t p_thread_id, const Array &p_data);
void _msg_scene_inspect_objects(uint64_t p_thread_id, const Array &p_data);
#ifndef DISABLE_DEPRECATED
void _msg_scene_inspect_object(uint64_t p_thread_id, const Array &p_data);
#endif // DISABLE_DEPRECATED
void _msg_servers_memory_usage(uint64_t p_thread_id, const Array &p_data);
void _msg_servers_drawn(uint64_t p_thread_id, const Array &p_data);
void _msg_stack_dump(uint64_t p_thread_id, const Array &p_data);
void _msg_stack_frame_vars(uint64_t p_thread_id, const Array &p_data);
void _msg_stack_frame_var(uint64_t p_thread_id, const Array &p_data);
void _msg_output(uint64_t p_thread_id, const Array &p_data);
void _msg_performance_profile_frame(uint64_t p_thread_id, const Array &p_data);
void _msg_visual_hardware_info(uint64_t p_thread_id, const Array &p_data);
void _msg_visual_profile_frame(uint64_t p_thread_id, const Array &p_data);
void _msg_error(uint64_t p_thread_id, const Array &p_data);
void _msg_servers_function_signature(uint64_t p_thread_id, const Array &p_data);
void _msg_servers_profile_common(const Array &p_data, const bool p_final);
void _msg_servers_profile_frame(uint64_t p_thread_id, const Array &p_data);
void _msg_servers_profile_total(uint64_t p_thread_id, const Array &p_data);
void _msg_request_quit(uint64_t p_thread_id, const Array &p_data);
void _msg_remote_objects_selected(uint64_t p_thread_id, const Array &p_data);
void _msg_remote_nothing_selected(uint64_t p_thread_id, const Array &p_data);
void _msg_remote_selection_invalidated(uint64_t p_thread_id, const Array &p_data);
void _msg_show_selection_limit_warning(uint64_t p_thread_id, const Array &p_data);
void _msg_performance_profile_names(uint64_t p_thread_id, const Array &p_data);
void _msg_filesystem_update_file(uint64_t p_thread_id, const Array &p_data);
void _msg_evaluation_return(uint64_t p_thread_id, const Array &p_data);
void _msg_window_title(uint64_t p_thread_id, const Array &p_data);
void _msg_embed_suspend_toggle(uint64_t p_thread_id, const Array &p_data);
void _msg_embed_next_frame(uint64_t p_thread_id, const Array &p_data);
void _parse_message(const String &p_msg, uint64_t p_thread_id, const Array &p_data);
void _set_reason_text(const String &p_reason, MessageType p_type);
void _update_reason_content_height();
void _update_buttons_state();
void _remote_object_selected(ObjectID p_object);
void _remote_objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field);
void _remote_object_property_updated(ObjectID p_id, const String &p_property);
void _video_mem_request();
void _video_mem_export();
void _resources_reimported(const PackedStringArray &p_resources);
int _get_node_path_cache(const NodePath &p_path);
int _get_res_path_cache(const String &p_path);
void _live_edit_set();
void _live_edit_clear();
void _method_changed(Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount);
void _property_changed(Object *p_base, const StringName &p_property, const Variant &p_value);
void _error_activated();
void _error_selected();
void _expand_errors_list();
void _collapse_errors_list();
void _vmem_item_activated();
void _profiler_activate(bool p_enable, int p_profiler);
void _profiler_seeked();
void _clear_errors_list();
void _breakpoints_item_rmb_selected(const Vector2 &p_pos, MouseButton p_button);
void _error_tree_item_rmb_selected(const Vector2 &p_pos, MouseButton p_button);
void _item_menu_id_pressed(int p_option);
void _tab_changed(int p_tab);
void _put_msg(const String &p_message, const Array &p_data, uint64_t p_thread_id = Thread::MAIN_ID);
void _export_csv();
void _clear_execution();
void _stop_and_notify();
void _set_breakpoint(const String &p_path, const int &p_line, const bool &p_enabled);
void _clear_breakpoints();
void _breakpoint_tree_clicked();
String _format_frame_text(const ScriptLanguage::StackInfo *info);
void _thread_debug_enter(uint64_t p_thread_id);
protected:
void _notification(int p_what);
static void _bind_methods();
public:
enum EmbedShortcutAction {
EMBED_SUSPEND_TOGGLE,
EMBED_NEXT_FRAME,
};
void request_remote_objects(const TypedArray<uint64_t> &p_obj_ids, bool p_update_selection = true);
void update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value, const String &p_field = "");
void clear_inspector(bool p_send_msg = true);
// Needed by _live_edit_set, buttons state.
void set_editor_remote_tree(const Tree *p_tree) { editor_remote_tree = p_tree; }
void request_remote_tree();
const SceneDebuggerTree *get_remote_tree();
void request_remote_evaluate(const String &p_expression, int p_stack_frame);
void start(Ref<RemoteDebuggerPeer> p_peer);
void stop();
void debug_skip_breakpoints();
void debug_ignore_error_breaks();
void debug_copy();
void debug_next();
void debug_step();
void debug_break();
void debug_continue();
bool is_breaked() const { return threads_debugged.size() > 0; }
bool is_debuggable() const { return threads_debugged.size() > 0 && threads_debugged[debugging_thread_id].can_debug; }
bool is_session_active() { return peer.is_valid() && peer->is_peer_connected(); }
int get_remote_pid() const { return remote_pid; }
bool is_move_to_foreground() const;
void set_move_to_foreground(const bool &p_move_to_foreground);
int get_error_count() const { return error_count; }
int get_warning_count() const { return warning_count; }
String get_stack_script_file() const;
int get_stack_script_line() const;
int get_stack_script_frame() const;
bool request_stack_dump(const int &p_frame);
void update_tabs();
void clear_style();
String get_var_value(const String &p_var) const;
void save_node(ObjectID p_id, const String &p_file);
void set_live_debugging(bool p_enable);
void live_debug_create_node(const NodePath &p_parent, const String &p_type, const String &p_name);
void live_debug_instantiate_node(const NodePath &p_parent, const String &p_path, const String &p_name);
void live_debug_remove_node(const NodePath &p_at);
void live_debug_remove_and_keep_node(const NodePath &p_at, ObjectID p_keep_id);
void live_debug_restore_node(ObjectID p_id, const NodePath &p_at, int p_at_pos);
void live_debug_duplicate_node(const NodePath &p_at, const String &p_new_name);
void live_debug_reparent_node(const NodePath &p_at, const NodePath &p_new_place, const String &p_new_name, int p_at_pos);
bool get_debug_mute_audio() const;
void set_debug_mute_audio(bool p_mute);
EditorDebuggerNode::CameraOverride get_camera_override() const;
void set_camera_override(EditorDebuggerNode::CameraOverride p_override);
void set_breakpoint(const String &p_path, int p_line, bool p_enabled);
void update_live_edit_root();
void reload_all_scripts();
void reload_scripts(const Vector<String> &p_script_paths);
bool is_skip_breakpoints() const;
bool is_ignore_error_breaks() const;
virtual Size2 get_minimum_size() const override;
void add_debugger_tab(Control *p_control);
void remove_debugger_tab(Control *p_control);
int get_current_debugger_tab() const;
void switch_to_debugger(int p_debugger_tab_idx);
void send_message(const String &p_message, const Array &p_args);
void toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data);
ScriptEditorDebugger();
~ScriptEditorDebugger();
};