initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
432
modules/multiplayer/editor/editor_network_profiler.cpp
Normal file
432
modules/multiplayer/editor/editor_network_profiler.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
/**************************************************************************/
|
||||
/* editor_network_profiler.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "editor_network_profiler.h"
|
||||
|
||||
#include "editor/editor_string_names.h"
|
||||
#include "editor/run/editor_run_bar.h"
|
||||
#include "editor/settings/editor_settings.h"
|
||||
#include "editor/themes/editor_scale.h"
|
||||
#include "scene/gui/check_box.h"
|
||||
#include "scene/gui/flow_container.h"
|
||||
#include "scene/gui/line_edit.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_THEME_CHANGED: {
|
||||
if (activate->is_pressed()) {
|
||||
activate->set_button_icon(theme_cache.stop_icon);
|
||||
} else {
|
||||
activate->set_button_icon(theme_cache.play_icon);
|
||||
}
|
||||
clear_button->set_button_icon(theme_cache.clear_icon);
|
||||
|
||||
incoming_bandwidth_text->set_right_icon(theme_cache.incoming_bandwidth_icon);
|
||||
outgoing_bandwidth_text->set_right_icon(theme_cache.outgoing_bandwidth_icon);
|
||||
|
||||
// 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", theme_cache.incoming_bandwidth_color * Color(1, 1, 1, 0.5));
|
||||
outgoing_bandwidth_text->add_theme_color_override("font_uneditable_color", theme_cache.outgoing_bandwidth_color * Color(1, 1, 1, 0.5));
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::_update_theme_item_cache() {
|
||||
VBoxContainer::_update_theme_item_cache();
|
||||
|
||||
theme_cache.node_icon = get_theme_icon(SNAME("Node"), EditorStringName(EditorIcons));
|
||||
theme_cache.stop_icon = get_theme_icon(SNAME("Stop"), EditorStringName(EditorIcons));
|
||||
theme_cache.play_icon = get_theme_icon(SNAME("Play"), EditorStringName(EditorIcons));
|
||||
theme_cache.clear_icon = get_theme_icon(SNAME("Clear"), EditorStringName(EditorIcons));
|
||||
|
||||
theme_cache.multiplayer_synchronizer_icon = get_theme_icon("MultiplayerSynchronizer", EditorStringName(EditorIcons));
|
||||
theme_cache.instance_options_icon = get_theme_icon(SNAME("InstanceOptions"), EditorStringName(EditorIcons));
|
||||
|
||||
theme_cache.incoming_bandwidth_icon = get_theme_icon(SNAME("ArrowDown"), EditorStringName(EditorIcons));
|
||||
theme_cache.outgoing_bandwidth_icon = get_theme_icon(SNAME("ArrowUp"), EditorStringName(EditorIcons));
|
||||
|
||||
theme_cache.incoming_bandwidth_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
|
||||
theme_cache.outgoing_bandwidth_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
|
||||
}
|
||||
|
||||
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, EditorStringName(EditorIcons)) ? get_theme_icon(root_info.type, EditorStringName(EditorIcons)) : theme_cache.node_icon);
|
||||
node->set_tooltip_text(0, root_info.path);
|
||||
|
||||
node->set_text(1, sync_info.path.get_file());
|
||||
node->set_icon(1, theme_cache.multiplayer_synchronizer_icon);
|
||||
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, theme_cache.instance_options_icon);
|
||||
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() {
|
||||
_update_button_text();
|
||||
|
||||
if (activate->is_pressed()) {
|
||||
refresh_timer->start();
|
||||
} else {
|
||||
refresh_timer->stop();
|
||||
}
|
||||
|
||||
emit_signal(SNAME("enable_profiling"), activate->is_pressed());
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::_update_button_text() {
|
||||
if (activate->is_pressed()) {
|
||||
activate->set_button_icon(theme_cache.stop_icon);
|
||||
activate->set_text(TTR("Stop"));
|
||||
} else {
|
||||
activate->set_button_icon(theme_cache.play_icon);
|
||||
activate->set_text(TTR("Start"));
|
||||
}
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::started() {
|
||||
_clear_pressed();
|
||||
activate->set_disabled(false);
|
||||
|
||||
if (EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_network_profiler", false)) {
|
||||
set_profiling(true);
|
||||
refresh_timer->start();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::stopped() {
|
||||
activate->set_disabled(true);
|
||||
set_profiling(false);
|
||||
refresh_timer->stop();
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::set_profiling(bool p_pressed) {
|
||||
activate->set_pressed(p_pressed);
|
||||
_update_button_text();
|
||||
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();
|
||||
clear_button->set_disabled(true);
|
||||
}
|
||||
|
||||
void EditorNetworkProfiler::_autostart_toggled(bool p_toggled_on) {
|
||||
EditorSettings::get_singleton()->set_project_metadata("debug_options", "autostart_network_profiler", p_toggled_on);
|
||||
EditorRunBar::get_singleton()->update_profiler_autostart_indicator();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (clear_button->is_disabled()) {
|
||||
clear_button->set_disabled(false);
|
||||
}
|
||||
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) {
|
||||
if (clear_button->is_disabled()) {
|
||||
clear_button->set_disabled(false);
|
||||
}
|
||||
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 (p_frame.incoming_syncs) {
|
||||
info.incoming_size = p_frame.incoming_size / p_frame.incoming_syncs;
|
||||
}
|
||||
if (p_frame.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",
|
||||
theme_cache.incoming_bandwidth_color * Color(1, 1, 1, p_incoming > 0 ? 1 : 0.5));
|
||||
outgoing_bandwidth_text->add_theme_color_override(
|
||||
"font_uneditable_color",
|
||||
theme_cache.outgoing_bandwidth_color * Color(1, 1, 1, p_outgoing > 0 ? 1 : 0.5));
|
||||
}
|
||||
|
||||
bool EditorNetworkProfiler::is_profiling() {
|
||||
return activate->is_pressed();
|
||||
}
|
||||
|
||||
EditorNetworkProfiler::EditorNetworkProfiler() {
|
||||
FlowContainer *container = memnew(FlowContainer);
|
||||
container->add_theme_constant_override(SNAME("h_separation"), 8 * EDSCALE);
|
||||
container->add_theme_constant_override(SNAME("v_separation"), 2 * EDSCALE);
|
||||
add_child(container);
|
||||
|
||||
activate = memnew(Button);
|
||||
activate->set_toggle_mode(true);
|
||||
activate->set_text(TTR("Start"));
|
||||
activate->set_disabled(true);
|
||||
activate->connect(SceneStringName(pressed), callable_mp(this, &EditorNetworkProfiler::_activate_pressed));
|
||||
container->add_child(activate);
|
||||
|
||||
clear_button = memnew(Button);
|
||||
clear_button->set_text(TTR("Clear"));
|
||||
clear_button->set_disabled(true);
|
||||
clear_button->connect(SceneStringName(pressed), callable_mp(this, &EditorNetworkProfiler::_clear_pressed));
|
||||
container->add_child(clear_button);
|
||||
|
||||
CheckBox *autostart_checkbox = memnew(CheckBox);
|
||||
autostart_checkbox->set_text(TTR("Autostart"));
|
||||
autostart_checkbox->set_pressed(EditorSettings::get_singleton()->get_project_metadata("debug_options", "autostart_network_profiler", false));
|
||||
autostart_checkbox->connect(SceneStringName(toggled), callable_mp(this, &EditorNetworkProfiler::_autostart_toggled));
|
||||
container->add_child(autostart_checkbox);
|
||||
|
||||
Control *c = memnew(Control);
|
||||
c->set_h_size_flags(SIZE_EXPAND_FILL);
|
||||
container->add_child(c);
|
||||
|
||||
HBoxContainer *hb = memnew(HBoxContainer);
|
||||
hb->add_theme_constant_override(SNAME("separation"), 8 * EDSCALE);
|
||||
container->add_child(hb);
|
||||
|
||||
Label *lb = memnew(Label);
|
||||
// TRANSLATORS: This is the label for the network profiler's incoming bandwidth.
|
||||
lb->set_focus_mode(FOCUS_ACCESSIBILITY);
|
||||
lb->set_text(TTR("Down", "Network"));
|
||||
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);
|
||||
incoming_bandwidth_text->set_accessibility_name(TTRC("Incoming Bandwidth"));
|
||||
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);
|
||||
// TRANSLATORS: This is the label for the network profiler's outgoing bandwidth.
|
||||
lb->set_focus_mode(FOCUS_ACCESSIBILITY);
|
||||
lb->set_text(TTR("Up", "Network"));
|
||||
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);
|
||||
outgoing_bandwidth_text->set_accessibility_name(TTRC("Outgoing Bandwidth"));
|
||||
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(280, 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(280, 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);
|
||||
}
|
121
modules/multiplayer/editor/editor_network_profiler.h
Normal file
121
modules/multiplayer/editor/editor_network_profiler.h
Normal file
@@ -0,0 +1,121 @@
|
||||
/**************************************************************************/
|
||||
/* editor_network_profiler.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../multiplayer_debugger.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"
|
||||
|
||||
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;
|
||||
|
||||
struct ThemeCache {
|
||||
Ref<Texture2D> node_icon;
|
||||
Ref<Texture2D> stop_icon;
|
||||
Ref<Texture2D> play_icon;
|
||||
Ref<Texture2D> clear_icon;
|
||||
|
||||
Ref<Texture2D> multiplayer_synchronizer_icon;
|
||||
Ref<Texture2D> instance_options_icon;
|
||||
|
||||
Ref<Texture2D> incoming_bandwidth_icon;
|
||||
Ref<Texture2D> outgoing_bandwidth_icon;
|
||||
|
||||
Color incoming_bandwidth_color;
|
||||
Color outgoing_bandwidth_color;
|
||||
} theme_cache;
|
||||
|
||||
void _activate_pressed();
|
||||
void _clear_pressed();
|
||||
void _autostart_toggled(bool p_toggled_on);
|
||||
void _refresh();
|
||||
void _update_button_text();
|
||||
void _replication_button_clicked(TreeItem *p_item, int p_column, int p_idx, MouseButton p_button);
|
||||
|
||||
protected:
|
||||
virtual void _update_theme_item_cache() override;
|
||||
|
||||
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();
|
||||
|
||||
void set_profiling(bool p_pressed);
|
||||
void started();
|
||||
void stopped();
|
||||
|
||||
EditorNetworkProfiler();
|
||||
};
|
180
modules/multiplayer/editor/multiplayer_editor_plugin.cpp
Normal file
180
modules/multiplayer/editor/multiplayer_editor_plugin.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
/**************************************************************************/
|
||||
/* multiplayer_editor_plugin.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "multiplayer_editor_plugin.h"
|
||||
|
||||
#include "../multiplayer_synchronizer.h"
|
||||
#include "editor_network_profiler.h"
|
||||
#include "replication_editor.h"
|
||||
|
||||
#include "editor/editor_interface.h"
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/gui/editor_bottom_panel.h"
|
||||
#include "editor/settings/editor_command_palette.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->connect("started", callable_mp(profiler, &EditorNetworkProfiler::started));
|
||||
session->connect("stopped", callable_mp(profiler, &EditorNetworkProfiler::stopped));
|
||||
session->add_session_tab(profiler);
|
||||
profilers[p_session_id] = profiler;
|
||||
}
|
||||
|
||||
/// MultiplayerEditorPlugin
|
||||
|
||||
MultiplayerEditorPlugin::MultiplayerEditorPlugin() {
|
||||
repl_editor = memnew(ReplicationEditor);
|
||||
button = EditorNode::get_bottom_panel()->add_item(TTRC("Replication"), repl_editor, ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_replication_bottom_panel", TTRC("Toggle Replication Bottom Panel")));
|
||||
button->hide();
|
||||
repl_editor->get_pin()->connect(SceneStringName(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) {
|
||||
EditorInterface::get_singleton()->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_bottom_panel()->hide_bottom_panel();
|
||||
}
|
||||
button->hide();
|
||||
repl_editor->get_pin()->set_pressed(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MultiplayerEditorPlugin::_pinned() {
|
||||
if (!repl_editor->get_pin()->is_pressed() && repl_editor->get_current() == nullptr) {
|
||||
if (repl_editor->is_visible_in_tree()) {
|
||||
EditorNode::get_bottom_panel()->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_bottom_panel()->make_item_visible(repl_editor);
|
||||
} else if (!repl_editor->get_pin()->is_pressed()) {
|
||||
if (repl_editor->is_visible_in_tree()) {
|
||||
EditorNode::get_bottom_panel()->hide_bottom_panel();
|
||||
}
|
||||
button->hide();
|
||||
}
|
||||
}
|
79
modules/multiplayer/editor/multiplayer_editor_plugin.h
Normal file
79
modules/multiplayer/editor/multiplayer_editor_plugin.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/**************************************************************************/
|
||||
/* multiplayer_editor_plugin.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "editor/debugger/editor_debugger_plugin.h"
|
||||
#include "editor/plugins/editor_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;
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
589
modules/multiplayer/editor/replication_editor.cpp
Normal file
589
modules/multiplayer/editor/replication_editor.cpp
Normal file
@@ -0,0 +1,589 @@
|
||||
/**************************************************************************/
|
||||
/* replication_editor.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "replication_editor.h"
|
||||
|
||||
#include "../multiplayer_synchronizer.h"
|
||||
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/editor_string_names.h"
|
||||
#include "editor/editor_undo_redo_manager.h"
|
||||
#include "editor/inspector/property_selector.h"
|
||||
#include "editor/scene/scene_tree_editor.h"
|
||||
#include "editor/settings/editor_settings.h"
|
||||
#include "editor/themes/editor_scale.h"
|
||||
#include "editor/themes/editor_theme_manager.h"
|
||||
#include "scene/gui/dialogs.h"
|
||||
#include "scene/gui/line_edit.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<Node *> 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<Node *> &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()).containsn(p_filter)) {
|
||||
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_selected(NodePath p_path) {
|
||||
Node *root = current->get_node(current->get_root_path());
|
||||
ERR_FAIL_NULL(root);
|
||||
Node *node = get_node(p_path);
|
||||
ERR_FAIL_NULL(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(nullptr, current);
|
||||
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;
|
||||
}
|
||||
|
||||
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
|
||||
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<SceneReplicationConfig>());
|
||||
_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("canceled", callable_mp(this, &ReplicationEditor::_dialog_closed).bind(false));
|
||||
delete_dialog->connect(SceneStringName(confirmed), callable_mp(this, &ReplicationEditor::_dialog_closed).bind(true));
|
||||
add_child(delete_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->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(SceneStringName(text_changed), callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed));
|
||||
|
||||
prop_selector = memnew(PropertySelector);
|
||||
add_child(prop_selector);
|
||||
// Filter out properties that cannot be synchronized.
|
||||
// * RIDs do not match across network.
|
||||
// * Objects are too large for replication.
|
||||
Vector<Variant::Type> types = {
|
||||
Variant::BOOL,
|
||||
Variant::INT,
|
||||
Variant::FLOAT,
|
||||
Variant::STRING,
|
||||
|
||||
Variant::VECTOR2,
|
||||
Variant::VECTOR2I,
|
||||
Variant::RECT2,
|
||||
Variant::RECT2I,
|
||||
Variant::VECTOR3,
|
||||
Variant::VECTOR3I,
|
||||
Variant::TRANSFORM2D,
|
||||
Variant::VECTOR4,
|
||||
Variant::VECTOR4I,
|
||||
Variant::PLANE,
|
||||
Variant::QUATERNION,
|
||||
Variant::AABB,
|
||||
Variant::BASIS,
|
||||
Variant::TRANSFORM3D,
|
||||
Variant::PROJECTION,
|
||||
|
||||
Variant::COLOR,
|
||||
Variant::STRING_NAME,
|
||||
Variant::NODE_PATH,
|
||||
// Variant::RID,
|
||||
// Variant::OBJECT,
|
||||
Variant::SIGNAL,
|
||||
Variant::DICTIONARY,
|
||||
Variant::ARRAY,
|
||||
|
||||
Variant::PACKED_BYTE_ARRAY,
|
||||
Variant::PACKED_INT32_ARRAY,
|
||||
Variant::PACKED_INT64_ARRAY,
|
||||
Variant::PACKED_FLOAT32_ARRAY,
|
||||
Variant::PACKED_FLOAT64_ARRAY,
|
||||
Variant::PACKED_STRING_ARRAY,
|
||||
Variant::PACKED_VECTOR2_ARRAY,
|
||||
Variant::PACKED_VECTOR3_ARRAY,
|
||||
Variant::PACKED_COLOR_ARRAY,
|
||||
Variant::PACKED_VECTOR4_ARRAY,
|
||||
};
|
||||
prop_selector->set_type_filter(types);
|
||||
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(SceneStringName(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_accessibility_name(TTRC("Path:"));
|
||||
np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
|
||||
np_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &ReplicationEditor::_np_text_submitted));
|
||||
hb->add_child(np_line_edit);
|
||||
|
||||
add_from_path_button = memnew(Button);
|
||||
add_from_path_button->connect(SceneStringName(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_theme_type_variation(SceneStringName(FlatButton));
|
||||
pin->set_toggle_mode(true);
|
||||
pin->set_tooltip_text(TTR("Pin replication editor"));
|
||||
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("Replicate"));
|
||||
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_focus_mode(FOCUS_ACCESSIBILITY);
|
||||
drop_label->set_text(TTR("Add properties using the options above, or\ndrag 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);
|
||||
|
||||
SET_DRAG_FORWARDING_CDU(tree, ReplicationEditor);
|
||||
}
|
||||
|
||||
void ReplicationEditor::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("_update_config"), &ReplicationEditor::_update_config);
|
||||
ClassDB::bind_method(D_METHOD("_update_value", "property", "column", "value"), &ReplicationEditor::_update_value);
|
||||
}
|
||||
|
||||
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<Node>(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<Node>(obj);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
String path = String(root->get_path_to(node));
|
||||
path += ":" + String(d["property"]);
|
||||
|
||||
_add_sync_property(path);
|
||||
}
|
||||
|
||||
void ReplicationEditor::_notification(int p_what) {
|
||||
switch (p_what) {
|
||||
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
|
||||
if (!EditorThemeManager::is_generated_theme_outdated()) {
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case NOTIFICATION_ENTER_TREE: {
|
||||
add_theme_style_override(SceneStringName(panel), EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SceneStringName(panel), SNAME("Panel")));
|
||||
add_pick_button->set_button_icon(get_theme_icon(SNAME("Add"), EditorStringName(EditorIcons)));
|
||||
pin->set_button_icon(get_theme_icon(SNAME("Pin"), EditorStringName(EditorIcons)));
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void ReplicationEditor::_add_pressed() {
|
||||
if (!current) {
|
||||
EditorNode::get_singleton()->show_warning(TTR("Please select a MultiplayerSynchronizer first."));
|
||||
return;
|
||||
}
|
||||
if (current->get_root_path().is_empty()) {
|
||||
EditorNode::get_singleton()->show_warning(TTR("The MultiplayerSynchronizer needs a root path."));
|
||||
return;
|
||||
}
|
||||
String np_text = np_line_edit->get_text();
|
||||
|
||||
if (np_text.is_empty()) {
|
||||
EditorNode::get_singleton()->show_warning(TTR("Property/path must not be empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = np_text.find_char(':');
|
||||
if (idx == -1) {
|
||||
np_text = ".:" + np_text;
|
||||
} else if (idx == 0) {
|
||||
np_text = "." + np_text;
|
||||
}
|
||||
NodePath path = NodePath(np_text);
|
||||
if (path.is_empty()) {
|
||||
EditorNode::get_singleton()->show_warning(vformat(TTR("Invalid property path: '%s'"), np_text));
|
||||
return;
|
||||
}
|
||||
|
||||
_add_sync_property(String(path));
|
||||
}
|
||||
|
||||
void ReplicationEditor::_np_text_submitted(const String &p_newtext) {
|
||||
_add_pressed();
|
||||
}
|
||||
|
||||
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);
|
||||
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
|
||||
|
||||
if (column == 1) {
|
||||
undo_redo->create_action(TTR("Set spawn property"));
|
||||
bool value = ti->is_checked(column);
|
||||
undo_redo->add_do_method(config.ptr(), "property_set_spawn", prop, value);
|
||||
undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, !value);
|
||||
undo_redo->add_do_method(this, "_update_value", prop, column, value ? 1 : 0);
|
||||
undo_redo->add_undo_method(this, "_update_value", prop, column, value ? 0 : 1);
|
||||
undo_redo->commit_action();
|
||||
} else if (column == 2) {
|
||||
undo_redo->create_action(TTR("Set sync property"));
|
||||
int value = ti->get_range(column);
|
||||
int old_value = config->property_get_replication_mode(prop);
|
||||
// We have a hard limit of 64 watchable properties per synchronizer.
|
||||
if (value == SceneReplicationConfig::REPLICATION_MODE_ON_CHANGE && config->get_watch_properties().size() >= 64) {
|
||||
EditorNode::get_singleton()->show_warning(TTR("Each MultiplayerSynchronizer can have no more than 64 watched properties."));
|
||||
ti->set_range(column, old_value);
|
||||
return;
|
||||
}
|
||||
undo_redo->add_do_method(config.ptr(), "property_set_replication_mode", prop, value);
|
||||
undo_redo->add_undo_method(config.ptr(), "property_set_replication_mode", prop, old_value);
|
||||
undo_redo->add_do_method(this, "_update_value", prop, column, value);
|
||||
undo_redo->add_undo_method(this, "_update_value", prop, column, old_value);
|
||||
undo_redo->commit_action();
|
||||
} else {
|
||||
ERR_FAIL();
|
||||
}
|
||||
}
|
||||
|
||||
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<TreeItem>(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);
|
||||
SceneReplicationConfig::ReplicationMode mode = config->property_get_replication_mode(prop);
|
||||
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
|
||||
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_replication_mode", prop, mode);
|
||||
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_value(const NodePath &p_prop, int p_column, int p_value) {
|
||||
if (!tree->get_root()) {
|
||||
return;
|
||||
}
|
||||
TreeItem *ti = tree->get_root()->get_first_child();
|
||||
while (ti) {
|
||||
if (ti->get_metadata(0).operator NodePath() == p_prop) {
|
||||
if (p_column == 1) {
|
||||
ti->set_checked(p_column, p_value != 0);
|
||||
} else if (p_column == 2) {
|
||||
ti->set_range(p_column, p_value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ti = ti->get_next();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplicationEditor::_update_config() {
|
||||
deleting = NodePath();
|
||||
tree->clear();
|
||||
tree->create_item();
|
||||
drop_label->set_visible(true);
|
||||
if (config.is_null()) {
|
||||
return;
|
||||
}
|
||||
TypedArray<NodePath> 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_replication_mode(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<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) {
|
||||
if (!p_node || !has_theme_icon(p_node->get_class(), EditorStringName(EditorIcons))) {
|
||||
return get_theme_icon(SNAME("ImportFail"), EditorStringName(EditorIcons));
|
||||
}
|
||||
return get_theme_icon(p_node->get_class(), EditorStringName(EditorIcons));
|
||||
}
|
||||
|
||||
static bool can_sync(const Variant &p_var) {
|
||||
switch (p_var.get_type()) {
|
||||
case Variant::RID:
|
||||
case Variant::OBJECT:
|
||||
return false;
|
||||
case Variant::ARRAY: {
|
||||
const Array &arr = p_var;
|
||||
if (arr.is_typed()) {
|
||||
const uint32_t type = arr.get_typed_builtin();
|
||||
return (type != Variant::RID) && (type != Variant::OBJECT);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, SceneReplicationConfig::ReplicationMode p_mode) {
|
||||
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<Texture2D> icon = _get_class_icon(root_node);
|
||||
if (root_node) {
|
||||
String path = prop.substr(0, prop.find_char(':'));
|
||||
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);
|
||||
bool valid = false;
|
||||
Variant value = node->get(subpath, &valid);
|
||||
if (valid && !can_sync(value)) {
|
||||
item->set_icon(0, get_theme_icon(SNAME("StatusWarning"), EditorStringName(EditorIcons)));
|
||||
item->set_tooltip_text(0, TTR("Property of this type not supported."));
|
||||
} else {
|
||||
item->set_icon(0, icon);
|
||||
}
|
||||
} else {
|
||||
item->set_icon(0, icon);
|
||||
}
|
||||
item->add_button(3, get_theme_icon(SNAME("Remove"), EditorStringName(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_RANGE);
|
||||
item->set_range_config(2, 0, 2, 1);
|
||||
item->set_text(2, TTR("Never", "Replication Mode") + "," + TTR("Always", "Replication Mode") + "," + TTR("On Change", "Replication Mode"));
|
||||
item->set_range(2, (int)p_mode);
|
||||
item->set_editable(2, true);
|
||||
}
|
104
modules/multiplayer/editor/replication_editor.h
Normal file
104
modules/multiplayer/editor/replication_editor.h
Normal file
@@ -0,0 +1,104 @@
|
||||
/**************************************************************************/
|
||||
/* replication_editor.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../scene_replication_config.h"
|
||||
|
||||
#include "editor/plugins/editor_plugin.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;
|
||||
|
||||
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<SceneReplicationConfig> config;
|
||||
NodePath deleting;
|
||||
Tree *tree = nullptr;
|
||||
|
||||
PropertySelector *prop_selector = nullptr;
|
||||
SceneTreeDialog *pick_node = nullptr;
|
||||
NodePath adding_node_path;
|
||||
|
||||
Button *pin = nullptr;
|
||||
|
||||
Ref<Texture2D> _get_class_icon(const Node *p_node);
|
||||
|
||||
void _add_pressed();
|
||||
void _np_text_submitted(const String &p_newtext);
|
||||
void _tree_item_edited();
|
||||
void _tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
|
||||
void _update_value(const NodePath &p_prop, int p_column, int p_checked);
|
||||
void _update_config();
|
||||
void _dialog_closed(bool p_confirmed);
|
||||
void _add_property(const NodePath &p_property, bool p_spawn, SceneReplicationConfig::ReplicationMode p_mode);
|
||||
|
||||
void _pick_node_filter_text_changed(const String &p_newtext);
|
||||
void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates);
|
||||
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();
|
||||
};
|
Reference in New Issue
Block a user