From 67265d14f7aac8fdd3f7394495a9bf2eef972810 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Wed, 5 Oct 2022 17:50:19 +0200 Subject: [MP] Move engine and editor profilers to a plugin. Also refactor the editor plugin out of the ReplicationEditor. --- .../multiplayer/editor/editor_network_profiler.cpp | 197 ++++++++ .../multiplayer/editor/editor_network_profiler.h | 76 +++ .../editor/multiplayer_editor_plugin.cpp | 140 ++++++ .../multiplayer/editor/multiplayer_editor_plugin.h | 81 +++ modules/multiplayer/editor/replication_editor.cpp | 496 ++++++++++++++++++ modules/multiplayer/editor/replication_editor.h | 108 ++++ .../editor/replication_editor_plugin.cpp | 554 --------------------- .../multiplayer/editor/replication_editor_plugin.h | 131 ----- modules/multiplayer/multiplayer_debugger.cpp | 194 ++++++++ modules/multiplayer/multiplayer_debugger.h | 95 ++++ modules/multiplayer/register_types.cpp | 9 +- 11 files changed, 1393 insertions(+), 688 deletions(-) create mode 100644 modules/multiplayer/editor/editor_network_profiler.cpp create mode 100644 modules/multiplayer/editor/editor_network_profiler.h create mode 100644 modules/multiplayer/editor/multiplayer_editor_plugin.cpp create mode 100644 modules/multiplayer/editor/multiplayer_editor_plugin.h create mode 100644 modules/multiplayer/editor/replication_editor.cpp create mode 100644 modules/multiplayer/editor/replication_editor.h delete mode 100644 modules/multiplayer/editor/replication_editor_plugin.cpp delete mode 100644 modules/multiplayer/editor/replication_editor_plugin.h create mode 100644 modules/multiplayer/multiplayer_debugger.cpp create mode 100644 modules/multiplayer/multiplayer_debugger.h (limited to 'modules') diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp new file mode 100644 index 0000000000..a7e5b80b66 --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -0,0 +1,197 @@ +/*************************************************************************/ +/* 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"))); +} + +void EditorNetworkProfiler::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + 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::_update_frame() { + counters_display->clear(); + + TreeItem *root = counters_display->create_item(); + + for (const KeyValue &E : nodes_data) { + TreeItem *node = counters_display->create_item(root); + + for (int j = 0; j < counters_display->get_columns(); ++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 ? "-" : itos(E.value.incoming_rpc)); + node->set_text(2, E.value.outgoing_rpc == 0 ? "-" : itos(E.value.outgoing_rpc)); + } +} + +void EditorNetworkProfiler::_activate_pressed() { + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons"))); + activate->set_text(TTR("Stop")); + } else { + 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() { + nodes_data.clear(); + set_bandwidth(0, 0); + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +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 { + nodes_data[p_frame.node].incoming_rpc += p_frame.incoming_rpc; + nodes_data[p_frame.node].outgoing_rpc += p_frame.outgoing_rpc; + } + + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +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); + + counters_display = memnew(Tree); + counters_display->set_custom_minimum_size(Size2(300, 0) * EDSCALE); + counters_display->set_v_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); + add_child(counters_display); + + frame_delay = memnew(Timer); + frame_delay->set_wait_time(0.1); + frame_delay->set_one_shot(true); + add_child(frame_delay); + frame_delay->connect("timeout", callable_mp(this, &EditorNetworkProfiler::_update_frame)); +} diff --git a/modules/multiplayer/editor/editor_network_profiler.h b/modules/multiplayer/editor/editor_network_profiler.h new file mode 100644 index 0000000000..98d12e3c0a --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.h @@ -0,0 +1,76 @@ +/*************************************************************************/ +/* 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) + +private: + using RPCNodeInfo = MultiplayerDebugger::RPCNodeInfo; + + Button *activate = nullptr; + Button *clear_button = nullptr; + Tree *counters_display = nullptr; + LineEdit *incoming_bandwidth_text = nullptr; + LineEdit *outgoing_bandwidth_text = nullptr; + + Timer *frame_delay = nullptr; + + HashMap nodes_data; + + void _update_frame(); + + void _activate_pressed(); + void _clear_pressed(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void add_node_frame_data(const RPCNodeInfo 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..00b1537827 --- /dev/null +++ b/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 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 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(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..6d1514cdb1 --- /dev/null +++ b/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 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 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 diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp new file mode 100644 index 0000000000..4ba9cd14f0 --- /dev/null +++ b/modules/multiplayer/editor/replication_editor.cpp @@ -0,0 +1,496 @@ +/*************************************************************************/ +/* replication_editor.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 "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 "scene/gui/dialogs.h" +#include "scene/gui/separator.h" +#include "scene/gui/tree.h" + +void ReplicationEditor::_pick_node_filter_text_changed(const String &p_newtext) { + TreeItem *root_item = pick_node->get_scene_tree()->get_scene_tree()->get_root(); + + Vector select_candidates; + Node *to_select = nullptr; + + String filter = pick_node->get_filter_line_edit()->get_text(); + + _pick_node_select_recursive(root_item, filter, select_candidates); + + if (!select_candidates.is_empty()) { + for (int i = 0; i < select_candidates.size(); ++i) { + Node *candidate = select_candidates[i]; + + if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) { + to_select = candidate; + break; + } + } + + if (!to_select) { + to_select = select_candidates[0]; + } + } + + pick_node->get_scene_tree()->set_selected(to_select); +} + +void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector &p_select_candidates) { + if (!p_item) { + return; + } + + NodePath np = p_item->get_metadata(0); + Node *node = get_node(np); + + if (!p_filter.is_empty() && ((String)node->get_name()).findn(p_filter) != -1) { + p_select_candidates.push_back(node); + } + + TreeItem *c = p_item->get_first_child(); + + while (c) { + _pick_node_select_recursive(c, p_filter, p_select_candidates); + c = c->get_next(); + } +} + +void ReplicationEditor::_pick_node_filter_input(const Ref &p_ie) { + Ref k = p_ie; + + if (k.is_valid()) { + switch (k->get_keycode()) { + case Key::UP: + case Key::DOWN: + case Key::PAGEUP: + case Key::PAGEDOWN: { + pick_node->get_scene_tree()->get_scene_tree()->gui_input(k); + pick_node->get_filter_line_edit()->accept_event(); + } break; + default: + break; + } + } +} + +void ReplicationEditor::_pick_node_selected(NodePath p_path) { + Node *root = current->get_node(current->get_root_path()); + ERR_FAIL_COND(!root); + Node *node = get_node(p_path); + ERR_FAIL_COND(!node); + NodePath path_to = root->get_path_to(node); + adding_node_path = path_to; + prop_selector->select_property_from_instance(node); +} + +void ReplicationEditor::_pick_new_property() { + if (current == nullptr) { + EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it.")); + return; + } + Node *root = current->get_node(current->get_root_path()); + if (!root) { + EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root.")); + return; + } + pick_node->popup_scenetree_dialog(); + pick_node->get_filter_line_edit()->clear(); + pick_node->get_filter_line_edit()->grab_focus(); +} + +void ReplicationEditor::_add_sync_property(String p_path) { + config = current->get_replication_config(); + + if (config.is_valid() && config->has_property(p_path)) { + EditorNode::get_singleton()->show_warning(TTR("Property is already being synchronized.")); + return; + } + + Ref &undo_redo = EditorNode::get_singleton()->get_undo_redo(); + undo_redo->create_action(TTR("Add property to synchronizer")); + + if (config.is_null()) { + config.instantiate(); + current->set_replication_config(config); + undo_redo->add_do_method(current, "set_replication_config", config); + undo_redo->add_undo_method(current, "set_replication_config", Ref()); + _update_config(); + } + + undo_redo->add_do_method(config.ptr(), "add_property", p_path); + undo_redo->add_undo_method(config.ptr(), "remove_property", p_path); + undo_redo->add_do_method(this, "_update_config"); + undo_redo->add_undo_method(this, "_update_config"); + undo_redo->commit_action(); +} + +void ReplicationEditor::_pick_node_property_selected(String p_name) { + String adding_prop_path = String(adding_node_path) + ":" + p_name; + + _add_sync_property(adding_prop_path); +} + +/// ReplicationEditor +ReplicationEditor::ReplicationEditor() { + set_v_size_flags(SIZE_EXPAND_FILL); + set_custom_minimum_size(Size2(0, 200) * EDSCALE); + + delete_dialog = memnew(ConfirmationDialog); + delete_dialog->connect("cancelled", callable_mp(this, &ReplicationEditor::_dialog_closed).bind(false)); + delete_dialog->connect("confirmed", callable_mp(this, &ReplicationEditor::_dialog_closed).bind(true)); + add_child(delete_dialog); + + error_dialog = memnew(AcceptDialog); + error_dialog->set_ok_button_text(TTR("Close")); + error_dialog->set_title(TTR("Error!")); + add_child(error_dialog); + + VBoxContainer *vb = memnew(VBoxContainer); + vb->set_v_size_flags(SIZE_EXPAND_FILL); + add_child(vb); + + pick_node = memnew(SceneTreeDialog); + add_child(pick_node); + pick_node->register_text_enter(pick_node->get_filter_line_edit()); + pick_node->set_title(TTR("Pick a node to synchronize:")); + pick_node->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_selected)); + pick_node->get_filter_line_edit()->connect("text_changed", callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed)); + pick_node->get_filter_line_edit()->connect("gui_input", callable_mp(this, &ReplicationEditor::_pick_node_filter_input)); + + prop_selector = memnew(PropertySelector); + add_child(prop_selector); + prop_selector->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_property_selected)); + + HBoxContainer *hb = memnew(HBoxContainer); + vb->add_child(hb); + + 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..")); + hb->add_child(add_pick_button); + VSeparator *vs = memnew(VSeparator); + vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); + hb->add_child(vs); + hb->add_child(memnew(Label(TTR("Path:")))); + np_line_edit = memnew(LineEdit); + np_line_edit->set_placeholder(":property"); + np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + hb->add_child(np_line_edit); + add_from_path_button = memnew(Button); + add_from_path_button->connect("pressed", callable_mp(this, &ReplicationEditor::_add_pressed)); + add_from_path_button->set_text(TTR("Add from path")); + hb->add_child(add_from_path_button); + vs = memnew(VSeparator); + vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); + hb->add_child(vs); + pin = memnew(Button); + pin->set_flat(true); + pin->set_toggle_mode(true); + hb->add_child(pin); + + tree = memnew(Tree); + tree->set_hide_root(true); + tree->set_columns(4); + tree->set_column_titles_visible(true); + tree->set_column_title(0, TTR("Properties")); + tree->set_column_expand(0, true); + tree->set_column_title(1, TTR("Spawn")); + tree->set_column_expand(1, false); + tree->set_column_custom_minimum_width(1, 100); + tree->set_column_title(2, TTR("Sync")); + tree->set_column_custom_minimum_width(2, 100); + tree->set_column_expand(2, false); + tree->set_column_expand(3, false); + tree->create_item(); + tree->connect("button_clicked", callable_mp(this, &ReplicationEditor::_tree_button_pressed)); + tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited)); + tree->set_v_size_flags(SIZE_EXPAND_FILL); + vb->add_child(tree); + + drop_label = memnew(Label); + drop_label->set_text(TTR("Add properties using the buttons above or\ndrag them them from the inspector and drop them here.")); + drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); + tree->add_child(drop_label); + drop_label->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + + tree->set_drag_forwarding(this); +} + +void ReplicationEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_update_config"), &ReplicationEditor::_update_config); + ClassDB::bind_method(D_METHOD("_update_checked", "property", "column", "checked"), &ReplicationEditor::_update_checked); + ClassDB::bind_method("_can_drop_data_fw", &ReplicationEditor::_can_drop_data_fw); + ClassDB::bind_method("_drop_data_fw", &ReplicationEditor::_drop_data_fw); +} + +bool ReplicationEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { + Dictionary d = p_data; + if (!d.has("type")) { + return false; + } + String t = d["type"]; + if (t != "obj_property") { + return false; + } + Object *obj = d["object"]; + if (!obj) { + return false; + } + Node *node = Object::cast_to(obj); + if (!node) { + return false; + } + + return true; +} + +void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + if (current == nullptr) { + EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it.")); + return; + } + Node *root = current->get_node(current->get_root_path()); + if (!root) { + EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root.")); + return; + } + + Dictionary d = p_data; + if (!d.has("type")) { + return; + } + String t = d["type"]; + if (t != "obj_property") { + return; + } + Object *obj = d["object"]; + if (!obj) { + return; + } + Node *node = Object::cast_to(obj); + if (!node) { + return; + } + + String path = root->get_path_to(node); + path += ":" + String(d["property"]); + + _add_sync_property(path); +} + +void ReplicationEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox(SNAME("panel"), SNAME("Panel"))); + add_pick_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); + pin->set_icon(get_theme_icon(SNAME("Pin"), SNAME("EditorIcons"))); + } break; + } +} + +void ReplicationEditor::_add_pressed() { + if (!current) { + error_dialog->set_text(TTR("Please select a MultiplayerSynchronizer first.")); + error_dialog->popup_centered(); + return; + } + if (current->get_root_path().is_empty()) { + error_dialog->set_text(TTR("The MultiplayerSynchronizer needs a root path.")); + error_dialog->popup_centered(); + return; + } + String np_text = np_line_edit->get_text(); + int idx = np_text.find(":"); + if (idx == -1) { + np_text = ".:" + np_text; + } else if (idx == 0) { + np_text = "." + np_text; + } + NodePath path = NodePath(np_text); + + _add_sync_property(path); +} + +void ReplicationEditor::_tree_item_edited() { + TreeItem *ti = tree->get_edited(); + if (!ti || config.is_null()) { + return; + } + int column = tree->get_edited_column(); + ERR_FAIL_COND(column < 1 || column > 2); + const NodePath prop = ti->get_metadata(0); + Ref &undo_redo = EditorNode::get_undo_redo(); + bool value = ti->is_checked(column); + String method; + if (column == 1) { + undo_redo->create_action(TTR("Set spawn property")); + method = "property_set_spawn"; + } else { + undo_redo->create_action(TTR("Set sync property")); + method = "property_set_sync"; + } + undo_redo->add_do_method(config.ptr(), method, prop, value); + undo_redo->add_undo_method(config.ptr(), method, prop, !value); + undo_redo->add_do_method(this, "_update_checked", prop, column, value); + undo_redo->add_undo_method(this, "_update_checked", prop, column, !value); + undo_redo->commit_action(); +} + +void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { + if (p_button != MouseButton::LEFT) { + return; + } + + TreeItem *ti = Object::cast_to(p_item); + if (!ti) { + return; + } + deleting = ti->get_metadata(0); + delete_dialog->set_text(TTR("Delete Property?") + "\n\"" + ti->get_text(0) + "\""); + delete_dialog->popup_centered(); +} + +void ReplicationEditor::_dialog_closed(bool p_confirmed) { + if (deleting.is_empty() || config.is_null()) { + return; + } + if (p_confirmed) { + const NodePath prop = deleting; + int idx = config->property_get_index(prop); + bool spawn = config->property_get_spawn(prop); + bool sync = config->property_get_sync(prop); + Ref &undo_redo = EditorNode::get_undo_redo(); + undo_redo->create_action(TTR("Remove Property")); + undo_redo->add_do_method(config.ptr(), "remove_property", prop); + undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx); + undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn); + undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync); + undo_redo->add_do_method(this, "_update_config"); + undo_redo->add_undo_method(this, "_update_config"); + undo_redo->commit_action(); + } + deleting = NodePath(); +} + +void ReplicationEditor::_update_checked(const NodePath &p_prop, int p_column, bool p_checked) { + if (!tree->get_root()) { + return; + } + TreeItem *ti = tree->get_root()->get_first_child(); + while (ti) { + if (ti->get_metadata(0).operator NodePath() == p_prop) { + ti->set_checked(p_column, p_checked); + return; + } + ti = ti->get_next(); + } +} + +void ReplicationEditor::_update_config() { + deleting = NodePath(); + tree->clear(); + tree->create_item(); + drop_label->set_visible(true); + if (!config.is_valid()) { + return; + } + TypedArray props = config->get_properties(); + if (props.size()) { + drop_label->set_visible(false); + } + for (int i = 0; i < props.size(); i++) { + const NodePath path = props[i]; + _add_property(path, config->property_get_spawn(path), config->property_get_sync(path)); + } +} + +void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) { + if (current == p_sync) { + return; + } + current = p_sync; + if (current) { + config = current->get_replication_config(); + } else { + config.unref(); + } + _update_config(); +} + +Ref ReplicationEditor::_get_class_icon(const Node *p_node) { + if (!p_node || !has_theme_icon(p_node->get_class(), "EditorIcons")) { + return get_theme_icon(SNAME("ImportFail"), SNAME("EditorIcons")); + } + return get_theme_icon(p_node->get_class(), "EditorIcons"); +} + +void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) { + String prop = String(p_property); + TreeItem *item = tree->create_item(); + item->set_selectable(0, false); + item->set_selectable(1, false); + item->set_selectable(2, false); + item->set_selectable(3, false); + item->set_text(0, prop); + item->set_metadata(0, prop); + Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr; + Ref icon = _get_class_icon(root_node); + if (root_node) { + String path = prop.substr(0, prop.find(":")); + String subpath = prop.substr(path.size()); + Node *node = root_node->get_node_or_null(path); + if (!node) { + node = root_node; + } + item->set_text(0, String(node->get_name()) + ":" + subpath); + icon = _get_class_icon(node); + } + item->set_icon(0, icon); + item->add_button(3, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); + item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER); + item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); + item->set_checked(1, p_spawn); + item->set_editable(1, true); + item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER); + item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK); + item->set_checked(2, p_sync); + item->set_editable(2, true); +} diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h new file mode 100644 index 0000000000..8a48e8dbe7 --- /dev/null +++ b/modules/multiplayer/editor/replication_editor.h @@ -0,0 +1,108 @@ +/*************************************************************************/ +/* replication_editor.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 REPLICATION_EDITOR_H +#define REPLICATION_EDITOR_H + +#include "editor/editor_plugin.h" +#include "modules/multiplayer/scene_replication_config.h" +#include "scene/gui/box_container.h" + +class ConfirmationDialog; +class MultiplayerSynchronizer; +class AcceptDialog; +class LineEdit; +class Tree; +class TreeItem; +class PropertySelector; +class SceneTreeDialog; + +class ReplicationEditor : public VBoxContainer { + GDCLASS(ReplicationEditor, VBoxContainer); + +private: + MultiplayerSynchronizer *current = nullptr; + + AcceptDialog *error_dialog = nullptr; + ConfirmationDialog *delete_dialog = nullptr; + Button *add_pick_button = nullptr; + Button *add_from_path_button = nullptr; + LineEdit *np_line_edit = nullptr; + + Label *drop_label = nullptr; + + Ref config; + NodePath deleting; + Tree *tree = nullptr; + + PropertySelector *prop_selector = nullptr; + SceneTreeDialog *pick_node = nullptr; + NodePath adding_node_path; + + Button *pin = nullptr; + + Ref _get_class_icon(const Node *p_node); + + void _add_pressed(); + void _tree_item_edited(); + void _tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button); + void _update_checked(const NodePath &p_prop, int p_column, bool p_checked); + void _update_config(); + void _dialog_closed(bool p_confirmed); + void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true); + + void _pick_node_filter_text_changed(const String &p_newtext); + void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector &p_select_candidates); + void _pick_node_filter_input(const Ref &p_ie); + void _pick_node_selected(NodePath p_path); + + void _pick_new_property(); + void _pick_node_property_selected(String p_name); + + bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; + void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + + void _add_sync_property(String p_path); + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +public: + void edit(MultiplayerSynchronizer *p_object); + MultiplayerSynchronizer *get_current() const { return current; } + + Button *get_pin() { return pin; } + ReplicationEditor(); + ~ReplicationEditor() {} +}; + +#endif // REPLICATION_EDITOR_H diff --git a/modules/multiplayer/editor/replication_editor_plugin.cpp b/modules/multiplayer/editor/replication_editor_plugin.cpp deleted file mode 100644 index de10420652..0000000000 --- a/modules/multiplayer/editor/replication_editor_plugin.cpp +++ /dev/null @@ -1,554 +0,0 @@ -/*************************************************************************/ -/* replication_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 "replication_editor_plugin.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" - -void ReplicationEditor::_pick_node_filter_text_changed(const String &p_newtext) { - TreeItem *root_item = pick_node->get_scene_tree()->get_scene_tree()->get_root(); - - Vector select_candidates; - Node *to_select = nullptr; - - String filter = pick_node->get_filter_line_edit()->get_text(); - - _pick_node_select_recursive(root_item, filter, select_candidates); - - if (!select_candidates.is_empty()) { - for (int i = 0; i < select_candidates.size(); ++i) { - Node *candidate = select_candidates[i]; - - if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) { - to_select = candidate; - break; - } - } - - if (!to_select) { - to_select = select_candidates[0]; - } - } - - pick_node->get_scene_tree()->set_selected(to_select); -} - -void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector &p_select_candidates) { - if (!p_item) { - return; - } - - NodePath np = p_item->get_metadata(0); - Node *node = get_node(np); - - if (!p_filter.is_empty() && ((String)node->get_name()).findn(p_filter) != -1) { - p_select_candidates.push_back(node); - } - - TreeItem *c = p_item->get_first_child(); - - while (c) { - _pick_node_select_recursive(c, p_filter, p_select_candidates); - c = c->get_next(); - } -} - -void ReplicationEditor::_pick_node_filter_input(const Ref &p_ie) { - Ref k = p_ie; - - if (k.is_valid()) { - switch (k->get_keycode()) { - case Key::UP: - case Key::DOWN: - case Key::PAGEUP: - case Key::PAGEDOWN: { - pick_node->get_scene_tree()->get_scene_tree()->gui_input(k); - pick_node->get_filter_line_edit()->accept_event(); - } break; - default: - break; - } - } -} - -void ReplicationEditor::_pick_node_selected(NodePath p_path) { - Node *root = current->get_node(current->get_root_path()); - ERR_FAIL_COND(!root); - Node *node = get_node(p_path); - ERR_FAIL_COND(!node); - NodePath path_to = root->get_path_to(node); - adding_node_path = path_to; - prop_selector->select_property_from_instance(node); -} - -void ReplicationEditor::_pick_new_property() { - if (current == nullptr) { - EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it.")); - return; - } - Node *root = current->get_node(current->get_root_path()); - if (!root) { - EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root.")); - return; - } - pick_node->popup_scenetree_dialog(); - pick_node->get_filter_line_edit()->clear(); - pick_node->get_filter_line_edit()->grab_focus(); -} - -void ReplicationEditor::_add_sync_property(String p_path) { - config = current->get_replication_config(); - - if (config.is_valid() && config->has_property(p_path)) { - EditorNode::get_singleton()->show_warning(TTR("Property is already being synchronized.")); - return; - } - - Ref undo_redo = EditorNode::get_singleton()->get_undo_redo(); - undo_redo->create_action(TTR("Add property to synchronizer")); - - if (config.is_null()) { - config.instantiate(); - current->set_replication_config(config); - undo_redo->add_do_method(current, "set_replication_config", config); - undo_redo->add_undo_method(current, "set_replication_config", Ref()); - _update_config(); - } - - undo_redo->add_do_method(config.ptr(), "add_property", p_path); - undo_redo->add_undo_method(config.ptr(), "remove_property", p_path); - undo_redo->add_do_method(this, "_update_config"); - undo_redo->add_undo_method(this, "_update_config"); - undo_redo->commit_action(); -} - -void ReplicationEditor::_pick_node_property_selected(String p_name) { - String adding_prop_path = String(adding_node_path) + ":" + p_name; - - _add_sync_property(adding_prop_path); -} - -/// ReplicationEditor -ReplicationEditor::ReplicationEditor() { - set_v_size_flags(SIZE_EXPAND_FILL); - set_custom_minimum_size(Size2(0, 200) * EDSCALE); - - delete_dialog = memnew(ConfirmationDialog); - delete_dialog->connect("cancelled", callable_mp(this, &ReplicationEditor::_dialog_closed).bind(false)); - delete_dialog->connect("confirmed", callable_mp(this, &ReplicationEditor::_dialog_closed).bind(true)); - add_child(delete_dialog); - - error_dialog = memnew(AcceptDialog); - error_dialog->set_ok_button_text(TTR("Close")); - error_dialog->set_title(TTR("Error!")); - add_child(error_dialog); - - VBoxContainer *vb = memnew(VBoxContainer); - vb->set_v_size_flags(SIZE_EXPAND_FILL); - add_child(vb); - - pick_node = memnew(SceneTreeDialog); - add_child(pick_node); - pick_node->register_text_enter(pick_node->get_filter_line_edit()); - pick_node->set_title(TTR("Pick a node to synchronize:")); - pick_node->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_selected)); - pick_node->get_filter_line_edit()->connect("text_changed", callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed)); - pick_node->get_filter_line_edit()->connect("gui_input", callable_mp(this, &ReplicationEditor::_pick_node_filter_input)); - - prop_selector = memnew(PropertySelector); - add_child(prop_selector); - prop_selector->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_property_selected)); - - HBoxContainer *hb = memnew(HBoxContainer); - vb->add_child(hb); - - 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..")); - hb->add_child(add_pick_button); - VSeparator *vs = memnew(VSeparator); - vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); - hb->add_child(vs); - hb->add_child(memnew(Label(TTR("Path:")))); - np_line_edit = memnew(LineEdit); - np_line_edit->set_placeholder(":property"); - np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); - hb->add_child(np_line_edit); - add_from_path_button = memnew(Button); - add_from_path_button->connect("pressed", callable_mp(this, &ReplicationEditor::_add_pressed)); - add_from_path_button->set_text(TTR("Add from path")); - hb->add_child(add_from_path_button); - vs = memnew(VSeparator); - vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); - hb->add_child(vs); - pin = memnew(Button); - pin->set_flat(true); - pin->set_toggle_mode(true); - hb->add_child(pin); - - tree = memnew(Tree); - tree->set_hide_root(true); - tree->set_columns(4); - tree->set_column_titles_visible(true); - tree->set_column_title(0, TTR("Properties")); - tree->set_column_expand(0, true); - tree->set_column_title(1, TTR("Spawn")); - tree->set_column_expand(1, false); - tree->set_column_custom_minimum_width(1, 100); - tree->set_column_title(2, TTR("Sync")); - tree->set_column_custom_minimum_width(2, 100); - tree->set_column_expand(2, false); - tree->set_column_expand(3, false); - tree->create_item(); - tree->connect("button_clicked", callable_mp(this, &ReplicationEditor::_tree_button_pressed)); - tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited)); - tree->set_v_size_flags(SIZE_EXPAND_FILL); - vb->add_child(tree); - - drop_label = memnew(Label); - drop_label->set_text(TTR("Add properties using the buttons above or\ndrag them them from the inspector and drop them here.")); - drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); - tree->add_child(drop_label); - drop_label->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); - - tree->set_drag_forwarding(this); -} - -void ReplicationEditor::_bind_methods() { - ClassDB::bind_method(D_METHOD("_update_config"), &ReplicationEditor::_update_config); - ClassDB::bind_method(D_METHOD("_update_checked", "property", "column", "checked"), &ReplicationEditor::_update_checked); - ClassDB::bind_method("_can_drop_data_fw", &ReplicationEditor::_can_drop_data_fw); - ClassDB::bind_method("_drop_data_fw", &ReplicationEditor::_drop_data_fw); -} - -bool ReplicationEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { - Dictionary d = p_data; - if (!d.has("type")) { - return false; - } - String t = d["type"]; - if (t != "obj_property") { - return false; - } - Object *obj = d["object"]; - if (!obj) { - return false; - } - Node *node = Object::cast_to(obj); - if (!node) { - return false; - } - - return true; -} - -void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { - if (current == nullptr) { - EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it.")); - return; - } - Node *root = current->get_node(current->get_root_path()); - if (!root) { - EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root.")); - return; - } - - Dictionary d = p_data; - if (!d.has("type")) { - return; - } - String t = d["type"]; - if (t != "obj_property") { - return; - } - Object *obj = d["object"]; - if (!obj) { - return; - } - Node *node = Object::cast_to(obj); - if (!node) { - return; - } - - String path = root->get_path_to(node); - path += ":" + String(d["property"]); - - _add_sync_property(path); -} - -void ReplicationEditor::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: - case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { - add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox(SNAME("panel"), SNAME("Panel"))); - add_pick_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons"))); - pin->set_icon(get_theme_icon(SNAME("Pin"), SNAME("EditorIcons"))); - } break; - } -} - -void ReplicationEditor::_add_pressed() { - if (!current) { - error_dialog->set_text(TTR("Please select a MultiplayerSynchronizer first.")); - error_dialog->popup_centered(); - return; - } - if (current->get_root_path().is_empty()) { - error_dialog->set_text(TTR("The MultiplayerSynchronizer needs a root path.")); - error_dialog->popup_centered(); - return; - } - String np_text = np_line_edit->get_text(); - int idx = np_text.find(":"); - if (idx == -1) { - np_text = ".:" + np_text; - } else if (idx == 0) { - np_text = "." + np_text; - } - NodePath path = NodePath(np_text); - - _add_sync_property(path); -} - -void ReplicationEditor::_tree_item_edited() { - TreeItem *ti = tree->get_edited(); - if (!ti || config.is_null()) { - return; - } - int column = tree->get_edited_column(); - ERR_FAIL_COND(column < 1 || column > 2); - const NodePath prop = ti->get_metadata(0); - Ref &undo_redo = EditorNode::get_undo_redo(); - bool value = ti->is_checked(column); - String method; - if (column == 1) { - undo_redo->create_action(TTR("Set spawn property")); - method = "property_set_spawn"; - } else { - undo_redo->create_action(TTR("Set sync property")); - method = "property_set_sync"; - } - undo_redo->add_do_method(config.ptr(), method, prop, value); - undo_redo->add_undo_method(config.ptr(), method, prop, !value); - undo_redo->add_do_method(this, "_update_checked", prop, column, value); - undo_redo->add_undo_method(this, "_update_checked", prop, column, !value); - undo_redo->commit_action(); -} - -void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { - if (p_button != MouseButton::LEFT) { - return; - } - - TreeItem *ti = Object::cast_to(p_item); - if (!ti) { - return; - } - deleting = ti->get_metadata(0); - delete_dialog->set_text(TTR("Delete Property?") + "\n\"" + ti->get_text(0) + "\""); - delete_dialog->popup_centered(); -} - -void ReplicationEditor::_dialog_closed(bool p_confirmed) { - if (deleting.is_empty() || config.is_null()) { - return; - } - if (p_confirmed) { - const NodePath prop = deleting; - int idx = config->property_get_index(prop); - bool spawn = config->property_get_spawn(prop); - bool sync = config->property_get_sync(prop); - Ref &undo_redo = EditorNode::get_undo_redo(); - undo_redo->create_action(TTR("Remove Property")); - undo_redo->add_do_method(config.ptr(), "remove_property", prop); - undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx); - undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn); - undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync); - undo_redo->add_do_method(this, "_update_config"); - undo_redo->add_undo_method(this, "_update_config"); - undo_redo->commit_action(); - } - deleting = NodePath(); -} - -void ReplicationEditor::_update_checked(const NodePath &p_prop, int p_column, bool p_checked) { - if (!tree->get_root()) { - return; - } - TreeItem *ti = tree->get_root()->get_first_child(); - while (ti) { - if (ti->get_metadata(0).operator NodePath() == p_prop) { - ti->set_checked(p_column, p_checked); - return; - } - ti = ti->get_next(); - } -} - -void ReplicationEditor::_update_config() { - deleting = NodePath(); - tree->clear(); - tree->create_item(); - drop_label->set_visible(true); - if (!config.is_valid()) { - return; - } - TypedArray props = config->get_properties(); - if (props.size()) { - drop_label->set_visible(false); - } - for (int i = 0; i < props.size(); i++) { - const NodePath path = props[i]; - _add_property(path, config->property_get_spawn(path), config->property_get_sync(path)); - } -} - -void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) { - if (current == p_sync) { - return; - } - current = p_sync; - if (current) { - config = current->get_replication_config(); - } else { - config.unref(); - } - _update_config(); -} - -Ref ReplicationEditor::_get_class_icon(const Node *p_node) { - if (!p_node || !has_theme_icon(p_node->get_class(), "EditorIcons")) { - return get_theme_icon(SNAME("ImportFail"), SNAME("EditorIcons")); - } - return get_theme_icon(p_node->get_class(), "EditorIcons"); -} - -void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) { - String prop = String(p_property); - TreeItem *item = tree->create_item(); - item->set_selectable(0, false); - item->set_selectable(1, false); - item->set_selectable(2, false); - item->set_selectable(3, false); - item->set_text(0, prop); - item->set_metadata(0, prop); - Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr; - Ref icon = _get_class_icon(root_node); - if (root_node) { - String path = prop.substr(0, prop.find(":")); - String subpath = prop.substr(path.size()); - Node *node = root_node->get_node_or_null(path); - if (!node) { - node = root_node; - } - item->set_text(0, String(node->get_name()) + ":" + subpath); - icon = _get_class_icon(node); - } - item->set_icon(0, icon); - item->add_button(3, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); - item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER); - item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); - item->set_checked(1, p_spawn); - item->set_editable(1, true); - item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER); - item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK); - 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(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_plugin.h deleted file mode 100644 index 6c40a99293..0000000000 --- a/modules/multiplayer/editor/replication_editor_plugin.h +++ /dev/null @@ -1,131 +0,0 @@ -/*************************************************************************/ -/* replication_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 REPLICATION_EDITOR_PLUGIN_H -#define REPLICATION_EDITOR_PLUGIN_H - -#include "editor/editor_plugin.h" -#include "modules/multiplayer/scene_replication_config.h" -#include "scene/gui/box_container.h" - -class ConfirmationDialog; -class MultiplayerSynchronizer; -class AcceptDialog; -class LineEdit; -class Tree; -class TreeItem; -class PropertySelector; -class SceneTreeDialog; - -class ReplicationEditor : public VBoxContainer { - GDCLASS(ReplicationEditor, VBoxContainer); - -private: - MultiplayerSynchronizer *current = nullptr; - - AcceptDialog *error_dialog = nullptr; - ConfirmationDialog *delete_dialog = nullptr; - Button *add_pick_button = nullptr; - Button *add_from_path_button = nullptr; - LineEdit *np_line_edit = nullptr; - - Label *drop_label = nullptr; - - Ref config; - NodePath deleting; - Tree *tree = nullptr; - - PropertySelector *prop_selector = nullptr; - SceneTreeDialog *pick_node = nullptr; - NodePath adding_node_path; - - Button *pin = nullptr; - - Ref _get_class_icon(const Node *p_node); - - void _add_pressed(); - void _tree_item_edited(); - void _tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button); - void _update_checked(const NodePath &p_prop, int p_column, bool p_checked); - void _update_config(); - void _dialog_closed(bool p_confirmed); - void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true); - - void _pick_node_filter_text_changed(const String &p_newtext); - void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector &p_select_candidates); - void _pick_node_filter_input(const Ref &p_ie); - void _pick_node_selected(NodePath p_path); - - void _pick_new_property(); - void _pick_node_property_selected(String p_name); - - bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; - void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); - - void _add_sync_property(String p_path); - -protected: - static void _bind_methods(); - - void _notification(int p_what); - -public: - void edit(MultiplayerSynchronizer *p_object); - MultiplayerSynchronizer *get_current() const { return current; } - - Button *get_pin() { return pin; } - ReplicationEditor(); - ~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 diff --git a/modules/multiplayer/multiplayer_debugger.cpp b/modules/multiplayer/multiplayer_debugger.cpp new file mode 100644 index 0000000000..3d22af04dc --- /dev/null +++ b/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> multiplayer_profilers; + +void MultiplayerDebugger::initialize() { + Ref bandwidth; + bandwidth.instantiate(); + bandwidth->bind("multiplayer"); + multiplayer_profilers.push_back(bandwidth); + + Ref 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 &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(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 &E : rpc_node_data) { + frame.infos.push_back(E.value); + } + rpc_node_data.clear(); + EngineDebugger::get_singleton()->send_message("multiplayer:rpc", frame.serialize()); + } +} diff --git a/modules/multiplayer/multiplayer_debugger.h b/modules/multiplayer/multiplayer_debugger.h new file mode 100644 index 0000000000..4efd1da016 --- /dev/null +++ b/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 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 bandwidth_in; + int bandwidth_out_ptr = 0; + Vector bandwidth_out; + uint64_t last_bandwidth_time = 0; + + int bandwidth_usage(const Vector &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 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 diff --git a/modules/multiplayer/register_types.cpp b/modules/multiplayer/register_types.cpp index a2c524da80..2bf1041029 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) { @@ -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(); + EditorPlugins::add_by_type(); } #endif } void uninitialize_multiplayer_module(ModuleInitializationLevel p_level) { + MultiplayerDebugger::deinitialize(); } -- cgit v1.2.3