Procházet zdrojové kódy

Merge pull request #66938 from Faless/mp/4.x_debugger_split

[Editor] Better expose editor debugger plugins, use it in the multiplayer module.
Rémi Verschelde před 2 roky
rodič
revize
2f573f211e

+ 0 - 87
core/debugger/remote_debugger.cpp

@@ -39,89 +39,6 @@
 #include "core/object/script_language.h"
 #include "core/os/os.h"
 
-class RemoteDebugger::MultiplayerProfiler : public EngineProfiler {
-	struct BandwidthFrame {
-		uint32_t timestamp;
-		int packet_size;
-	};
-
-	int bandwidth_in_ptr = 0;
-	Vector<BandwidthFrame> bandwidth_in;
-	int bandwidth_out_ptr = 0;
-	Vector<BandwidthFrame> bandwidth_out;
-	uint64_t last_bandwidth_time = 0;
-
-	int bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer) {
-		ERR_FAIL_COND_V(p_buffer.size() == 0, 0);
-		int total_bandwidth = 0;
-
-		uint64_t timestamp = OS::get_singleton()->get_ticks_msec();
-		uint64_t final_timestamp = timestamp - 1000;
-
-		int i = (p_pointer + p_buffer.size() - 1) % p_buffer.size();
-
-		while (i != p_pointer && p_buffer[i].packet_size > 0) {
-			if (p_buffer[i].timestamp < final_timestamp) {
-				return total_bandwidth;
-			}
-			total_bandwidth += p_buffer[i].packet_size;
-			i = (i + p_buffer.size() - 1) % p_buffer.size();
-		}
-
-		ERR_FAIL_COND_V_MSG(i == p_pointer, total_bandwidth, "Reached the end of the bandwidth profiler buffer, values might be inaccurate.");
-		return total_bandwidth;
-	}
-
-public:
-	void toggle(bool p_enable, const Array &p_opts) {
-		if (!p_enable) {
-			bandwidth_in.clear();
-			bandwidth_out.clear();
-		} else {
-			bandwidth_in_ptr = 0;
-			bandwidth_in.resize(16384); // ~128kB
-			for (int i = 0; i < bandwidth_in.size(); ++i) {
-				bandwidth_in.write[i].packet_size = -1;
-			}
-			bandwidth_out_ptr = 0;
-			bandwidth_out.resize(16384); // ~128kB
-			for (int i = 0; i < bandwidth_out.size(); ++i) {
-				bandwidth_out.write[i].packet_size = -1;
-			}
-		}
-	}
-
-	void add(const Array &p_data) {
-		ERR_FAIL_COND(p_data.size() < 3);
-		const String inout = p_data[0];
-		int time = p_data[1];
-		int size = p_data[2];
-		if (inout == "in") {
-			bandwidth_in.write[bandwidth_in_ptr].timestamp = time;
-			bandwidth_in.write[bandwidth_in_ptr].packet_size = size;
-			bandwidth_in_ptr = (bandwidth_in_ptr + 1) % bandwidth_in.size();
-		} else if (inout == "out") {
-			bandwidth_out.write[bandwidth_out_ptr].timestamp = time;
-			bandwidth_out.write[bandwidth_out_ptr].packet_size = size;
-			bandwidth_out_ptr = (bandwidth_out_ptr + 1) % bandwidth_out.size();
-		}
-	}
-
-	void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
-		uint64_t pt = OS::get_singleton()->get_ticks_msec();
-		if (pt - last_bandwidth_time > 200) {
-			last_bandwidth_time = pt;
-			int incoming_bandwidth = bandwidth_usage(bandwidth_in, bandwidth_in_ptr);
-			int outgoing_bandwidth = bandwidth_usage(bandwidth_out, bandwidth_out_ptr);
-
-			Array arr;
-			arr.push_back(incoming_bandwidth);
-			arr.push_back(outgoing_bandwidth);
-			EngineDebugger::get_singleton()->send_message("multiplayer:bandwidth", arr);
-		}
-	}
-};
-
 class RemoteDebugger::PerformanceProfiler : public EngineProfiler {
 	Object *performance = nullptr;
 	int last_perf_time = 0;
@@ -659,10 +576,6 @@ RemoteDebugger::RemoteDebugger(Ref<RemoteDebuggerPeer> p_peer) {
 	max_errors_per_second = GLOBAL_GET("network/limits/debugger/max_errors_per_second");
 	max_warnings_per_second = GLOBAL_GET("network/limits/debugger/max_warnings_per_second");
 
-	// Multiplayer Profiler
-	multiplayer_profiler.instantiate();
-	multiplayer_profiler->bind("multiplayer");
-
 	// Performance Profiler
 	Object *perf = Engine::get_singleton()->get_singleton_object("Performance");
 	if (perf) {

+ 0 - 2
core/debugger/remote_debugger.h

@@ -50,10 +50,8 @@ public:
 private:
 	typedef DebuggerMarshalls::OutputError ErrorMessage;
 
-	class MultiplayerProfiler;
 	class PerformanceProfiler;
 
-	Ref<MultiplayerProfiler> multiplayer_profiler;
 	Ref<PerformanceProfiler> performance_profiler;
 
 	Ref<RemoteDebuggerPeer> peer;

+ 58 - 58
doc/classes/EditorDebuggerPlugin.xml

@@ -1,88 +1,88 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<class name="EditorDebuggerPlugin" inherits="Control" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+<class name="EditorDebuggerPlugin" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
 	<brief_description>
 		A base class to implement debugger plugins.
 	</brief_description>
 	<description>
 		[EditorDebuggerPlugin] provides functions related to the editor side of the debugger.
-		You don't need to instantiate this class; that is automatically handled by the debugger. [Control] nodes can be added as child nodes to provide a GUI for the plugin.
-		Do not free or reparent this node, otherwise it becomes unusable.
-		To use [EditorDebuggerPlugin], register it using the [method EditorPlugin.add_debugger_plugin] method first.
+		To interact with the debugger, an instance of this class must be added to the editor via [method EditorPlugin.add_debugger_plugin].
+		Once added, the [method _setup_session] callback will be called for every [EditorDebuggerSession] available to the plugin, and when new ones are created (the sessions may be inactive during this stage).
+		You can retrieve the available [EditorDebuggerSession]s via [method get_sessions] or get a specific one via [method get_session].
+		[codeblocks]
+		[gdscript]
+		@tool
+		extends EditorPlugin
+
+		class ExampleEditorDebugger extends EditorDebuggerPlugin:
+
+		    func _has_capture(prefix):
+		        # Return true if you wish to handle message with this prefix.
+		        return prefix == "my_plugin"
+
+		    func _capture(message, data, session_id):
+		        if message == "my_plugin:ping":
+		            get_session(session_id).send_message("my_plugin:echo", data)
+
+		    func _setup_session(session_id):
+		        # Add a new tab in the debugger session UI containing a label.
+		        var label = Label.new()
+		        label.name = "Example plugin"
+		        label.text = "Example plugin"
+		        var session = get_session(session_id)
+		        # Listens to the session started and stopped signals.
+		        session.started.connect(func (): print("Session started"))
+		        session.stopped.connect(func (): print("Session stopped"))
+		        session.add_session_tab(label)
+
+		var debugger = ExampleEditorDebugger.new()
+
+		func _enter_tree():
+		    add_debugger_plugin(debugger)
+
+		func _exit_tree():
+		    remove_debugger_plugin(debugger)
+		[/gdscript]
+		[/codeblocks]
 	</description>
 	<tutorials>
 	</tutorials>
 	<methods>
-		<method name="has_capture">
-			<return type="bool" />
-			<param index="0" name="name" type="StringName" />
-			<description>
-				Returns [code]true[/code] if a message capture with given name is present otherwise [code]false[/code].
-			</description>
-		</method>
-		<method name="is_breaked">
-			<return type="bool" />
-			<description>
-				Returns [code]true[/code] if the game is in break state otherwise [code]false[/code].
-			</description>
-		</method>
-		<method name="is_debuggable">
+		<method name="_capture" qualifiers="virtual">
 			<return type="bool" />
+			<param index="0" name="message" type="String" />
+			<param index="1" name="data" type="Array" />
+			<param index="2" name="session_id" type="int" />
 			<description>
-				Returns [code]true[/code] if the game can be debugged otherwise [code]false[/code].
+				Override this method to process incoming messages. The [param session_id] is the ID of the [EditorDebuggerSession] that received the message (which you can retrieve via [method get_session]).
 			</description>
 		</method>
-		<method name="is_session_active">
+		<method name="_has_capture" qualifiers="virtual const">
 			<return type="bool" />
+			<param index="0" name="capture" type="String" />
 			<description>
-				Returns [code]true[/code] if there is an instance of the game running with the attached debugger otherwise [code]false[/code].
+				Override this method to enable receiving messages from the debugger. If [param capture] is "my_message" then messages starting with "my_message:" will be passes to the [method _capture] method.
 			</description>
 		</method>
-		<method name="register_message_capture">
+		<method name="_setup_session" qualifiers="virtual">
 			<return type="void" />
-			<param index="0" name="name" type="StringName" />
-			<param index="1" name="callable" type="Callable" />
+			<param index="0" name="session_id" type="int" />
 			<description>
-				Registers a message capture with given [param name]. If [param name] is "my_message" then messages starting with "my_message:" will be called with the given callable.
-				Callable must accept a message string and a data array as argument. If the message and data are valid then callable must return [code]true[/code] otherwise [code]false[/code].
+				Override this method to be notified whenever a new [EditorDebuggerSession] is created (the session may be inactive during this stage).
 			</description>
 		</method>
-		<method name="send_message">
-			<return type="void" />
-			<param index="0" name="message" type="String" />
-			<param index="1" name="data" type="Array" />
+		<method name="get_session">
+			<return type="EditorDebuggerSession" />
+			<param index="0" name="id" type="int" />
 			<description>
-				Sends a message with given [param message] and [param data] array.
+				Returns the [EditorDebuggerSession] with the given [param id].
 			</description>
 		</method>
-		<method name="unregister_message_capture">
-			<return type="void" />
-			<param index="0" name="name" type="StringName" />
+		<method name="get_sessions">
+			<return type="Array" />
 			<description>
-				Unregisters the message capture with given name.
+				Returns an array of [EditorDebuggerSession] currently available to this debugger plugin.
+				Note: Not sessions in the array may be inactive, check their state via [method EditorDebuggerSession.is_active]
 			</description>
 		</method>
 	</methods>
-	<signals>
-		<signal name="breaked">
-			<param index="0" name="can_debug" type="bool" />
-			<description>
-				Emitted when the game enters a break state.
-			</description>
-		</signal>
-		<signal name="continued">
-			<description>
-				Emitted when the game exists a break state.
-			</description>
-		</signal>
-		<signal name="started">
-			<description>
-				Emitted when the debugging starts.
-			</description>
-		</signal>
-		<signal name="stopped">
-			<description>
-				Emitted when the debugging stops.
-			</description>
-		</signal>
-	</signals>
 </class>

+ 86 - 0
doc/classes/EditorDebuggerSession.xml

@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="EditorDebuggerSession" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		A class to interact with the editor debugger.
+	</brief_description>
+	<description>
+		This class cannot be directly instantiated and must be retrieved via a [EditorDebuggerPlugin].
+		You can add tabs to the session UI via [method add_session_tab], send messages via [method send_message], and toggle [EngineProfiler]s via [method toggle_profiler].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="add_session_tab">
+			<return type="void" />
+			<param index="0" name="control" type="Control" />
+			<description>
+				Adds the given [param control] to the debug session UI in the debugger bottom panel.
+			</description>
+		</method>
+		<method name="is_active">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the debug session is currently attached to a remote instance.
+			</description>
+		</method>
+		<method name="is_breaked">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the attached remote instance is currently in the debug loop.
+			</description>
+		</method>
+		<method name="is_debuggable">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if the attached remote instance can be debugged.
+			</description>
+		</method>
+		<method name="remove_session_tab">
+			<return type="void" />
+			<param index="0" name="control" type="Control" />
+			<description>
+				Removes the given [param control] from the debug session UI in the debugger bottom panel.
+			</description>
+		</method>
+		<method name="send_message">
+			<return type="void" />
+			<param index="0" name="message" type="String" />
+			<param index="1" name="data" type="Array" default="[]" />
+			<description>
+				Sends the given [param message] to the attached remote instance, optionally passing additionally [param data]. See [EngineDebugger] for how to retrieve those messages.
+			</description>
+		</method>
+		<method name="toggle_profiler">
+			<return type="void" />
+			<param index="0" name="profiler" type="String" />
+			<param index="1" name="enable" type="bool" />
+			<param index="2" name="data" type="Array" default="[]" />
+			<description>
+				Toggle the given [param profiler] on the attached remote instance, optionally passing additionally [param data]. See [EngineProfiler] for more details.
+			</description>
+		</method>
+	</methods>
+	<signals>
+		<signal name="breaked">
+			<param index="0" name="can_debug" type="bool" />
+			<description>
+				Emitted when the attached remote instance enters a break state. If [param can_debug] is [code]true[/code], the remote instance will enter the debug loop.
+			</description>
+		</signal>
+		<signal name="continued">
+			<description>
+				Emitted when the attached remote instance exits a break state.
+			</description>
+		</signal>
+		<signal name="started">
+			<description>
+				Emitted when a remote instance is attached to this session (i.e. the session becomes active).
+			</description>
+		</signal>
+		<signal name="stopped">
+			<description>
+				Emitted when a remote instance is detached from this session (i.e. the session becomes inactive).
+			</description>
+		</signal>
+	</signals>
+</class>

+ 2 - 2
doc/classes/EditorPlugin.xml

@@ -415,7 +415,7 @@
 		</method>
 		<method name="add_debugger_plugin">
 			<return type="void" />
-			<param index="0" name="script" type="Script" />
+			<param index="0" name="script" type="EditorDebuggerPlugin" />
 			<description>
 				Adds a [Script] as debugger plugin to the Debugger. The script must extend [EditorDebuggerPlugin].
 			</description>
@@ -599,7 +599,7 @@
 		</method>
 		<method name="remove_debugger_plugin">
 			<return type="void" />
-			<param index="0" name="script" type="Script" />
+			<param index="0" name="script" type="EditorDebuggerPlugin" />
 			<description>
 				Removes the debugger plugin with given script from the Debugger.
 			</description>

+ 29 - 15
editor/debugger/editor_debugger_node.cpp

@@ -119,8 +119,8 @@ ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() {
 	}
 
 	if (!debugger_plugins.is_empty()) {
-		for (const Ref<Script> &i : debugger_plugins) {
-			node->add_debugger_plugin(i);
+		for (Ref<EditorDebuggerPlugin> plugin : debugger_plugins) {
+			plugin->create_session(node);
 		}
 	}
 
@@ -723,22 +723,36 @@ EditorDebuggerNode::CameraOverride EditorDebuggerNode::get_camera_override() {
 	return camera_override;
 }
 
-void EditorDebuggerNode::add_debugger_plugin(const Ref<Script> &p_script) {
-	ERR_FAIL_COND_MSG(debugger_plugins.has(p_script), "Debugger plugin already exists.");
-	ERR_FAIL_COND_MSG(p_script.is_null(), "Debugger plugin script is null");
-	ERR_FAIL_COND_MSG(p_script->get_instance_base_type() == StringName(), "Debugger plugin script has error.");
-	ERR_FAIL_COND_MSG(String(p_script->get_instance_base_type()) != "EditorDebuggerPlugin", "Base type of debugger plugin is not 'EditorDebuggerPlugin'.");
-	ERR_FAIL_COND_MSG(!p_script->is_tool(), "Debugger plugin script is not in tool mode.");
-	debugger_plugins.insert(p_script);
+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++) {
-		get_debugger(i)->add_debugger_plugin(p_script);
+		plugin->create_session(get_debugger(i));
 	}
 }
 
-void EditorDebuggerNode::remove_debugger_plugin(const Ref<Script> &p_script) {
-	ERR_FAIL_COND_MSG(!debugger_plugins.has(p_script), "Debugger plugin doesn't exists.");
-	debugger_plugins.erase(p_script);
-	for (int i = 0; get_debugger(i); i++) {
-		get_debugger(i)->remove_debugger_plugin(p_script);
+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;
 }

+ 5 - 3
editor/debugger/editor_debugger_node.h

@@ -36,6 +36,7 @@
 
 class Button;
 class DebugAdapterParser;
+class EditorDebuggerPlugin;
 class EditorDebuggerTree;
 class EditorDebuggerRemoteObject;
 class MenuButton;
@@ -113,7 +114,7 @@ private:
 	CameraOverride camera_override = OVERRIDE_NONE;
 	HashMap<Breakpoint, bool, Breakpoint> breakpoints;
 
-	HashSet<Ref<Script>> debugger_plugins;
+	HashSet<Ref<EditorDebuggerPlugin>> debugger_plugins;
 
 	ScriptEditorDebugger *_add_debugger();
 	EditorDebuggerRemoteObject *get_inspected_remote_object();
@@ -205,8 +206,9 @@ public:
 	Error start(const String &p_uri = "tcp://");
 	void stop();
 
-	void add_debugger_plugin(const Ref<Script> &p_script);
-	void remove_debugger_plugin(const Ref<Script> &p_script);
+	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);
 };
 
 #endif // EDITOR_DEBUGGER_NODE_H

+ 13 - 66
editor/debugger/script_editor_debugger.cpp

@@ -37,7 +37,6 @@
 #include "core/string/ustring.h"
 #include "core/version.h"
 #include "editor/debugger/debug_adapter/debug_adapter_protocol.h"
-#include "editor/debugger/editor_network_profiler.h"
 #include "editor/debugger/editor_performance_profiler.h"
 #include "editor/debugger/editor_profiler.h"
 #include "editor/debugger/editor_visual_profiler.h"
@@ -52,6 +51,7 @@
 #include "editor/plugins/node_3d_editor_plugin.h"
 #include "main/performance.h"
 #include "scene/3d/camera_3d.h"
+#include "scene/debugger/scene_debugger.h"
 #include "scene/gui/dialogs.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
@@ -713,17 +713,6 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da
 			profiler->add_frame_metric(metric, true);
 		}
 
-	} else if (p_msg == "multiplayer:rpc") {
-		SceneDebugger::RPCProfilerFrame frame;
-		frame.deserialize(p_data);
-		for (int i = 0; i < frame.infos.size(); i++) {
-			network_profiler->add_node_frame_data(frame.infos[i]);
-		}
-
-	} else if (p_msg == "multiplayer:bandwidth") {
-		ERR_FAIL_COND(p_data.size() < 2);
-		network_profiler->set_bandwidth(p_data[0], p_data[1]);
-
 	} else if (p_msg == "request_quit") {
 		emit_signal(SNAME("stop_requested"));
 		_stop_and_notify();
@@ -741,22 +730,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da
 		int colon_index = p_msg.find_char(':');
 		ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received");
 
-		bool parsed = false;
-		const String cap = p_msg.substr(0, colon_index);
-		HashMap<StringName, Callable>::Iterator element = captures.find(cap);
-		if (element) {
-			Callable &c = element->value;
-			ERR_FAIL_COND_MSG(c.is_null(), "Invalid callable registered: " + cap);
-			Variant cmd = p_msg.substr(colon_index + 1), cmd_data = p_data;
-			const Variant *args[2] = { &cmd, &cmd_data };
-			Variant retval;
-			Callable::CallError err;
-			c.callp(args, 2, retval, err);
-			ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'capture' to callable: " + Variant::get_callable_error_text(c, args, 2, err));
-			ERR_FAIL_COND_MSG(retval.get_type() != Variant::BOOL, "Error calling 'capture' to callable: " + String(c) + ". Return type is not bool.");
-			parsed = retval;
-		}
-
+		bool parsed = EditorDebuggerNode::get_singleton()->plugins_capture(this, p_msg, p_data);
 		if (!parsed) {
 			WARN_PRINT("unknown message " + p_msg);
 		}
@@ -982,10 +956,6 @@ void ScriptEditorDebugger::_profiler_activate(bool p_enable, int p_type) {
 	Array msg_data;
 	msg_data.push_back(p_enable);
 	switch (p_type) {
-		case PROFILER_NETWORK:
-			_put_msg("profiler:multiplayer", msg_data);
-			_put_msg("profiler:rpc", msg_data);
-			break;
 		case PROFILER_VISUAL:
 			_put_msg("profiler:visual", msg_data);
 			break;
@@ -1658,41 +1628,25 @@ void ScriptEditorDebugger::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("errors_cleared"));
 }
 
-void ScriptEditorDebugger::add_debugger_plugin(const Ref<Script> &p_script) {
-	if (!debugger_plugins.has(p_script)) {
-		EditorDebuggerPlugin *plugin = memnew(EditorDebuggerPlugin());
-		plugin->attach_debugger(this);
-		plugin->set_script(p_script);
-		tabs->add_child(plugin);
-		debugger_plugins.insert(p_script, plugin);
-	}
+void ScriptEditorDebugger::add_debugger_tab(Control *p_control) {
+	tabs->add_child(p_control);
 }
 
-void ScriptEditorDebugger::remove_debugger_plugin(const Ref<Script> &p_script) {
-	if (debugger_plugins.has(p_script)) {
-		tabs->remove_child(debugger_plugins[p_script]);
-		debugger_plugins[p_script]->detach_debugger(false);
-		memdelete(debugger_plugins[p_script]);
-		debugger_plugins.erase(p_script);
-	}
+void ScriptEditorDebugger::remove_debugger_tab(Control *p_control) {
+	int idx = tabs->get_tab_idx_from_control(p_control);
+	ERR_FAIL_COND(idx < 0);
+	p_control->queue_free();
 }
 
 void ScriptEditorDebugger::send_message(const String &p_message, const Array &p_args) {
 	_put_msg(p_message, p_args);
 }
 
-void ScriptEditorDebugger::register_message_capture(const StringName &p_name, const Callable &p_callable) {
-	ERR_FAIL_COND_MSG(has_capture(p_name), "Capture already registered: " + p_name);
-	captures.insert(p_name, p_callable);
-}
-
-void ScriptEditorDebugger::unregister_message_capture(const StringName &p_name) {
-	ERR_FAIL_COND_MSG(!has_capture(p_name), "Capture not registered: " + p_name);
-	captures.erase(p_name);
-}
-
-bool ScriptEditorDebugger::has_capture(const StringName &p_name) {
-	return captures.has(p_name);
+void ScriptEditorDebugger::toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data) {
+	Array msg_data;
+	msg_data.push_back(p_enable);
+	msg_data.append_array(p_data);
+	_put_msg("profiler:" + p_profiler, msg_data);
 }
 
 ScriptEditorDebugger::ScriptEditorDebugger() {
@@ -1904,13 +1858,6 @@ ScriptEditorDebugger::ScriptEditorDebugger() {
 		visual_profiler->connect("enable_profiling", callable_mp(this, &ScriptEditorDebugger::_profiler_activate).bind(PROFILER_VISUAL));
 	}
 
-	{ //network profiler
-		network_profiler = memnew(EditorNetworkProfiler);
-		network_profiler->set_name(TTR("Network Profiler"));
-		tabs->add_child(network_profiler);
-		network_profiler->connect("enable_profiling", callable_mp(this, &ScriptEditorDebugger::_profiler_activate).bind(PROFILER_NETWORK));
-	}
-
 	{ //monitors
 		performance_profiler = memnew(EditorPerformanceProfiler);
 		tabs->add_child(performance_profiler);

+ 3 - 13
editor/debugger/script_editor_debugger.h

@@ -50,7 +50,6 @@ class ItemList;
 class EditorProfiler;
 class EditorFileDialog;
 class EditorVisualProfiler;
-class EditorNetworkProfiler;
 class EditorPerformanceProfiler;
 class SceneDebuggerTree;
 class EditorDebuggerPlugin;
@@ -72,7 +71,6 @@ private:
 	};
 
 	enum ProfilerType {
-		PROFILER_NETWORK,
 		PROFILER_VISUAL,
 		PROFILER_SCRIPTS_SERVERS
 	};
@@ -151,7 +149,6 @@ private:
 
 	EditorProfiler *profiler = nullptr;
 	EditorVisualProfiler *visual_profiler = nullptr;
-	EditorNetworkProfiler *network_profiler = nullptr;
 	EditorPerformanceProfiler *performance_profiler = nullptr;
 
 	OS::ProcessID remote_pid = 0;
@@ -163,10 +160,6 @@ private:
 
 	EditorDebuggerNode::CameraOverride camera_override;
 
-	HashMap<Ref<Script>, EditorDebuggerPlugin *> debugger_plugins;
-
-	HashMap<StringName, Callable> captures;
-
 	void _stack_dump_frame_selected();
 
 	void _file_selected(const String &p_file);
@@ -286,14 +279,11 @@ public:
 
 	virtual Size2 get_minimum_size() const override;
 
-	void add_debugger_plugin(const Ref<Script> &p_script);
-	void remove_debugger_plugin(const Ref<Script> &p_script);
+	void add_debugger_tab(Control *p_control);
+	void remove_debugger_tab(Control *p_control);
 
 	void send_message(const String &p_message, const Array &p_args);
-
-	void register_message_capture(const StringName &p_name, const Callable &p_callable);
-	void unregister_message_capture(const StringName &p_name);
-	bool has_capture(const StringName &p_name);
+	void toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data);
 
 	ScriptEditorDebugger();
 	~ScriptEditorDebugger();

+ 1 - 0
editor/editor_node.cpp

@@ -4174,6 +4174,7 @@ void EditorNode::register_editor_types() {
 	GDREGISTER_CLASS(EditorScenePostImport);
 	GDREGISTER_CLASS(EditorCommandPalette);
 	GDREGISTER_CLASS(EditorDebuggerPlugin);
+	GDREGISTER_ABSTRACT_CLASS(EditorDebuggerSession);
 }
 
 void EditorNode::unregister_editor_types() {

+ 5 - 4
editor/editor_plugin.cpp

@@ -43,6 +43,7 @@
 #include "editor/import/editor_import_plugin.h"
 #include "editor/import/resource_importer_scene.h"
 #include "editor/plugins/canvas_item_editor_plugin.h"
+#include "editor/plugins/editor_debugger_plugin.h"
 #include "editor/plugins/node_3d_editor_plugin.h"
 #include "editor/plugins/script_editor_plugin.h"
 #include "editor/project_settings_editor.h"
@@ -841,12 +842,12 @@ ScriptCreateDialog *EditorPlugin::get_script_create_dialog() {
 	return SceneTreeDock::get_singleton()->get_script_create_dialog();
 }
 
-void EditorPlugin::add_debugger_plugin(const Ref<Script> &p_script) {
-	EditorDebuggerNode::get_singleton()->add_debugger_plugin(p_script);
+void EditorPlugin::add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
+	EditorDebuggerNode::get_singleton()->add_debugger_plugin(p_plugin);
 }
 
-void EditorPlugin::remove_debugger_plugin(const Ref<Script> &p_script) {
-	EditorDebuggerNode::get_singleton()->remove_debugger_plugin(p_script);
+void EditorPlugin::remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin) {
+	EditorDebuggerNode::get_singleton()->remove_debugger_plugin(p_plugin);
 }
 
 void EditorPlugin::_editor_project_settings_changed() {

+ 3 - 2
editor/editor_plugin.h

@@ -39,6 +39,7 @@ class Node3D;
 class Button;
 class PopupMenu;
 class EditorCommandPalette;
+class EditorDebuggerPlugin;
 class EditorExport;
 class EditorExportPlugin;
 class EditorFileSystem;
@@ -302,8 +303,8 @@ public:
 	void add_autoload_singleton(const String &p_name, const String &p_path);
 	void remove_autoload_singleton(const String &p_name);
 
-	void add_debugger_plugin(const Ref<Script> &p_script);
-	void remove_debugger_plugin(const Ref<Script> &p_script);
+	void add_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
+	void remove_debugger_plugin(const Ref<EditorDebuggerPlugin> &p_plugin);
 
 	void enable_plugin();
 	void disable_plugin();

+ 114 - 45
editor/plugins/editor_debugger_plugin.cpp

@@ -32,7 +32,7 @@
 
 #include "editor/debugger/script_editor_debugger.h"
 
-void EditorDebuggerPlugin::_breaked(bool p_really_did, bool p_can_debug, String p_message, bool p_has_stackdump) {
+void EditorDebuggerSession::_breaked(bool p_really_did, bool p_can_debug, String p_message, bool p_has_stackdump) {
 	if (p_really_did) {
 		emit_signal(SNAME("breaked"), p_can_debug);
 	} else {
@@ -40,22 +40,22 @@ void EditorDebuggerPlugin::_breaked(bool p_really_did, bool p_can_debug, String
 	}
 }
 
-void EditorDebuggerPlugin::_started() {
+void EditorDebuggerSession::_started() {
 	emit_signal(SNAME("started"));
 }
 
-void EditorDebuggerPlugin::_stopped() {
+void EditorDebuggerSession::_stopped() {
 	emit_signal(SNAME("stopped"));
 }
 
-void EditorDebuggerPlugin::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("send_message", "message", "data"), &EditorDebuggerPlugin::send_message);
-	ClassDB::bind_method(D_METHOD("register_message_capture", "name", "callable"), &EditorDebuggerPlugin::register_message_capture);
-	ClassDB::bind_method(D_METHOD("unregister_message_capture", "name"), &EditorDebuggerPlugin::unregister_message_capture);
-	ClassDB::bind_method(D_METHOD("has_capture", "name"), &EditorDebuggerPlugin::has_capture);
-	ClassDB::bind_method(D_METHOD("is_breaked"), &EditorDebuggerPlugin::is_breaked);
-	ClassDB::bind_method(D_METHOD("is_debuggable"), &EditorDebuggerPlugin::is_debuggable);
-	ClassDB::bind_method(D_METHOD("is_session_active"), &EditorDebuggerPlugin::is_session_active);
+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);
 
 	ADD_SIGNAL(MethodInfo("started"));
 	ADD_SIGNAL(MethodInfo("stopped"));
@@ -63,62 +63,131 @@ void EditorDebuggerPlugin::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("continued"));
 }
 
-void EditorDebuggerPlugin::attach_debugger(ScriptEditorDebugger *p_debugger) {
-	debugger = p_debugger;
-	if (debugger) {
-		debugger->connect("started", callable_mp(this, &EditorDebuggerPlugin::_started));
-		debugger->connect("stopped", callable_mp(this, &EditorDebuggerPlugin::_stopped));
-		debugger->connect("breaked", callable_mp(this, &EditorDebuggerPlugin::_breaked));
-	}
+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 EditorDebuggerPlugin::detach_debugger(bool p_call_debugger) {
-	if (debugger) {
-		debugger->disconnect("started", callable_mp(this, &EditorDebuggerPlugin::_started));
-		debugger->disconnect("stopped", callable_mp(this, &EditorDebuggerPlugin::_stopped));
-		debugger->disconnect("breaked", callable_mp(this, &EditorDebuggerPlugin::_breaked));
-		if (p_call_debugger && get_script_instance()) {
-			debugger->remove_debugger_plugin(get_script_instance()->get_script());
-		}
-		debugger = nullptr;
-	}
+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 EditorDebuggerPlugin::send_message(const String &p_message, const Array &p_args) {
+void EditorDebuggerSession::send_message(const String &p_message, const Array &p_args) {
 	ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger");
 	debugger->send_message(p_message, p_args);
 }
 
-void EditorDebuggerPlugin::register_message_capture(const StringName &p_name, const Callable &p_callable) {
+void EditorDebuggerSession::toggle_profiler(const String &p_profiler, bool p_enable, const Array &p_data) {
 	ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger");
-	debugger->register_message_capture(p_name, p_callable);
+	debugger->toggle_profiler(p_profiler, p_enable, p_data);
 }
 
-void EditorDebuggerPlugin::unregister_message_capture(const StringName &p_name) {
-	ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger");
-	debugger->unregister_message_capture(p_name);
-}
-
-bool EditorDebuggerPlugin::has_capture(const StringName &p_name) {
-	ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger");
-	return debugger->has_capture(p_name);
-}
-
-bool EditorDebuggerPlugin::is_breaked() {
+bool EditorDebuggerSession::is_breaked() {
 	ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger");
 	return debugger->is_breaked();
 }
 
-bool EditorDebuggerPlugin::is_debuggable() {
+bool EditorDebuggerSession::is_debuggable() {
 	ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger");
 	return debugger->is_debuggable();
 }
 
-bool EditorDebuggerPlugin::is_session_active() {
+bool EditorDebuggerSession::is_active() {
 	ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger");
 	return debugger->is_session_active();
 }
 
+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("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_COND(!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("tree_exited", callable_mp(this, &EditorDebuggerSession::_debugger_gone_away), CONNECT_ONE_SHOT);
+}
+
+EditorDebuggerSession::~EditorDebuggerSession() {
+	detach_debugger();
+}
+
+/// EditorDebuggerPlugin
+
 EditorDebuggerPlugin::~EditorDebuggerPlugin() {
-	detach_debugger(true);
+	clear();
+}
+
+void EditorDebuggerPlugin::clear() {
+	for (int i = 0; i < sessions.size(); i++) {
+		sessions[i]->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[p_idx];
+}
+
+Array EditorDebuggerPlugin::get_sessions() {
+	Array ret;
+	for (int i = 0; i < sessions.size(); i++) {
+		ret.push_back(sessions[i]);
+	}
+	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::_bind_methods() {
+	GDVIRTUAL_BIND(_setup_session, "session_id");
+	GDVIRTUAL_BIND(_has_capture, "capture");
+	GDVIRTUAL_BIND(_capture, "message", "data", "session_id");
+	ClassDB::bind_method(D_METHOD("get_session", "id"), &EditorDebuggerPlugin::get_session);
+	ClassDB::bind_method(D_METHOD("get_sessions"), &EditorDebuggerPlugin::get_sessions);
 }

+ 42 - 9
editor/plugins/editor_debugger_plugin.h

@@ -35,29 +35,62 @@
 
 class ScriptEditorDebugger;
 
-class EditorDebuggerPlugin : public Control {
-	GDCLASS(EditorDebuggerPlugin, Control);
+class EditorDebuggerSession : public RefCounted {
+	GDCLASS(EditorDebuggerSession, RefCounted);
 
 private:
+	HashSet<Control *> tabs;
+
 	ScriptEditorDebugger *debugger = nullptr;
 
 	void _breaked(bool p_really_did, bool p_can_debug, String p_message, bool p_has_stackdump);
 	void _started();
 	void _stopped();
+	void _debugger_gone_away();
 
 protected:
 	static void _bind_methods();
 
 public:
-	void attach_debugger(ScriptEditorDebugger *p_debugger);
-	void detach_debugger(bool p_call_debugger);
-	void send_message(const String &p_message, const Array &p_args);
-	void register_message_capture(const StringName &p_name, const Callable &p_callable);
-	void unregister_message_capture(const StringName &p_name);
-	bool has_capture(const StringName &p_name);
+	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_session_active();
+	bool is_active();
+
+	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);
+
+	EditorDebuggerPlugin() {}
 	~EditorDebuggerPlugin();
 };
 

+ 2 - 2
editor/debugger/editor_network_profiler.cpp → modules/multiplayer/editor/editor_network_profiler.cpp

@@ -59,7 +59,7 @@ void EditorNetworkProfiler::_update_frame() {
 
 	TreeItem *root = counters_display->create_item();
 
-	for (const KeyValue<ObjectID, SceneDebugger::RPCNodeInfo> &E : nodes_data) {
+	for (const KeyValue<ObjectID, RPCNodeInfo> &E : nodes_data) {
 		TreeItem *node = counters_display->create_item(root);
 
 		for (int j = 0; j < counters_display->get_columns(); ++j) {
@@ -92,7 +92,7 @@ void EditorNetworkProfiler::_clear_pressed() {
 	}
 }
 
-void EditorNetworkProfiler::add_node_frame_data(const SceneDebugger::RPCNodeInfo p_frame) {
+void EditorNetworkProfiler::add_node_frame_data(const RPCNodeInfo p_frame) {
 	if (!nodes_data.has(p_frame.node)) {
 		nodes_data.insert(p_frame.node, p_frame);
 	} else {

+ 6 - 2
editor/debugger/editor_network_profiler.h → modules/multiplayer/editor/editor_network_profiler.h

@@ -38,10 +38,14 @@
 #include "scene/gui/split_container.h"
 #include "scene/gui/tree.h"
 
+#include "../multiplayer_debugger.h"
+
 class EditorNetworkProfiler : public VBoxContainer {
 	GDCLASS(EditorNetworkProfiler, VBoxContainer)
 
 private:
+	using RPCNodeInfo = MultiplayerDebugger::RPCNodeInfo;
+
 	Button *activate = nullptr;
 	Button *clear_button = nullptr;
 	Tree *counters_display = nullptr;
@@ -50,7 +54,7 @@ private:
 
 	Timer *frame_delay = nullptr;
 
-	HashMap<ObjectID, SceneDebugger::RPCNodeInfo> nodes_data;
+	HashMap<ObjectID, RPCNodeInfo> nodes_data;
 
 	void _update_frame();
 
@@ -62,7 +66,7 @@ protected:
 	static void _bind_methods();
 
 public:
-	void add_node_frame_data(const SceneDebugger::RPCNodeInfo p_frame);
+	void add_node_frame_data(const RPCNodeInfo p_frame);
 	void set_bandwidth(int p_incoming, int p_outgoing);
 	bool is_profiling();
 

+ 140 - 0
modules/multiplayer/editor/multiplayer_editor_plugin.cpp

@@ -0,0 +1,140 @@
+/*************************************************************************/
+/*  multiplayer_editor_plugin.cpp                                        */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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 "multiplayer_editor_plugin.h"
+
+#include "../multiplayer_synchronizer.h"
+#include "editor_network_profiler.h"
+#include "replication_editor.h"
+
+#include "editor/editor_node.h"
+
+bool MultiplayerEditorDebugger::has_capture(const String &p_capture) const {
+	return p_capture == "multiplayer";
+}
+
+bool MultiplayerEditorDebugger::capture(const String &p_message, const Array &p_data, int p_session) {
+	ERR_FAIL_COND_V(!profilers.has(p_session), false);
+	EditorNetworkProfiler *profiler = profilers[p_session];
+	if (p_message == "multiplayer:rpc") {
+		MultiplayerDebugger::RPCFrame frame;
+		frame.deserialize(p_data);
+		for (int i = 0; i < frame.infos.size(); i++) {
+			profiler->add_node_frame_data(frame.infos[i]);
+		}
+		return true;
+
+	} else if (p_message == "multiplayer:bandwidth") {
+		ERR_FAIL_COND_V(p_data.size() < 2, false);
+		profiler->set_bandwidth(p_data[0], p_data[1]);
+		return true;
+	}
+	return false;
+}
+
+void MultiplayerEditorDebugger::_profiler_activate(bool p_enable, int p_session_id) {
+	Ref<EditorDebuggerSession> session = get_session(p_session_id);
+	ERR_FAIL_COND(session.is_null());
+	session->toggle_profiler("multiplayer", p_enable);
+	session->toggle_profiler("rpc", p_enable);
+}
+
+void MultiplayerEditorDebugger::setup_session(int p_session_id) {
+	Ref<EditorDebuggerSession> session = get_session(p_session_id);
+	ERR_FAIL_COND(session.is_null());
+	EditorNetworkProfiler *profiler = memnew(EditorNetworkProfiler);
+	profiler->connect("enable_profiling", callable_mp(this, &MultiplayerEditorDebugger::_profiler_activate).bind(p_session_id));
+	profiler->set_name(TTR("Network Profiler"));
+	session->add_session_tab(profiler);
+	profilers[p_session_id] = profiler;
+}
+
+MultiplayerEditorPlugin::MultiplayerEditorPlugin() {
+	repl_editor = memnew(ReplicationEditor);
+	button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor);
+	button->hide();
+	repl_editor->get_pin()->connect("pressed", callable_mp(this, &MultiplayerEditorPlugin::_pinned));
+	debugger.instantiate();
+}
+
+MultiplayerEditorPlugin::~MultiplayerEditorPlugin() {
+}
+
+void MultiplayerEditorPlugin::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			get_tree()->connect("node_removed", callable_mp(this, &MultiplayerEditorPlugin::_node_removed));
+			add_debugger_plugin(debugger);
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			remove_debugger_plugin(debugger);
+		}
+	}
+}
+
+void MultiplayerEditorPlugin::_node_removed(Node *p_node) {
+	if (p_node && p_node == repl_editor->get_current()) {
+		repl_editor->edit(nullptr);
+		if (repl_editor->is_visible_in_tree()) {
+			EditorNode::get_singleton()->hide_bottom_panel();
+		}
+		button->hide();
+		repl_editor->get_pin()->set_pressed(false);
+	}
+}
+
+void MultiplayerEditorPlugin::_pinned() {
+	if (!repl_editor->get_pin()->is_pressed()) {
+		if (repl_editor->is_visible_in_tree()) {
+			EditorNode::get_singleton()->hide_bottom_panel();
+		}
+		button->hide();
+	}
+}
+
+void MultiplayerEditorPlugin::edit(Object *p_object) {
+	repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object));
+}
+
+bool MultiplayerEditorPlugin::handles(Object *p_object) const {
+	return p_object->is_class("MultiplayerSynchronizer");
+}
+
+void MultiplayerEditorPlugin::make_visible(bool p_visible) {
+	if (p_visible) {
+		button->show();
+		EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor);
+	} else if (!repl_editor->get_pin()->is_pressed()) {
+		if (repl_editor->is_visible_in_tree()) {
+			EditorNode::get_singleton()->hide_bottom_panel();
+		}
+		button->hide();
+	}
+}

+ 81 - 0
modules/multiplayer/editor/multiplayer_editor_plugin.h

@@ -0,0 +1,81 @@
+/*************************************************************************/
+/*  multiplayer_editor_plugin.h                                          */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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.                */
+/*************************************************************************/
+
+#ifndef MULTIPLAYER_EDITOR_PLUGIN_H
+#define MULTIPLAYER_EDITOR_PLUGIN_H
+
+#include "editor/editor_plugin.h"
+
+#include "editor/plugins/editor_debugger_plugin.h"
+
+class EditorNetworkProfiler;
+class MultiplayerEditorDebugger : public EditorDebuggerPlugin {
+	GDCLASS(MultiplayerEditorDebugger, EditorDebuggerPlugin);
+
+private:
+	HashMap<int, EditorNetworkProfiler *> profilers;
+
+	void _profiler_activate(bool p_enable, int p_session_id);
+
+public:
+	virtual bool has_capture(const String &p_capture) const override;
+	virtual bool capture(const String &p_message, const Array &p_data, int p_index) override;
+	virtual void setup_session(int p_session_id) override;
+
+	MultiplayerEditorDebugger() {}
+};
+
+class ReplicationEditor;
+
+class MultiplayerEditorPlugin : public EditorPlugin {
+	GDCLASS(MultiplayerEditorPlugin, EditorPlugin);
+
+private:
+	Button *button = nullptr;
+	ReplicationEditor *repl_editor = nullptr;
+	Ref<MultiplayerEditorDebugger> debugger;
+
+	void _node_removed(Node *p_node);
+
+	void _pinned();
+
+protected:
+	void _notification(int p_what);
+
+public:
+	virtual void edit(Object *p_object) override;
+	virtual bool handles(Object *p_object) const override;
+	virtual void make_visible(bool p_visible) override;
+
+	MultiplayerEditorPlugin();
+	~MultiplayerEditorPlugin();
+};
+
+#endif // MULTIPLAYER_EDITOR_PLUGIN_H

+ 5 - 63
modules/multiplayer/editor/replication_editor_plugin.cpp → modules/multiplayer/editor/replication_editor.cpp

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  replication_editor_plugin.cpp                                        */
+/*  replication_editor.cpp                                               */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -28,7 +28,9 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
-#include "replication_editor_plugin.h"
+#include "replication_editor.h"
+
+#include "../multiplayer_synchronizer.h"
 
 #include "editor/editor_node.h"
 #include "editor/editor_scale.h"
@@ -37,7 +39,6 @@
 #include "editor/inspector_dock.h"
 #include "editor/property_selector.h"
 #include "editor/scene_tree_editor.h"
-#include "modules/multiplayer/multiplayer_synchronizer.h"
 #include "scene/gui/dialogs.h"
 #include "scene/gui/separator.h"
 #include "scene/gui/tree.h"
@@ -141,7 +142,7 @@ void ReplicationEditor::_add_sync_property(String p_path) {
 		return;
 	}
 
-	Ref<EditorUndoRedoManager> undo_redo = EditorNode::get_singleton()->get_undo_redo();
+	Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_singleton()->get_undo_redo();
 	undo_redo->create_action(TTR("Add property to synchronizer"));
 
 	if (config.is_null()) {
@@ -493,62 +494,3 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
 	item->set_checked(2, p_sync);
 	item->set_editable(2, true);
 }
-
-/// ReplicationEditorPlugin
-ReplicationEditorPlugin::ReplicationEditorPlugin() {
-	repl_editor = memnew(ReplicationEditor);
-	button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor);
-	button->hide();
-	repl_editor->get_pin()->connect("pressed", callable_mp(this, &ReplicationEditorPlugin::_pinned));
-}
-
-ReplicationEditorPlugin::~ReplicationEditorPlugin() {
-}
-
-void ReplicationEditorPlugin::_notification(int p_what) {
-	switch (p_what) {
-		case NOTIFICATION_ENTER_TREE: {
-			get_tree()->connect("node_removed", callable_mp(this, &ReplicationEditorPlugin::_node_removed));
-		} break;
-	}
-}
-
-void ReplicationEditorPlugin::_node_removed(Node *p_node) {
-	if (p_node && p_node == repl_editor->get_current()) {
-		repl_editor->edit(nullptr);
-		if (repl_editor->is_visible_in_tree()) {
-			EditorNode::get_singleton()->hide_bottom_panel();
-		}
-		button->hide();
-		repl_editor->get_pin()->set_pressed(false);
-	}
-}
-
-void ReplicationEditorPlugin::_pinned() {
-	if (!repl_editor->get_pin()->is_pressed()) {
-		if (repl_editor->is_visible_in_tree()) {
-			EditorNode::get_singleton()->hide_bottom_panel();
-		}
-		button->hide();
-	}
-}
-
-void ReplicationEditorPlugin::edit(Object *p_object) {
-	repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object));
-}
-
-bool ReplicationEditorPlugin::handles(Object *p_object) const {
-	return p_object->is_class("MultiplayerSynchronizer");
-}
-
-void ReplicationEditorPlugin::make_visible(bool p_visible) {
-	if (p_visible) {
-		button->show();
-		EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor);
-	} else if (!repl_editor->get_pin()->is_pressed()) {
-		if (repl_editor->is_visible_in_tree()) {
-			EditorNode::get_singleton()->hide_bottom_panel();
-		}
-		button->hide();
-	}
-}

+ 4 - 27
modules/multiplayer/editor/replication_editor_plugin.h → modules/multiplayer/editor/replication_editor.h

@@ -1,5 +1,5 @@
 /*************************************************************************/
-/*  replication_editor_plugin.h                                          */
+/*  replication_editor.h                                                 */
 /*************************************************************************/
 /*                       This file is part of:                           */
 /*                           GODOT ENGINE                                */
@@ -28,8 +28,8 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
-#ifndef REPLICATION_EDITOR_PLUGIN_H
-#define REPLICATION_EDITOR_PLUGIN_H
+#ifndef REPLICATION_EDITOR_H
+#define REPLICATION_EDITOR_H
 
 #include "editor/editor_plugin.h"
 #include "modules/multiplayer/scene_replication_config.h"
@@ -105,27 +105,4 @@ public:
 	~ReplicationEditor() {}
 };
 
-class ReplicationEditorPlugin : public EditorPlugin {
-	GDCLASS(ReplicationEditorPlugin, EditorPlugin);
-
-private:
-	Button *button = nullptr;
-	ReplicationEditor *repl_editor = nullptr;
-
-	void _node_removed(Node *p_node);
-
-	void _pinned();
-
-protected:
-	void _notification(int p_what);
-
-public:
-	virtual void edit(Object *p_object) override;
-	virtual bool handles(Object *p_object) const override;
-	virtual void make_visible(bool p_visible) override;
-
-	ReplicationEditorPlugin();
-	~ReplicationEditorPlugin();
-};
-
-#endif // REPLICATION_EDITOR_PLUGIN_H
+#endif // REPLICATION_EDITOR_H

+ 194 - 0
modules/multiplayer/multiplayer_debugger.cpp

@@ -0,0 +1,194 @@
+/*************************************************************************/
+/*  multiplayer_debugger.cpp                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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 "multiplayer_debugger.h"
+
+#include "core/debugger/engine_debugger.h"
+#include "scene/main/node.h"
+
+List<Ref<EngineProfiler>> multiplayer_profilers;
+
+void MultiplayerDebugger::initialize() {
+	Ref<BandwidthProfiler> bandwidth;
+	bandwidth.instantiate();
+	bandwidth->bind("multiplayer");
+	multiplayer_profilers.push_back(bandwidth);
+
+	Ref<RPCProfiler> rpc_profiler;
+	rpc_profiler.instantiate();
+	rpc_profiler->bind("rpc");
+	multiplayer_profilers.push_back(rpc_profiler);
+}
+
+void MultiplayerDebugger::deinitialize() {
+	multiplayer_profilers.clear();
+}
+
+// BandwidthProfiler
+
+int MultiplayerDebugger::BandwidthProfiler::bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer) {
+	ERR_FAIL_COND_V(p_buffer.size() == 0, 0);
+	int total_bandwidth = 0;
+
+	uint64_t timestamp = OS::get_singleton()->get_ticks_msec();
+	uint64_t final_timestamp = timestamp - 1000;
+
+	int i = (p_pointer + p_buffer.size() - 1) % p_buffer.size();
+
+	while (i != p_pointer && p_buffer[i].packet_size > 0) {
+		if (p_buffer[i].timestamp < final_timestamp) {
+			return total_bandwidth;
+		}
+		total_bandwidth += p_buffer[i].packet_size;
+		i = (i + p_buffer.size() - 1) % p_buffer.size();
+	}
+
+	ERR_FAIL_COND_V_MSG(i == p_pointer, total_bandwidth, "Reached the end of the bandwidth profiler buffer, values might be inaccurate.");
+	return total_bandwidth;
+}
+
+void MultiplayerDebugger::BandwidthProfiler::toggle(bool p_enable, const Array &p_opts) {
+	if (!p_enable) {
+		bandwidth_in.clear();
+		bandwidth_out.clear();
+	} else {
+		bandwidth_in_ptr = 0;
+		bandwidth_in.resize(16384); // ~128kB
+		for (int i = 0; i < bandwidth_in.size(); ++i) {
+			bandwidth_in.write[i].packet_size = -1;
+		}
+		bandwidth_out_ptr = 0;
+		bandwidth_out.resize(16384); // ~128kB
+		for (int i = 0; i < bandwidth_out.size(); ++i) {
+			bandwidth_out.write[i].packet_size = -1;
+		}
+	}
+}
+
+void MultiplayerDebugger::BandwidthProfiler::add(const Array &p_data) {
+	ERR_FAIL_COND(p_data.size() < 3);
+	const String inout = p_data[0];
+	int time = p_data[1];
+	int size = p_data[2];
+	if (inout == "in") {
+		bandwidth_in.write[bandwidth_in_ptr].timestamp = time;
+		bandwidth_in.write[bandwidth_in_ptr].packet_size = size;
+		bandwidth_in_ptr = (bandwidth_in_ptr + 1) % bandwidth_in.size();
+	} else if (inout == "out") {
+		bandwidth_out.write[bandwidth_out_ptr].timestamp = time;
+		bandwidth_out.write[bandwidth_out_ptr].packet_size = size;
+		bandwidth_out_ptr = (bandwidth_out_ptr + 1) % bandwidth_out.size();
+	}
+}
+
+void MultiplayerDebugger::BandwidthProfiler::tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
+	uint64_t pt = OS::get_singleton()->get_ticks_msec();
+	if (pt - last_bandwidth_time > 200) {
+		last_bandwidth_time = pt;
+		int incoming_bandwidth = bandwidth_usage(bandwidth_in, bandwidth_in_ptr);
+		int outgoing_bandwidth = bandwidth_usage(bandwidth_out, bandwidth_out_ptr);
+
+		Array arr;
+		arr.push_back(incoming_bandwidth);
+		arr.push_back(outgoing_bandwidth);
+		EngineDebugger::get_singleton()->send_message("multiplayer:bandwidth", arr);
+	}
+}
+
+// RPCProfiler
+
+Array MultiplayerDebugger::RPCFrame::serialize() {
+	Array arr;
+	arr.push_back(infos.size() * 4);
+	for (int i = 0; i < infos.size(); ++i) {
+		arr.push_back(uint64_t(infos[i].node));
+		arr.push_back(infos[i].node_path);
+		arr.push_back(infos[i].incoming_rpc);
+		arr.push_back(infos[i].outgoing_rpc);
+	}
+	return arr;
+}
+
+bool MultiplayerDebugger::RPCFrame::deserialize(const Array &p_arr) {
+	ERR_FAIL_COND_V(p_arr.size() < 1, false);
+	uint32_t size = p_arr[0];
+	ERR_FAIL_COND_V(size % 4, false);
+	ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false);
+	infos.resize(size / 4);
+	int idx = 1;
+	for (uint32_t i = 0; i < size / 4; ++i) {
+		infos.write[i].node = uint64_t(p_arr[idx]);
+		infos.write[i].node_path = p_arr[idx + 1];
+		infos.write[i].incoming_rpc = p_arr[idx + 2];
+		infos.write[i].outgoing_rpc = p_arr[idx + 3];
+	}
+	return true;
+}
+
+void MultiplayerDebugger::RPCProfiler::init_node(const ObjectID p_node) {
+	if (rpc_node_data.has(p_node)) {
+		return;
+	}
+	rpc_node_data.insert(p_node, RPCNodeInfo());
+	rpc_node_data[p_node].node = p_node;
+	rpc_node_data[p_node].node_path = Object::cast_to<Node>(ObjectDB::get_instance(p_node))->get_path();
+	rpc_node_data[p_node].incoming_rpc = 0;
+	rpc_node_data[p_node].outgoing_rpc = 0;
+}
+
+void MultiplayerDebugger::RPCProfiler::toggle(bool p_enable, const Array &p_opts) {
+	rpc_node_data.clear();
+}
+
+void MultiplayerDebugger::RPCProfiler::add(const Array &p_data) {
+	ERR_FAIL_COND(p_data.size() < 2);
+	const ObjectID id = p_data[0];
+	const String what = p_data[1];
+	init_node(id);
+	RPCNodeInfo &info = rpc_node_data[id];
+	if (what == "rpc_in") {
+		info.incoming_rpc++;
+	} else if (what == "rpc_out") {
+		info.outgoing_rpc++;
+	}
+}
+
+void MultiplayerDebugger::RPCProfiler::tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
+	uint64_t pt = OS::get_singleton()->get_ticks_msec();
+	if (pt - last_profile_time > 100) {
+		last_profile_time = pt;
+		RPCFrame frame;
+		for (const KeyValue<ObjectID, RPCNodeInfo> &E : rpc_node_data) {
+			frame.infos.push_back(E.value);
+		}
+		rpc_node_data.clear();
+		EngineDebugger::get_singleton()->send_message("multiplayer:rpc", frame.serialize());
+	}
+}

+ 95 - 0
modules/multiplayer/multiplayer_debugger.h

@@ -0,0 +1,95 @@
+/*************************************************************************/
+/*  multiplayer_debugger.h                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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.                */
+/*************************************************************************/
+
+#ifndef MULTIPLAYER_DEBUGGER_H
+#define MULTIPLAYER_DEBUGGER_H
+
+#include "core/debugger/engine_profiler.h"
+
+#include "core/os/os.h"
+
+class MultiplayerDebugger {
+public:
+	struct RPCNodeInfo {
+		ObjectID node;
+		String node_path;
+		int incoming_rpc = 0;
+		int outgoing_rpc = 0;
+	};
+
+	struct RPCFrame {
+		Vector<RPCNodeInfo> infos;
+
+		Array serialize();
+		bool deserialize(const Array &p_arr);
+	};
+
+private:
+	class BandwidthProfiler : public EngineProfiler {
+	protected:
+		struct BandwidthFrame {
+			uint32_t timestamp;
+			int packet_size;
+		};
+
+		int bandwidth_in_ptr = 0;
+		Vector<BandwidthFrame> bandwidth_in;
+		int bandwidth_out_ptr = 0;
+		Vector<BandwidthFrame> bandwidth_out;
+		uint64_t last_bandwidth_time = 0;
+
+		int bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer);
+
+	public:
+		void toggle(bool p_enable, const Array &p_opts);
+		void add(const Array &p_data);
+		void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time);
+	};
+
+	class RPCProfiler : public EngineProfiler {
+	public:
+	private:
+		HashMap<ObjectID, RPCNodeInfo> rpc_node_data;
+		uint64_t last_profile_time = 0;
+
+		void init_node(const ObjectID p_node);
+
+	public:
+		void toggle(bool p_enable, const Array &p_opts);
+		void add(const Array &p_data);
+		void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time);
+	};
+
+public:
+	static void initialize();
+	static void deinitialize();
+};
+
+#endif // MULTIPLAYER_DEBUGGER_H

+ 6 - 3
modules/multiplayer/register_types.cpp

@@ -36,9 +36,10 @@
 #include "scene_replication_interface.h"
 #include "scene_rpc_interface.h"
 
+#include "multiplayer_debugger.h"
+
 #ifdef TOOLS_ENABLED
-#include "editor/editor_plugin.h"
-#include "editor/replication_editor_plugin.h"
+#include "editor/multiplayer_editor_plugin.h"
 #endif
 
 void initialize_multiplayer_module(ModuleInitializationLevel p_level) {
@@ -48,13 +49,15 @@ void initialize_multiplayer_module(ModuleInitializationLevel p_level) {
 		GDREGISTER_CLASS(MultiplayerSynchronizer);
 		GDREGISTER_CLASS(SceneMultiplayer);
 		MultiplayerAPI::set_default_interface("SceneMultiplayer");
+		MultiplayerDebugger::initialize();
 	}
 #ifdef TOOLS_ENABLED
 	if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
-		EditorPlugins::add_by_type<ReplicationEditorPlugin>();
+		EditorPlugins::add_by_type<MultiplayerEditorPlugin>();
 	}
 #endif
 }
 
 void uninitialize_multiplayer_module(ModuleInitializationLevel p_level) {
+	MultiplayerDebugger::deinitialize();
 }

+ 0 - 77
scene/debugger/scene_debugger.cpp

@@ -39,87 +39,10 @@
 #include "scene/main/window.h"
 #include "scene/resources/packed_scene.h"
 
-Array SceneDebugger::RPCProfilerFrame::serialize() {
-	Array arr;
-	arr.push_back(infos.size() * 4);
-	for (int i = 0; i < infos.size(); ++i) {
-		arr.push_back(uint64_t(infos[i].node));
-		arr.push_back(infos[i].node_path);
-		arr.push_back(infos[i].incoming_rpc);
-		arr.push_back(infos[i].outgoing_rpc);
-	}
-	return arr;
-}
-
-bool SceneDebugger::RPCProfilerFrame::deserialize(const Array &p_arr) {
-	ERR_FAIL_COND_V(p_arr.size() < 1, false);
-	uint32_t size = p_arr[0];
-	ERR_FAIL_COND_V(size % 4, false);
-	ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false);
-	infos.resize(size / 4);
-	int idx = 1;
-	for (uint32_t i = 0; i < size / 4; ++i) {
-		infos.write[i].node = uint64_t(p_arr[idx]);
-		infos.write[i].node_path = p_arr[idx + 1];
-		infos.write[i].incoming_rpc = p_arr[idx + 2];
-		infos.write[i].outgoing_rpc = p_arr[idx + 3];
-	}
-	return true;
-}
-
-class SceneDebugger::RPCProfiler : public EngineProfiler {
-	HashMap<ObjectID, RPCNodeInfo> rpc_node_data;
-	uint64_t last_profile_time = 0;
-
-	void init_node(const ObjectID p_node) {
-		if (rpc_node_data.has(p_node)) {
-			return;
-		}
-		rpc_node_data.insert(p_node, RPCNodeInfo());
-		rpc_node_data[p_node].node = p_node;
-		rpc_node_data[p_node].node_path = Object::cast_to<Node>(ObjectDB::get_instance(p_node))->get_path();
-		rpc_node_data[p_node].incoming_rpc = 0;
-		rpc_node_data[p_node].outgoing_rpc = 0;
-	}
-
-public:
-	void toggle(bool p_enable, const Array &p_opts) {
-		rpc_node_data.clear();
-	}
-
-	void add(const Array &p_data) {
-		ERR_FAIL_COND(p_data.size() < 2);
-		const ObjectID id = p_data[0];
-		const String what = p_data[1];
-		init_node(id);
-		RPCNodeInfo &info = rpc_node_data[id];
-		if (what == "rpc_in") {
-			info.incoming_rpc++;
-		} else if (what == "rpc_out") {
-			info.outgoing_rpc++;
-		}
-	}
-
-	void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
-		uint64_t pt = OS::get_singleton()->get_ticks_msec();
-		if (pt - last_profile_time > 100) {
-			last_profile_time = pt;
-			RPCProfilerFrame frame;
-			for (const KeyValue<ObjectID, RPCNodeInfo> &E : rpc_node_data) {
-				frame.infos.push_back(E.value);
-			}
-			rpc_node_data.clear();
-			EngineDebugger::get_singleton()->send_message("multiplayer:rpc", frame.serialize());
-		}
-	}
-};
-
 SceneDebugger *SceneDebugger::singleton = nullptr;
 
 SceneDebugger::SceneDebugger() {
 	singleton = this;
-	rpc_profiler.instantiate();
-	rpc_profiler->bind("rpc");
 #ifdef DEBUG_ENABLED
 	LiveEditor::singleton = memnew(LiveEditor);
 	EngineDebugger::register_message_capture("scene", EngineDebugger::Capture(nullptr, SceneDebugger::parse_message));

+ 0 - 19
scene/debugger/scene_debugger.h

@@ -42,28 +42,9 @@ class Node;
 
 class SceneDebugger {
 public:
-	// RPC profiler
-	struct RPCNodeInfo {
-		ObjectID node;
-		String node_path;
-		int incoming_rpc = 0;
-		int outgoing_rpc = 0;
-	};
-
-	struct RPCProfilerFrame {
-		Vector<RPCNodeInfo> infos;
-
-		Array serialize();
-		bool deserialize(const Array &p_arr);
-	};
-
 private:
-	class RPCProfiler;
-
 	static SceneDebugger *singleton;
 
-	Ref<RPCProfiler> rpc_profiler;
-
 	SceneDebugger();
 
 public: