diff options
Diffstat (limited to 'modules/multiplayer')
17 files changed, 1377 insertions, 174 deletions
diff --git a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml index 7ed6255a62..af7c345f15 100644 --- a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml +++ b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml @@ -17,7 +17,7 @@ <param index="0" name="filter" type="Callable" /> <description> Adds a peer visibility filter for this synchronizer. - [code]filter[/code] should take a peer id [int] and return a [bool]. + [code]filter[/code] should take a peer ID [int] and return a [bool]. </description> </method> <method name="get_visibility_for" qualifiers="const"> diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp new file mode 100644 index 0000000000..cce22b9084 --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -0,0 +1,347 @@ +/*************************************************************************/ +/* editor_network_profiler.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 "editor_network_profiler.h" + +#include "core/os/os.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void EditorNetworkProfiler::_bind_methods() { + ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); + ADD_SIGNAL(MethodInfo("open_request", PropertyInfo(Variant::STRING, "path"))); +} + +void EditorNetworkProfiler::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + node_icon = get_theme_icon(SNAME("Node"), SNAME("EditorIcons")); + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons"))); + } else { + activate->set_icon(get_theme_icon(SNAME("Play"), SNAME("EditorIcons"))); + } + clear_button->set_icon(get_theme_icon(SNAME("Clear"), SNAME("EditorIcons"))); + incoming_bandwidth_text->set_right_icon(get_theme_icon(SNAME("ArrowDown"), SNAME("EditorIcons"))); + outgoing_bandwidth_text->set_right_icon(get_theme_icon(SNAME("ArrowUp"), SNAME("EditorIcons"))); + + // This needs to be done here to set the faded color when the profiler is first opened + incoming_bandwidth_text->add_theme_color_override("font_uneditable_color", get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, 0.5)); + outgoing_bandwidth_text->add_theme_color_override("font_uneditable_color", get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, 0.5)); + } break; + } +} + +void EditorNetworkProfiler::_refresh() { + if (!dirty) { + return; + } + dirty = false; + refresh_rpc_data(); + refresh_replication_data(); +} + +void EditorNetworkProfiler::refresh_rpc_data() { + counters_display->clear(); + + TreeItem *root = counters_display->create_item(); + int cols = counters_display->get_columns(); + + for (const KeyValue<ObjectID, RPCNodeInfo> &E : rpc_data) { + TreeItem *node = counters_display->create_item(root); + + for (int j = 0; j < cols; ++j) { + node->set_text_alignment(j, j > 0 ? HORIZONTAL_ALIGNMENT_RIGHT : HORIZONTAL_ALIGNMENT_LEFT); + } + + node->set_text(0, E.value.node_path); + node->set_text(1, E.value.incoming_rpc == 0 ? "-" : vformat(TTR("%d (%s)"), E.value.incoming_rpc, String::humanize_size(E.value.incoming_size))); + node->set_text(2, E.value.outgoing_rpc == 0 ? "-" : vformat(TTR("%d (%s)"), E.value.outgoing_rpc, String::humanize_size(E.value.outgoing_size))); + } +} + +void EditorNetworkProfiler::refresh_replication_data() { + replication_display->clear(); + + TreeItem *root = replication_display->create_item(); + + for (const KeyValue<ObjectID, SyncInfo> &E : sync_data) { + // Ensure the nodes have at least a temporary cache. + ObjectID ids[3] = { E.value.synchronizer, E.value.config, E.value.root_node }; + for (uint32_t i = 0; i < 3; i++) { + const ObjectID &id = ids[i]; + if (!node_data.has(id)) { + missing_node_data.insert(id); + node_data[id] = NodeInfo(id); + } + } + + TreeItem *node = replication_display->create_item(root); + + const NodeInfo &root_info = node_data[E.value.root_node]; + const NodeInfo &sync_info = node_data[E.value.synchronizer]; + const NodeInfo &cfg_info = node_data[E.value.config]; + + node->set_text(0, root_info.path.get_file()); + node->set_icon(0, has_theme_icon(root_info.type, SNAME("EditorIcons")) ? get_theme_icon(root_info.type, SNAME("EditorIcons")) : node_icon); + node->set_tooltip_text(0, root_info.path); + + node->set_text(1, sync_info.path.get_file()); + node->set_icon(1, get_theme_icon("MultiplayerSynchronizer", SNAME("EditorIcons"))); + node->set_tooltip_text(1, sync_info.path); + + int cfg_idx = cfg_info.path.find("::"); + if (cfg_info.path.begins_with("res://") && ResourceLoader::exists(cfg_info.path) && cfg_idx > 0) { + String res_idstr = cfg_info.path.substr(cfg_idx + 2).replace("SceneReplicationConfig_", ""); + String scene_path = cfg_info.path.substr(0, cfg_idx); + node->set_text(2, vformat("%s (%s)", res_idstr, scene_path.get_file())); + node->add_button(2, get_theme_icon(SNAME("InstanceOptions"), SNAME("EditorIcons"))); + node->set_tooltip_text(2, cfg_info.path); + node->set_metadata(2, scene_path); + } else { + node->set_text(2, cfg_info.path); + node->set_metadata(2, ""); + } + + node->set_text(3, vformat("%d - %d", E.value.incoming_syncs, E.value.outgoing_syncs)); + node->set_text(4, vformat("%d - %d", E.value.incoming_size, E.value.outgoing_size)); + } +} + +Array EditorNetworkProfiler::pop_missing_node_data() { + Array out; + for (const ObjectID &id : missing_node_data) { + out.push_back(id); + } + missing_node_data.clear(); + return out; +} + +void EditorNetworkProfiler::add_node_data(const NodeInfo &p_info) { + ERR_FAIL_COND(!node_data.has(p_info.id)); + node_data[p_info.id] = p_info; + dirty = true; +} + +void EditorNetworkProfiler::_activate_pressed() { + if (activate->is_pressed()) { + refresh_timer->start(); + activate->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons"))); + activate->set_text(TTR("Stop")); + } else { + refresh_timer->stop(); + activate->set_icon(get_theme_icon(SNAME("Play"), SNAME("EditorIcons"))); + activate->set_text(TTR("Start")); + } + emit_signal(SNAME("enable_profiling"), activate->is_pressed()); +} + +void EditorNetworkProfiler::_clear_pressed() { + rpc_data.clear(); + sync_data.clear(); + node_data.clear(); + missing_node_data.clear(); + set_bandwidth(0, 0); + refresh_rpc_data(); + refresh_replication_data(); +} + +void EditorNetworkProfiler::_replication_button_clicked(TreeItem *p_item, int p_column, int p_idx, MouseButton p_button) { + if (!p_item) { + return; + } + String meta = p_item->get_metadata(p_column); + if (meta.size() && ResourceLoader::exists(meta)) { + emit_signal("open_request", meta); + } +} + +void EditorNetworkProfiler::add_rpc_frame_data(const RPCNodeInfo &p_frame) { + dirty = true; + if (!rpc_data.has(p_frame.node)) { + rpc_data.insert(p_frame.node, p_frame); + } else { + rpc_data[p_frame.node].incoming_rpc += p_frame.incoming_rpc; + rpc_data[p_frame.node].outgoing_rpc += p_frame.outgoing_rpc; + } + if (p_frame.incoming_rpc) { + rpc_data[p_frame.node].incoming_size = p_frame.incoming_size / p_frame.incoming_rpc; + } + if (p_frame.outgoing_rpc) { + rpc_data[p_frame.node].outgoing_size = p_frame.outgoing_size / p_frame.outgoing_rpc; + } +} + +void EditorNetworkProfiler::add_sync_frame_data(const SyncInfo &p_frame) { + dirty = true; + if (!sync_data.has(p_frame.synchronizer)) { + sync_data[p_frame.synchronizer] = p_frame; + } else { + sync_data[p_frame.synchronizer].incoming_syncs += p_frame.incoming_syncs; + sync_data[p_frame.synchronizer].outgoing_syncs += p_frame.outgoing_syncs; + } + SyncInfo &info = sync_data[p_frame.synchronizer]; + if (info.incoming_syncs) { + info.incoming_size = p_frame.incoming_size / p_frame.incoming_syncs; + } + if (info.outgoing_syncs) { + info.outgoing_size = p_frame.outgoing_size / p_frame.outgoing_syncs; + } +} + +void EditorNetworkProfiler::set_bandwidth(int p_incoming, int p_outgoing) { + incoming_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_incoming))); + outgoing_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_outgoing))); + + // Make labels more prominent when the bandwidth is greater than 0 to attract user attention + incoming_bandwidth_text->add_theme_color_override( + "font_uneditable_color", + get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, p_incoming > 0 ? 1 : 0.5)); + outgoing_bandwidth_text->add_theme_color_override( + "font_uneditable_color", + get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, p_outgoing > 0 ? 1 : 0.5)); +} + +bool EditorNetworkProfiler::is_profiling() { + return activate->is_pressed(); +} + +EditorNetworkProfiler::EditorNetworkProfiler() { + HBoxContainer *hb = memnew(HBoxContainer); + hb->add_theme_constant_override("separation", 8 * EDSCALE); + add_child(hb); + + activate = memnew(Button); + activate->set_toggle_mode(true); + activate->set_text(TTR("Start")); + activate->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_activate_pressed)); + hb->add_child(activate); + + clear_button = memnew(Button); + clear_button->set_text(TTR("Clear")); + clear_button->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_clear_pressed)); + hb->add_child(clear_button); + + hb->add_spacer(); + + Label *lb = memnew(Label); + lb->set_text(TTR("Down")); + hb->add_child(lb); + + incoming_bandwidth_text = memnew(LineEdit); + incoming_bandwidth_text->set_editable(false); + incoming_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + incoming_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + hb->add_child(incoming_bandwidth_text); + + Control *down_up_spacer = memnew(Control); + down_up_spacer->set_custom_minimum_size(Size2(30, 0) * EDSCALE); + hb->add_child(down_up_spacer); + + lb = memnew(Label); + lb->set_text(TTR("Up")); + hb->add_child(lb); + + outgoing_bandwidth_text = memnew(LineEdit); + outgoing_bandwidth_text->set_editable(false); + outgoing_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + outgoing_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + hb->add_child(outgoing_bandwidth_text); + + // Set initial texts in the incoming/outgoing bandwidth labels + set_bandwidth(0, 0); + + HSplitContainer *sc = memnew(HSplitContainer); + add_child(sc); + sc->set_v_size_flags(SIZE_EXPAND_FILL); + sc->set_h_size_flags(SIZE_EXPAND_FILL); + sc->set_split_offset(100 * EDSCALE); + + // RPC + counters_display = memnew(Tree); + counters_display->set_custom_minimum_size(Size2(320, 0) * EDSCALE); + counters_display->set_v_size_flags(SIZE_EXPAND_FILL); + counters_display->set_h_size_flags(SIZE_EXPAND_FILL); + counters_display->set_hide_folding(true); + counters_display->set_hide_root(true); + counters_display->set_columns(3); + counters_display->set_column_titles_visible(true); + counters_display->set_column_title(0, TTR("Node")); + counters_display->set_column_expand(0, true); + counters_display->set_column_clip_content(0, true); + counters_display->set_column_custom_minimum_width(0, 60 * EDSCALE); + counters_display->set_column_title(1, TTR("Incoming RPC")); + counters_display->set_column_expand(1, false); + counters_display->set_column_clip_content(1, true); + counters_display->set_column_custom_minimum_width(1, 120 * EDSCALE); + counters_display->set_column_title(2, TTR("Outgoing RPC")); + counters_display->set_column_expand(2, false); + counters_display->set_column_clip_content(2, true); + counters_display->set_column_custom_minimum_width(2, 120 * EDSCALE); + sc->add_child(counters_display); + + // Replication + replication_display = memnew(Tree); + replication_display->set_custom_minimum_size(Size2(320, 0) * EDSCALE); + replication_display->set_v_size_flags(SIZE_EXPAND_FILL); + replication_display->set_h_size_flags(SIZE_EXPAND_FILL); + replication_display->set_hide_folding(true); + replication_display->set_hide_root(true); + replication_display->set_columns(5); + replication_display->set_column_titles_visible(true); + replication_display->set_column_title(0, TTR("Root")); + replication_display->set_column_expand(0, true); + replication_display->set_column_clip_content(0, true); + replication_display->set_column_custom_minimum_width(0, 80 * EDSCALE); + replication_display->set_column_title(1, TTR("Synchronizer")); + replication_display->set_column_expand(1, true); + replication_display->set_column_clip_content(1, true); + replication_display->set_column_custom_minimum_width(1, 80 * EDSCALE); + replication_display->set_column_title(2, TTR("Config")); + replication_display->set_column_expand(2, true); + replication_display->set_column_clip_content(2, true); + replication_display->set_column_custom_minimum_width(2, 80 * EDSCALE); + replication_display->set_column_title(3, TTR("Count")); + replication_display->set_column_expand(3, false); + replication_display->set_column_clip_content(3, true); + replication_display->set_column_custom_minimum_width(3, 80 * EDSCALE); + replication_display->set_column_title(4, TTR("Size")); + replication_display->set_column_expand(4, false); + replication_display->set_column_clip_content(4, true); + replication_display->set_column_custom_minimum_width(4, 80 * EDSCALE); + replication_display->connect("button_clicked", callable_mp(this, &EditorNetworkProfiler::_replication_button_clicked)); + sc->add_child(replication_display); + + refresh_timer = memnew(Timer); + refresh_timer->set_wait_time(0.5); + refresh_timer->connect("timeout", callable_mp(this, &EditorNetworkProfiler::_refresh)); + add_child(refresh_timer); +} diff --git a/modules/multiplayer/editor/editor_network_profiler.h b/modules/multiplayer/editor/editor_network_profiler.h new file mode 100644 index 0000000000..630747d988 --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.h @@ -0,0 +1,101 @@ +/*************************************************************************/ +/* editor_network_profiler.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 EDITOR_NETWORK_PROFILER_H +#define EDITOR_NETWORK_PROFILER_H + +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/label.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tree.h" + +#include "../multiplayer_debugger.h" + +class EditorNetworkProfiler : public VBoxContainer { + GDCLASS(EditorNetworkProfiler, VBoxContainer) + +public: + struct NodeInfo { + ObjectID id; + String type; + String path; + + NodeInfo() {} + NodeInfo(const ObjectID &p_id) { + id = p_id; + path = String::num_int64(p_id); + } + }; + +private: + using RPCNodeInfo = MultiplayerDebugger::RPCNodeInfo; + using SyncInfo = MultiplayerDebugger::SyncInfo; + + bool dirty = false; + Timer *refresh_timer = nullptr; + Button *activate = nullptr; + Button *clear_button = nullptr; + Tree *counters_display = nullptr; + LineEdit *incoming_bandwidth_text = nullptr; + LineEdit *outgoing_bandwidth_text = nullptr; + Tree *replication_display = nullptr; + + HashMap<ObjectID, RPCNodeInfo> rpc_data; + HashMap<ObjectID, SyncInfo> sync_data; + HashMap<ObjectID, NodeInfo> node_data; + HashSet<ObjectID> missing_node_data; + Ref<Texture2D> node_icon; + + void _activate_pressed(); + void _clear_pressed(); + void _refresh(); + void _replication_button_clicked(TreeItem *p_item, int p_column, int p_idx, MouseButton p_button); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void refresh_rpc_data(); + void refresh_replication_data(); + + Array pop_missing_node_data(); + void add_node_data(const NodeInfo &p_info); + void add_rpc_frame_data(const RPCNodeInfo &p_frame); + void add_sync_frame_data(const SyncInfo &p_frame); + void set_bandwidth(int p_incoming, int p_outgoing); + bool is_profiling(); + + EditorNetworkProfiler(); +}; + +#endif // EDITOR_NETWORK_PROFILER_H diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.cpp b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp new file mode 100644 index 0000000000..c5cf3e6f24 --- /dev/null +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp @@ -0,0 +1,175 @@ +/*************************************************************************/ +/* 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" + +void MultiplayerEditorDebugger::_bind_methods() { + ADD_SIGNAL(MethodInfo("open_request", PropertyInfo(Variant::STRING, "path"))); +} + +bool MultiplayerEditorDebugger::has_capture(const String &p_capture) const { + return p_capture == "multiplayer"; +} + +void MultiplayerEditorDebugger::_open_request(const String &p_path) { + emit_signal("open_request", p_path); +} + +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_rpc_frame_data(frame.infos[i]); + } + return true; + } else if (p_message == "multiplayer:syncs") { + MultiplayerDebugger::ReplicationFrame frame; + frame.deserialize(p_data); + for (const KeyValue<ObjectID, MultiplayerDebugger::SyncInfo> &E : frame.infos) { + profiler->add_sync_frame_data(E.value); + } + Array missing = profiler->pop_missing_node_data(); + if (missing.size()) { + // Asks for the object information. + get_session(p_session)->send_message("multiplayer:cache", missing); + } + return true; + } else if (p_message == "multiplayer:cache") { + ERR_FAIL_COND_V(p_data.size() % 3, false); + for (int i = 0; i < p_data.size(); i += 3) { + EditorNetworkProfiler::NodeInfo info; + info.id = p_data[i].operator ObjectID(); + info.type = p_data[i + 1].operator String(); + info.path = p_data[i + 2].operator String(); + profiler->add_node_data(info); + } + 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:bandwidth", p_enable); + session->toggle_profiler("multiplayer:rpc", p_enable); + session->toggle_profiler("multiplayer:replication", 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->connect("open_request", callable_mp(this, &MultiplayerEditorDebugger::_open_request)); + profiler->set_name(TTR("Network Profiler")); + session->add_session_tab(profiler); + profilers[p_session_id] = profiler; +} + +/// MultiplayerEditorPlugin + +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(); + debugger->connect("open_request", callable_mp(this, &MultiplayerEditorPlugin::_open_request)); +} + +void MultiplayerEditorPlugin::_open_request(const String &p_path) { + get_editor_interface()->open_scene_from_path(p_path); +} + +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(); + } +} diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.h b/modules/multiplayer/editor/multiplayer_editor_plugin.h new file mode 100644 index 0000000000..f29a70e897 --- /dev/null +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.h @@ -0,0 +1,85 @@ +/*************************************************************************/ +/* 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 _open_request(const String &p_path); + void _profiler_activate(bool p_enable, int p_session_id); + +protected: + static void _bind_methods(); + +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 _open_request(const String &p_path); + 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(); +}; + +#endif // MULTIPLAYER_EDITOR_PLUGIN_H diff --git a/modules/multiplayer/editor/replication_editor_plugin.cpp b/modules/multiplayer/editor/replication_editor.cpp index aee5f5b483..ccde9fed22 100644 --- a/modules/multiplayer/editor/replication_editor_plugin.cpp +++ b/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,15 +28,17 @@ /* 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" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" #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" @@ -140,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()) { @@ -200,7 +202,7 @@ ReplicationEditor::ReplicationEditor() { add_pick_button = memnew(Button); add_pick_button->connect("pressed", callable_mp(this, &ReplicationEditor::_pick_new_property)); - add_pick_button->set_text(TTR("Add property to sync..")); + add_pick_button->set_text(TTR("Add property to sync...")); hb->add_child(add_pick_button); VSeparator *vs = memnew(VSeparator); vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); @@ -492,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(); - } -} diff --git a/modules/multiplayer/editor/replication_editor_plugin.h b/modules/multiplayer/editor/replication_editor.h index e60e49cc25..8a48e8dbe7 100644 --- a/modules/multiplayer/editor/replication_editor_plugin.h +++ b/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,21 +28,21 @@ /* 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 "editor/editor_spin_slider.h" -#include "editor/property_selector.h" - -#include "../scene_replication_config.h" +#include "modules/multiplayer/scene_replication_config.h" +#include "scene/gui/box_container.h" class ConfirmationDialog; class MultiplayerSynchronizer; -class SceneTreeDialog; +class AcceptDialog; +class LineEdit; class Tree; class TreeItem; +class PropertySelector; +class SceneTreeDialog; class ReplicationEditor : public VBoxContainer { GDCLASS(ReplicationEditor, VBoxContainer); @@ -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 diff --git a/modules/multiplayer/multiplayer_debugger.cpp b/modules/multiplayer/multiplayer_debugger.cpp new file mode 100644 index 0000000000..9086ee6ec9 --- /dev/null +++ b/modules/multiplayer/multiplayer_debugger.cpp @@ -0,0 +1,333 @@ +/*************************************************************************/ +/* 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 "multiplayer_synchronizer.h" +#include "scene_replication_config.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:bandwidth"); + multiplayer_profilers.push_back(bandwidth); + + Ref<RPCProfiler> rpc_profiler; + rpc_profiler.instantiate(); + rpc_profiler->bind("multiplayer:rpc"); + multiplayer_profilers.push_back(rpc_profiler); + + Ref<ReplicationProfiler> replication_profiler; + replication_profiler.instantiate(); + replication_profiler->bind("multiplayer:replication"); + multiplayer_profilers.push_back(replication_profiler); + + EngineDebugger::register_message_capture("multiplayer", EngineDebugger::Capture(nullptr, &_capture)); +} + +void MultiplayerDebugger::deinitialize() { + multiplayer_profilers.clear(); +} + +Error MultiplayerDebugger::_capture(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured) { + if (p_msg == "cache") { + Array out; + for (int i = 0; i < p_args.size(); i++) { + ObjectID id = p_args[i].operator ObjectID(); + Object *obj = ObjectDB::get_instance(id); + ERR_CONTINUE(!obj); + if (Object::cast_to<SceneReplicationConfig>(obj)) { + out.push_back(id); + out.push_back(obj->get_class()); + out.push_back(((SceneReplicationConfig *)obj)->get_path()); + } else if (Object::cast_to<Node>(obj)) { + out.push_back(id); + out.push_back(obj->get_class()); + out.push_back(String(((Node *)obj)->get_path())); + } else { + ERR_FAIL_V(FAILED); + } + } + EngineDebugger::get_singleton()->send_message("multiplayer:cache", out); + return OK; + } + ERR_FAIL_V(FAILED); +} + +// 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() * 6); + 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].incoming_size); + arr.push_back(infos[i].outgoing_rpc); + arr.push_back(infos[i].outgoing_size); + } + 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 % 6, false); + ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false); + infos.resize(size / 6); + int idx = 1; + for (uint32_t i = 0; i < size / 6; 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].incoming_size = p_arr[idx + 3]; + infos.write[i].outgoing_rpc = p_arr[idx + 4]; + infos.write[i].outgoing_size = p_arr[idx + 5]; + idx += 6; + } + 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(); +} + +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() != 3); + const String what = p_data[0]; + const ObjectID id = p_data[1]; + const int size = p_data[2]; + init_node(id); + RPCNodeInfo &info = rpc_node_data[id]; + if (what == "rpc_in") { + info.incoming_rpc++; + info.incoming_size += size; + } else if (what == "rpc_out") { + info.outgoing_rpc++; + info.outgoing_size += size; + } +} + +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()); + } +} + +// ReplicationProfiler + +MultiplayerDebugger::SyncInfo::SyncInfo(MultiplayerSynchronizer *p_sync) { + ERR_FAIL_COND(!p_sync); + synchronizer = p_sync->get_instance_id(); + if (p_sync->get_replication_config().is_valid()) { + config = p_sync->get_replication_config()->get_instance_id(); + } + if (p_sync->get_root_node()) { + root_node = p_sync->get_root_node()->get_instance_id(); + } +} + +void MultiplayerDebugger::SyncInfo::write_to_array(Array &r_arr) const { + r_arr.push_back(synchronizer); + r_arr.push_back(config); + r_arr.push_back(root_node); + r_arr.push_back(incoming_syncs); + r_arr.push_back(incoming_size); + r_arr.push_back(outgoing_syncs); + r_arr.push_back(outgoing_size); +} + +bool MultiplayerDebugger::SyncInfo::read_from_array(const Array &p_arr, int p_offset) { + ERR_FAIL_COND_V(p_arr.size() - p_offset < 7, false); + synchronizer = int64_t(p_arr[p_offset]); + config = int64_t(p_arr[p_offset + 1]); + root_node = int64_t(p_arr[p_offset + 2]); + incoming_syncs = p_arr[p_offset + 3]; + incoming_size = p_arr[p_offset + 4]; + outgoing_syncs = p_arr[p_offset + 5]; + outgoing_size = p_arr[p_offset + 6]; + return true; +} + +Array MultiplayerDebugger::ReplicationFrame::serialize() { + Array arr; + arr.push_back(infos.size() * 7); + for (const KeyValue<ObjectID, SyncInfo> &E : infos) { + E.value.write_to_array(arr); + } + return arr; +} + +bool MultiplayerDebugger::ReplicationFrame::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 % 7, false); + ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false); + int idx = 1; + for (uint32_t i = 0; i < size / 7; i++) { + SyncInfo info; + if (!info.read_from_array(p_arr, idx)) { + return false; + } + infos[info.synchronizer] = info; + idx += 7; + } + return true; +} + +void MultiplayerDebugger::ReplicationProfiler::toggle(bool p_enable, const Array &p_opts) { + sync_data.clear(); +} + +void MultiplayerDebugger::ReplicationProfiler::add(const Array &p_data) { + ERR_FAIL_COND(p_data.size() != 3); + const String what = p_data[0]; + const ObjectID id = p_data[1]; + const uint64_t size = p_data[2]; + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(ObjectDB::get_instance(id)); + ERR_FAIL_COND(!sync); + if (!sync_data.has(id)) { + sync_data[id] = SyncInfo(sync); + } + SyncInfo &info = sync_data[id]; + if (what == "sync_in") { + info.incoming_syncs++; + info.incoming_size += size; + } else if (what == "sync_out") { + info.outgoing_syncs++; + info.outgoing_size += size; + } +} + +void MultiplayerDebugger::ReplicationProfiler::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; + ReplicationFrame frame; + for (const KeyValue<ObjectID, SyncInfo> &E : sync_data) { + frame.infos[E.key] = E.value; + } + sync_data.clear(); + EngineDebugger::get_singleton()->send_message("multiplayer:syncs", frame.serialize()); + } +} diff --git a/modules/multiplayer/multiplayer_debugger.h b/modules/multiplayer/multiplayer_debugger.h new file mode 100644 index 0000000000..f5c092f0f9 --- /dev/null +++ b/modules/multiplayer/multiplayer_debugger.h @@ -0,0 +1,134 @@ +/*************************************************************************/ +/* 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 MultiplayerSynchronizer; + +class MultiplayerDebugger { +public: + struct RPCNodeInfo { + ObjectID node; + String node_path; + int incoming_rpc = 0; + int incoming_size = 0; + int outgoing_rpc = 0; + int outgoing_size = 0; + }; + + struct RPCFrame { + Vector<RPCNodeInfo> infos; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + + struct SyncInfo { + ObjectID synchronizer; + ObjectID config; + ObjectID root_node; + int incoming_syncs = 0; + int incoming_size = 0; + int outgoing_syncs = 0; + int outgoing_size = 0; + + void write_to_array(Array &r_arr) const; + bool read_from_array(const Array &p_arr, int p_offset); + + SyncInfo() {} + SyncInfo(MultiplayerSynchronizer *p_sync); + }; + + struct ReplicationFrame { + HashMap<ObjectID, SyncInfo> 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 { + 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); + }; + + class ReplicationProfiler : public EngineProfiler { + private: + HashMap<ObjectID, SyncInfo> sync_data; + uint64_t last_profile_time = 0; + + 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); + }; + + static Error _capture(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured); + +public: + static void initialize(); + static void deinitialize(); +}; + +#endif // MULTIPLAYER_DEBUGGER_H diff --git a/modules/multiplayer/register_types.cpp b/modules/multiplayer/register_types.cpp index a2c524da80..ca85a46f31 100644 --- a/modules/multiplayer/register_types.cpp +++ b/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) { @@ -46,15 +47,18 @@ void initialize_multiplayer_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(SceneReplicationConfig); GDREGISTER_CLASS(MultiplayerSpawner); GDREGISTER_CLASS(MultiplayerSynchronizer); + GDREGISTER_CLASS(OfflineMultiplayerPeer); 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(); } diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp index 7df9b95b30..f0da4f9dfc 100644 --- a/modules/multiplayer/scene_cache_interface.cpp +++ b/modules/multiplayer/scene_cache_interface.cpp @@ -99,10 +99,6 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac Ref<MultiplayerPeer> multiplayer_peer = multiplayer->get_multiplayer_peer(); ERR_FAIL_COND(multiplayer_peer.is_null()); -#ifdef DEBUG_ENABLED - multiplayer->profile_bandwidth("out", packet.size()); -#endif - multiplayer_peer->set_transfer_channel(0); multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); multiplayer->send_command(p_from, packet.ptr(), packet.size()); @@ -155,10 +151,6 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodePath p_path, Pat Ref<MultiplayerPeer> multiplayer_peer = multiplayer->get_multiplayer_peer(); ERR_FAIL_COND_V(multiplayer_peer.is_null(), ERR_BUG); -#ifdef DEBUG_ENABLED - multiplayer->profile_bandwidth("out", packet.size() * p_peers.size()); -#endif - Error err = OK; for (int peer_id : p_peers) { multiplayer_peer->set_transfer_channel(0); diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp index db7c5037cd..5042a0502d 100644 --- a/modules/multiplayer/scene_multiplayer.cpp +++ b/modules/multiplayer/scene_multiplayer.cpp @@ -40,13 +40,13 @@ #endif #ifdef DEBUG_ENABLED -void SceneMultiplayer::profile_bandwidth(const String &p_inout, int p_size) { - if (EngineDebugger::is_profiling("multiplayer")) { +_FORCE_INLINE_ void SceneMultiplayer::_profile_bandwidth(const String &p_what, int p_value) { + if (EngineDebugger::is_profiling("multiplayer:bandwidth")) { Array values; - values.push_back(p_inout); + values.push_back(p_what); values.push_back(OS::get_singleton()->get_ticks_msec()); - values.push_back(p_size); - EngineDebugger::profiler_add_frame_data("multiplayer", values); + values.push_back(p_value); + EngineDebugger::profiler_add_frame_data("multiplayer:bandwidth", values); } } #endif @@ -91,6 +91,10 @@ Error SceneMultiplayer::poll() { Error err = multiplayer_peer->get_packet(&packet, len); ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error getting packet! %d", err)); +#ifdef DEBUG_ENABLED + _profile_bandwidth("in", len); +#endif + if (pending_peers.has(sender)) { if (pending_peers[sender].local) { // If the auth is over, admit the peer at the first packet. @@ -220,10 +224,6 @@ void SceneMultiplayer::_process_packet(int p_from, const uint8_t *p_packet, int ERR_FAIL_COND_MSG(root_path.is_empty(), "Multiplayer root was not initialized. If you are using custom multiplayer, remember to set the root path via SceneMultiplayer.set_root_path before using it."); ERR_FAIL_COND_MSG(p_packet_len < 1, "Invalid packet received. Size too small."); -#ifdef DEBUG_ENABLED - profile_bandwidth("in", p_packet_len); -#endif - // Extract the `packet_type` from the LSB three bits: uint8_t packet_type = p_packet[0] & CMD_MASK; @@ -258,6 +258,13 @@ void SceneMultiplayer::_process_packet(int p_from, const uint8_t *p_packet, int } } +#ifdef DEBUG_ENABLED +_FORCE_INLINE_ Error SceneMultiplayer::_send(const uint8_t *p_packet, int p_packet_len) { + _profile_bandwidth("out", p_packet_len); + return multiplayer_peer->put_packet(p_packet, p_packet_len); +} +#endif + Error SceneMultiplayer::send_command(int p_to, const uint8_t *p_packet, int p_packet_len) { if (server_relay && get_unique_id() != 1 && p_to != 1 && multiplayer_peer->is_server_relay_supported()) { // Send relay packet. @@ -268,19 +275,19 @@ Error SceneMultiplayer::send_command(int p_to, const uint8_t *p_packet, int p_pa relay_buffer->put_data(p_packet, p_packet_len); multiplayer_peer->set_target_peer(1); const Vector<uint8_t> data = relay_buffer->get_data_array(); - return multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + return _send(data.ptr(), relay_buffer->get_position()); } if (p_to > 0) { ERR_FAIL_COND_V(!connected_peers.has(p_to), ERR_BUG); multiplayer_peer->set_target_peer(p_to); - return multiplayer_peer->put_packet(p_packet, p_packet_len); + return _send(p_packet, p_packet_len); } else { for (const int &pid : connected_peers) { if (p_to && pid == -p_to) { continue; } multiplayer_peer->set_target_peer(pid); - multiplayer_peer->put_packet(p_packet, p_packet_len); + _send(p_packet, p_packet_len); } return OK; } @@ -319,7 +326,7 @@ void SceneMultiplayer::_process_sys(int p_from, const uint8_t *p_packet, int p_p multiplayer_peer->set_transfer_channel(p_channel); if (peer > 0) { multiplayer_peer->set_target_peer(peer); - multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + _send(data.ptr(), relay_buffer->get_position()); } else { for (const int &P : connected_peers) { // Not to sender, nor excluded. @@ -327,7 +334,7 @@ void SceneMultiplayer::_process_sys(int p_from, const uint8_t *p_packet, int p_p continue; } multiplayer_peer->set_target_peer(P); - multiplayer_peer->put_packet(data.ptr(), relay_buffer->get_position()); + _send(data.ptr(), relay_buffer->get_position()); } } if (peer == 0 || peer == -1) { @@ -373,11 +380,11 @@ void SceneMultiplayer::_admit_peer(int p_id) { // Send new peer to already connected. encode_uint32(p_id, &buf[2]); multiplayer_peer->set_target_peer(P); - multiplayer_peer->put_packet(buf, sizeof(buf)); + _send(buf, sizeof(buf)); // Send already connected to new peer. encode_uint32(P, &buf[2]); multiplayer_peer->set_target_peer(p_id); - multiplayer_peer->put_packet(buf, sizeof(buf)); + _send(buf, sizeof(buf)); } } @@ -412,7 +419,7 @@ void SceneMultiplayer::_del_peer(int p_id) { continue; } multiplayer_peer->set_target_peer(P); - multiplayer_peer->put_packet(buf, sizeof(buf)); + _send(buf, sizeof(buf)); } } @@ -468,7 +475,7 @@ Error SceneMultiplayer::send_auth(int p_to, Vector<uint8_t> p_data) { multiplayer_peer->set_target_peer(p_to); multiplayer_peer->set_transfer_channel(0); multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); - return multiplayer_peer->put_packet(packet_cache.ptr(), p_data.size() + 2); + return _send(packet_cache.ptr(), p_data.size() + 2); } Error SceneMultiplayer::complete_auth(int p_peer) { @@ -478,7 +485,7 @@ Error SceneMultiplayer::complete_auth(int p_peer) { pending_peers[p_peer].local = true; // Notify the remote peer that the authentication has completed. uint8_t buf[2] = { NETWORK_COMMAND_SYS, SYS_COMMAND_AUTH }; - Error err = multiplayer_peer->put_packet(buf, 2); + Error err = _send(buf, 2); // The remote peer already reported the authentication as completed, so admit the peer. // May generate new packets, so it must happen after sending confirmation. if (pending_peers[p_peer].remote) { @@ -654,6 +661,7 @@ SceneMultiplayer::SceneMultiplayer() { replicator = Ref<SceneReplicationInterface>(memnew(SceneReplicationInterface(this))); rpc = Ref<SceneRPCInterface>(memnew(SceneRPCInterface(this))); cache = Ref<SceneCacheInterface>(memnew(SceneCacheInterface(this))); + set_multiplayer_peer(Ref<OfflineMultiplayerPeer>(memnew(OfflineMultiplayerPeer))); } SceneMultiplayer::~SceneMultiplayer() { diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h index b0ecc48f8c..1a8de11f3f 100644 --- a/modules/multiplayer/scene_multiplayer.h +++ b/modules/multiplayer/scene_multiplayer.h @@ -37,6 +37,31 @@ #include "scene_replication_interface.h" #include "scene_rpc_interface.h" +class OfflineMultiplayerPeer : public MultiplayerPeer { + GDCLASS(OfflineMultiplayerPeer, MultiplayerPeer); + +public: + virtual int get_available_packet_count() const override { return 0; } + virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size) override { + *r_buffer = nullptr; + r_buffer_size = 0; + return OK; + } + virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size) override { return OK; } + virtual int get_max_packet_size() const override { return 0; } + + virtual void set_target_peer(int p_peer_id) override {} + virtual int get_packet_peer() const override { return 0; } + virtual TransferMode get_packet_mode() const override { return TRANSFER_MODE_RELIABLE; }; + virtual int get_packet_channel() const override { return 0; } + virtual void disconnect_peer(int p_peer, bool p_force = false) override {} + virtual bool is_server() const override { return true; } + virtual void poll() override {} + virtual void close() override {} + virtual int get_unique_id() const override { return TARGET_PEER_SERVER; } + virtual ConnectionStatus get_connection_status() const override { return CONNECTION_CONNECTED; }; +}; + class SceneMultiplayer : public MultiplayerAPI { GDCLASS(SceneMultiplayer, MultiplayerAPI); @@ -103,6 +128,15 @@ private: Ref<SceneReplicationInterface> replicator; Ref<SceneRPCInterface> rpc; +#ifdef DEBUG_ENABLED + _FORCE_INLINE_ void _profile_bandwidth(const String &p_what, int p_value); + _FORCE_INLINE_ Error _send(const uint8_t *p_packet, int p_packet_len); // Also profiles. +#else + _FORCE_INLINE_ Error _send(const uint8_t *p_packet, int p_packet_len) { + return multiplayer_peer->put_packet(p_packet, p_packet_len); + } +#endif + protected: static void _bind_methods(); @@ -162,10 +196,7 @@ public: bool is_server_relay_enabled() const; Ref<SceneCacheInterface> get_path_cache() { return cache; } - -#ifdef DEBUG_ENABLED - void profile_bandwidth(const String &p_inout, int p_size); -#endif + Ref<SceneReplicationInterface> get_replicator() { return replicator; } SceneMultiplayer(); ~SceneMultiplayer(); diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp index f1bab7327a..7d9437936a 100644 --- a/modules/multiplayer/scene_replication_interface.cpp +++ b/modules/multiplayer/scene_replication_interface.cpp @@ -32,6 +32,7 @@ #include "scene_multiplayer.h" +#include "core/debugger/engine_debugger.h" #include "core/io/marshalls.h" #include "scene/main/node.h" #include "scene/scene_string_names.h" @@ -40,6 +41,18 @@ if (packet_cache.size() < m_amount) \ packet_cache.resize(m_amount); +#ifdef DEBUG_ENABLED +_FORCE_INLINE_ void SceneReplicationInterface::_profile_node_data(const String &p_what, ObjectID p_id, int p_size) { + if (EngineDebugger::is_profiling("multiplayer:replication")) { + Array values; + values.push_back(p_what); + values.push_back(p_id); + values.push_back(p_size); + EngineDebugger::profiler_add_frame_data("multiplayer:replication", values); + } +} +#endif + SceneReplicationInterface::TrackedNode &SceneReplicationInterface::_track(const ObjectID &p_id) { if (!tracked_nodes.has(p_id)) { tracked_nodes[p_id] = TrackedNode(p_id); @@ -244,15 +257,54 @@ void SceneReplicationInterface::_visibility_changed(int p_peer, ObjectID p_sid) Node *node = sync->get_root_node(); ERR_FAIL_COND(!node); // Bug. const ObjectID oid = node->get_instance_id(); - if (spawned_nodes.has(oid)) { + if (spawned_nodes.has(oid) && p_peer != multiplayer->get_unique_id()) { _update_spawn_visibility(p_peer, oid); } _update_sync_visibility(p_peer, sync); } +bool SceneReplicationInterface::is_rpc_visible(const ObjectID &p_oid, int p_peer) const { + if (!tracked_nodes.has(p_oid)) { + return true; // Untracked nodes are always visible to RPCs. + } + ERR_FAIL_COND_V(p_peer < 0, false); + const TrackedNode &tnode = tracked_nodes[p_oid]; + if (tnode.synchronizers.is_empty()) { + return true; // No synchronizers means no visibility restrictions. + } + if (tnode.remote_peer && uint32_t(p_peer) == tnode.remote_peer) { + return true; // RPCs on spawned nodes are always visible to spawner. + } else if (spawned_nodes.has(p_oid)) { + // It's a spwaned node we control, this can be fast + if (p_peer) { + return peers_info.has(p_peer) && peers_info[p_peer].spawn_nodes.has(p_oid); + } else { + for (const KeyValue<int, PeerInfo> &E : peers_info) { + if (!E.value.spawn_nodes.has(p_oid)) { + return false; // Not public. + } + } + return true; // All peers have this node. + } + } else { + // Cycle object synchronizers to check visibility. + for (const ObjectID &sid : tnode.synchronizers) { + MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(sid); + ERR_CONTINUE(!sync); + // RPC visibility is composed using OR when multiple synchronizers are present. + // Note that we don't really care about authority here which may lead to unexpected + // results when using multiple synchronizers to control the same node. + if (sync->is_visible_to(p_peer)) { + return true; + } + } + return false; // Not visible. + } +} + Error SceneReplicationInterface::_update_sync_visibility(int p_peer, MultiplayerSynchronizer *p_sync) { ERR_FAIL_COND_V(!p_sync, ERR_BUG); - if (!multiplayer->has_multiplayer_peer() || !p_sync->is_multiplayer_authority()) { + if (!multiplayer->has_multiplayer_peer() || !p_sync->is_multiplayer_authority() || p_peer == multiplayer->get_unique_id()) { return OK; } @@ -362,13 +414,8 @@ Error SceneReplicationInterface::_update_spawn_visibility(int p_peer, const Obje Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable) { ERR_FAIL_COND_V(!p_buffer || p_size < 1, ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V(!multiplayer, ERR_UNCONFIGURED); ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); -#ifdef DEBUG_ENABLED - multiplayer->profile_bandwidth("out", p_size); -#endif - Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); peer->set_transfer_channel(0); peer->set_transfer_mode(p_reliable ? MultiplayerPeer::TRANSFER_MODE_RELIABLE : MultiplayerPeer::TRANSFER_MODE_UNRELIABLE); @@ -640,6 +687,9 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), &ptr[ofs], size); ofs += size; } +#ifdef DEBUG_ENABLED + _profile_node_data("sync_out", oid, size); +#endif } if (ofs > 3) { // Got some left over to send. @@ -687,6 +737,9 @@ Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_bu err = MultiplayerSynchronizer::set_state(props, node, vars); ERR_FAIL_COND_V(err, err); ofs += size; +#ifdef DEBUG_ENABLED + _profile_node_data("sync_in", sync->get_instance_id(), size); +#endif } return OK; } diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h index c8bd96eb87..30d58f7129 100644 --- a/modules/multiplayer/scene_replication_interface.h +++ b/modules/multiplayer/scene_replication_interface.h @@ -105,6 +105,10 @@ private: return p_id.is_valid() ? Object::cast_to<T>(ObjectDB::get_instance(p_id)) : nullptr; } +#ifdef DEBUG_ENABLED + _FORCE_INLINE_ void _profile_node_data(const String &p_what, ObjectID p_id, int p_size); +#endif + public: static void make_default(); @@ -121,6 +125,8 @@ public: Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len); Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len); + bool is_rpc_visible(const ObjectID &p_oid, int p_peer) const; + SceneReplicationInterface(SceneMultiplayer *p_multiplayer) { multiplayer = p_multiplayer; } diff --git a/modules/multiplayer/scene_rpc_interface.cpp b/modules/multiplayer/scene_rpc_interface.cpp index acc113c901..a7e29dfcc7 100644 --- a/modules/multiplayer/scene_rpc_interface.cpp +++ b/modules/multiplayer/scene_rpc_interface.cpp @@ -52,16 +52,15 @@ #define BYTE_ONLY_OR_NO_ARGS_FLAG (1 << BYTE_ONLY_OR_NO_ARGS_SHIFT) #ifdef DEBUG_ENABLED -_FORCE_INLINE_ void SceneRPCInterface::_profile_node_data(const String &p_what, ObjectID p_id) { - if (EngineDebugger::is_profiling("rpc")) { +_FORCE_INLINE_ void SceneRPCInterface::_profile_node_data(const String &p_what, ObjectID p_id, int p_size) { + if (EngineDebugger::is_profiling("multiplayer:rpc")) { Array values; - values.push_back(p_id); values.push_back(p_what); - EngineDebugger::profiler_add_frame_data("rpc", values); + values.push_back(p_id); + values.push_back(p_size); + EngineDebugger::profiler_add_frame_data("multiplayer:rpc", values); } } -#else -_FORCE_INLINE_ void SceneRPCInterface::_profile_node_data(const String &p_what, ObjectID p_id) {} #endif // Returns the packet size stripping the node path added when the node is not yet cached. @@ -83,7 +82,7 @@ void SceneRPCInterface::_parse_rpc_config(const Variant &p_config, bool p_for_no Array names = config.keys(); names.sort(); // Ensure ID order for (int i = 0; i < names.size(); i++) { - ERR_CONTINUE(names[i].get_type() != Variant::STRING); + ERR_CONTINUE(names[i].get_type() != Variant::STRING && names[i].get_type() != Variant::STRING_NAME); String name = names[i].operator String(); ERR_CONTINUE(config[name].get_type() != Variant::DICTIONARY); ERR_CONTINUE(!config[name].operator Dictionary().has("rpc_mode")); @@ -277,7 +276,7 @@ void SceneRPCInterface::_process_rpc(Node *p_node, const uint16_t p_rpc_method_i argp.resize(argc); #ifdef DEBUG_ENABLED - _profile_node_data("rpc_in", p_node->get_instance_id()); + _profile_node_data("rpc_in", p_node->get_instance_id(), p_packet_len); #endif int out; @@ -296,7 +295,7 @@ void SceneRPCInterface::_process_rpc(Node *p_node, const uint16_t p_rpc_method_i } } -void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount) { +void SceneRPCInterface::_send_rpc(Node *p_node, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount) { Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); ERR_FAIL_COND_MSG(peer.is_null(), "Attempt to call RPC without active multiplayer peer."); @@ -312,12 +311,35 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con ERR_FAIL_MSG("Attempt to call RPC with unknown peer ID: " + itos(p_to) + "."); } - // See if all peers have cached path (if so, call can be fast). - int psc_id; - const bool has_all_peers = multiplayer->get_path_cache()->send_object_cache(p_from, p_to, psc_id); + // See if all peers have cached path (if so, call can be fast) while building the RPC target list. + HashSet<int> targets; + Ref<SceneCacheInterface> cache = multiplayer->get_path_cache(); + int psc_id = -1; + bool has_all_peers = true; + const ObjectID oid = p_node->get_instance_id(); + if (p_to > 0) { + ERR_FAIL_COND_MSG(!multiplayer->get_replicator()->is_rpc_visible(oid, p_to), "Attempt to call an RPC to a peer that cannot see this node. Peer ID: " + itos(p_to)); + targets.insert(p_to); + has_all_peers = cache->send_object_cache(p_node, p_to, psc_id); + } else { + bool restricted = !multiplayer->get_replicator()->is_rpc_visible(oid, 0); + for (const int &P : multiplayer->get_connected_peers()) { + if (p_to < 0 && P == -p_to) { + continue; // Excluded peer. + } + if (restricted && !multiplayer->get_replicator()->is_rpc_visible(oid, P)) { + continue; // Not visible to this peer. + } + targets.insert(P); + bool has_peer = cache->send_object_cache(p_node, P, psc_id); + has_all_peers = has_all_peers && has_peer; + } + } + if (targets.is_empty()) { + return; // No one in sight. + } // Create base packet, lots of hardcode because it must be tight. - int ofs = 0; #define MAKE_ROOM(m_amount) \ @@ -399,20 +421,21 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con ERR_FAIL_COND(node_id_compression > 3); ERR_FAIL_COND(name_id_compression > 1); - // We can now set the meta - packet_cache.write[0] = command_type + (node_id_compression << NODE_ID_COMPRESSION_SHIFT) + (name_id_compression << NAME_ID_COMPRESSION_SHIFT) + (byte_only_or_no_args ? BYTE_ONLY_OR_NO_ARGS_FLAG : 0); - #ifdef DEBUG_ENABLED - multiplayer->profile_bandwidth("out", ofs); + _profile_node_data("rpc_out", p_node->get_instance_id(), ofs); #endif + // We can now set the meta + packet_cache.write[0] = command_type + (node_id_compression << NODE_ID_COMPRESSION_SHIFT) + (name_id_compression << NAME_ID_COMPRESSION_SHIFT) + (byte_only_or_no_args ? BYTE_ONLY_OR_NO_ARGS_FLAG : 0); + // Take chance and set transfer mode, since all send methods will use it. peer->set_transfer_channel(p_config.channel); peer->set_transfer_mode(p_config.transfer_mode); if (has_all_peers) { - // They all have verified paths, so send fast. - multiplayer->send_command(p_to, packet_cache.ptr(), ofs); + for (const int P : targets) { + multiplayer->send_command(P, packet_cache.ptr(), ofs); + } } else { // Unreachable because the node ID is never compressed if the peers doesn't know it. CRASH_COND(node_id_compression != NETWORK_NODE_ID_COMPRESSION_32); @@ -420,23 +443,15 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con // Not all verified path, so send one by one. // Append path at the end, since we will need it for some packets. - NodePath from_path = multiplayer->get_root_path().rel_path_to(p_from->get_path()); + NodePath from_path = multiplayer->get_root_path().rel_path_to(p_node->get_path()); CharString pname = String(from_path).utf8(); int path_len = encode_cstring(pname.get_data(), nullptr); MAKE_ROOM(ofs + path_len); encode_cstring(pname.get_data(), &(packet_cache.write[ofs])); - for (const int &P : multiplayer->get_connected_peers()) { - if (p_to < 0 && P == -p_to) { - continue; // Continue, excluded. - } - - if (p_to > 0 && P != p_to) { - continue; // Continue, not for this peer. - } - + // Not all verified path, so check which needs the longer packet. + for (const int P : targets) { bool confirmed = multiplayer->get_path_cache()->is_cache_confirmed(from_path, P); - if (confirmed) { // This one confirmed path, so use id. encode_uint32(psc_id, &(packet_cache.write[1])); @@ -477,10 +492,6 @@ Error SceneRPCInterface::rpcp(Object *p_obj, int p_peer_id, const StringName &p_ } if (p_peer_id != caller_id) { -#ifdef DEBUG_ENABLED - _profile_node_data("rpc_out", node->get_instance_id()); -#endif - _send_rpc(node, p_peer_id, rpc_id, config, p_method, p_arg, p_argcount); } diff --git a/modules/multiplayer/scene_rpc_interface.h b/modules/multiplayer/scene_rpc_interface.h index aa9be525a2..800293714c 100644 --- a/modules/multiplayer/scene_rpc_interface.h +++ b/modules/multiplayer/scene_rpc_interface.h @@ -81,8 +81,11 @@ private: HashMap<ObjectID, RPCConfigCache> rpc_cache; +#ifdef DEBUG_ENABLED + _FORCE_INLINE_ void _profile_node_data(const String &p_what, ObjectID p_id, int p_size); +#endif + protected: - _FORCE_INLINE_ void _profile_node_data(const String &p_what, ObjectID p_id); void _process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset); void _send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount); |