| 1 | /**************************************************************************/ |
| 2 | /* replication_editor.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "replication_editor.h" |
| 32 | |
| 33 | #include "../multiplayer_synchronizer.h" |
| 34 | |
| 35 | #include "editor/editor_node.h" |
| 36 | #include "editor/editor_scale.h" |
| 37 | #include "editor/editor_settings.h" |
| 38 | #include "editor/editor_string_names.h" |
| 39 | #include "editor/editor_undo_redo_manager.h" |
| 40 | #include "editor/gui/scene_tree_editor.h" |
| 41 | #include "editor/inspector_dock.h" |
| 42 | #include "editor/property_selector.h" |
| 43 | #include "scene/gui/dialogs.h" |
| 44 | #include "scene/gui/separator.h" |
| 45 | #include "scene/gui/tree.h" |
| 46 | |
| 47 | void ReplicationEditor::_pick_node_filter_text_changed(const String &p_newtext) { |
| 48 | TreeItem *root_item = pick_node->get_scene_tree()->get_scene_tree()->get_root(); |
| 49 | |
| 50 | Vector<Node *> select_candidates; |
| 51 | Node *to_select = nullptr; |
| 52 | |
| 53 | String filter = pick_node->get_filter_line_edit()->get_text(); |
| 54 | |
| 55 | _pick_node_select_recursive(root_item, filter, select_candidates); |
| 56 | |
| 57 | if (!select_candidates.is_empty()) { |
| 58 | for (int i = 0; i < select_candidates.size(); ++i) { |
| 59 | Node *candidate = select_candidates[i]; |
| 60 | |
| 61 | if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) { |
| 62 | to_select = candidate; |
| 63 | break; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | if (!to_select) { |
| 68 | to_select = select_candidates[0]; |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | pick_node->get_scene_tree()->set_selected(to_select); |
| 73 | } |
| 74 | |
| 75 | void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates) { |
| 76 | if (!p_item) { |
| 77 | return; |
| 78 | } |
| 79 | |
| 80 | NodePath np = p_item->get_metadata(0); |
| 81 | Node *node = get_node(np); |
| 82 | |
| 83 | if (!p_filter.is_empty() && ((String)node->get_name()).findn(p_filter) != -1) { |
| 84 | p_select_candidates.push_back(node); |
| 85 | } |
| 86 | |
| 87 | TreeItem *c = p_item->get_first_child(); |
| 88 | |
| 89 | while (c) { |
| 90 | _pick_node_select_recursive(c, p_filter, p_select_candidates); |
| 91 | c = c->get_next(); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | void ReplicationEditor::_pick_node_filter_input(const Ref<InputEvent> &p_ie) { |
| 96 | Ref<InputEventKey> k = p_ie; |
| 97 | |
| 98 | if (k.is_valid()) { |
| 99 | switch (k->get_keycode()) { |
| 100 | case Key::UP: |
| 101 | case Key::DOWN: |
| 102 | case Key::PAGEUP: |
| 103 | case Key::PAGEDOWN: { |
| 104 | pick_node->get_scene_tree()->get_scene_tree()->gui_input(k); |
| 105 | pick_node->get_filter_line_edit()->accept_event(); |
| 106 | } break; |
| 107 | default: |
| 108 | break; |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | void ReplicationEditor::_pick_node_selected(NodePath p_path) { |
| 114 | Node *root = current->get_node(current->get_root_path()); |
| 115 | ERR_FAIL_COND(!root); |
| 116 | Node *node = get_node(p_path); |
| 117 | ERR_FAIL_COND(!node); |
| 118 | NodePath path_to = root->get_path_to(node); |
| 119 | adding_node_path = path_to; |
| 120 | prop_selector->select_property_from_instance(node); |
| 121 | } |
| 122 | |
| 123 | void ReplicationEditor::_pick_new_property() { |
| 124 | if (current == nullptr) { |
| 125 | EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it." )); |
| 126 | return; |
| 127 | } |
| 128 | Node *root = current->get_node(current->get_root_path()); |
| 129 | if (!root) { |
| 130 | EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root." )); |
| 131 | return; |
| 132 | } |
| 133 | pick_node->popup_scenetree_dialog(); |
| 134 | pick_node->get_filter_line_edit()->clear(); |
| 135 | pick_node->get_filter_line_edit()->grab_focus(); |
| 136 | } |
| 137 | |
| 138 | void ReplicationEditor::_add_sync_property(String p_path) { |
| 139 | config = current->get_replication_config(); |
| 140 | |
| 141 | if (config.is_valid() && config->has_property(p_path)) { |
| 142 | EditorNode::get_singleton()->show_warning(TTR("Property is already being synchronized." )); |
| 143 | return; |
| 144 | } |
| 145 | |
| 146 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
| 147 | undo_redo->create_action(TTR("Add property to synchronizer" )); |
| 148 | |
| 149 | if (config.is_null()) { |
| 150 | config.instantiate(); |
| 151 | current->set_replication_config(config); |
| 152 | undo_redo->add_do_method(current, "set_replication_config" , config); |
| 153 | undo_redo->add_undo_method(current, "set_replication_config" , Ref<SceneReplicationConfig>()); |
| 154 | _update_config(); |
| 155 | } |
| 156 | |
| 157 | undo_redo->add_do_method(config.ptr(), "add_property" , p_path); |
| 158 | undo_redo->add_undo_method(config.ptr(), "remove_property" , p_path); |
| 159 | undo_redo->add_do_method(this, "_update_config" ); |
| 160 | undo_redo->add_undo_method(this, "_update_config" ); |
| 161 | undo_redo->commit_action(); |
| 162 | } |
| 163 | |
| 164 | void ReplicationEditor::_pick_node_property_selected(String p_name) { |
| 165 | String adding_prop_path = String(adding_node_path) + ":" + p_name; |
| 166 | |
| 167 | _add_sync_property(adding_prop_path); |
| 168 | } |
| 169 | |
| 170 | /// ReplicationEditor |
| 171 | ReplicationEditor::ReplicationEditor() { |
| 172 | set_v_size_flags(SIZE_EXPAND_FILL); |
| 173 | set_custom_minimum_size(Size2(0, 200) * EDSCALE); |
| 174 | |
| 175 | delete_dialog = memnew(ConfirmationDialog); |
| 176 | delete_dialog->connect("canceled" , callable_mp(this, &ReplicationEditor::_dialog_closed).bind(false)); |
| 177 | delete_dialog->connect("confirmed" , callable_mp(this, &ReplicationEditor::_dialog_closed).bind(true)); |
| 178 | add_child(delete_dialog); |
| 179 | |
| 180 | VBoxContainer *vb = memnew(VBoxContainer); |
| 181 | vb->set_v_size_flags(SIZE_EXPAND_FILL); |
| 182 | add_child(vb); |
| 183 | |
| 184 | pick_node = memnew(SceneTreeDialog); |
| 185 | add_child(pick_node); |
| 186 | pick_node->register_text_enter(pick_node->get_filter_line_edit()); |
| 187 | pick_node->set_title(TTR("Pick a node to synchronize:" )); |
| 188 | pick_node->connect("selected" , callable_mp(this, &ReplicationEditor::_pick_node_selected)); |
| 189 | pick_node->get_filter_line_edit()->connect("text_changed" , callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed)); |
| 190 | pick_node->get_filter_line_edit()->connect("gui_input" , callable_mp(this, &ReplicationEditor::_pick_node_filter_input)); |
| 191 | |
| 192 | prop_selector = memnew(PropertySelector); |
| 193 | add_child(prop_selector); |
| 194 | // Filter out properties that cannot be synchronized. |
| 195 | // * RIDs do not match across network. |
| 196 | // * Objects are too large for replication. |
| 197 | Vector<Variant::Type> types = { |
| 198 | Variant::BOOL, |
| 199 | Variant::INT, |
| 200 | Variant::FLOAT, |
| 201 | Variant::STRING, |
| 202 | |
| 203 | Variant::VECTOR2, |
| 204 | Variant::VECTOR2I, |
| 205 | Variant::RECT2, |
| 206 | Variant::RECT2I, |
| 207 | Variant::VECTOR3, |
| 208 | Variant::VECTOR3I, |
| 209 | Variant::TRANSFORM2D, |
| 210 | Variant::VECTOR4, |
| 211 | Variant::VECTOR4I, |
| 212 | Variant::PLANE, |
| 213 | Variant::QUATERNION, |
| 214 | Variant::AABB, |
| 215 | Variant::BASIS, |
| 216 | Variant::TRANSFORM3D, |
| 217 | Variant::PROJECTION, |
| 218 | |
| 219 | Variant::COLOR, |
| 220 | Variant::STRING_NAME, |
| 221 | Variant::NODE_PATH, |
| 222 | // Variant::RID, |
| 223 | // Variant::OBJECT, |
| 224 | Variant::SIGNAL, |
| 225 | Variant::DICTIONARY, |
| 226 | Variant::ARRAY, |
| 227 | |
| 228 | Variant::PACKED_BYTE_ARRAY, |
| 229 | Variant::PACKED_INT32_ARRAY, |
| 230 | Variant::PACKED_INT64_ARRAY, |
| 231 | Variant::PACKED_FLOAT32_ARRAY, |
| 232 | Variant::PACKED_FLOAT64_ARRAY, |
| 233 | Variant::PACKED_STRING_ARRAY, |
| 234 | Variant::PACKED_VECTOR2_ARRAY, |
| 235 | Variant::PACKED_VECTOR3_ARRAY, |
| 236 | Variant::PACKED_COLOR_ARRAY |
| 237 | }; |
| 238 | prop_selector->set_type_filter(types); |
| 239 | prop_selector->connect("selected" , callable_mp(this, &ReplicationEditor::_pick_node_property_selected)); |
| 240 | |
| 241 | HBoxContainer *hb = memnew(HBoxContainer); |
| 242 | vb->add_child(hb); |
| 243 | |
| 244 | add_pick_button = memnew(Button); |
| 245 | add_pick_button->connect("pressed" , callable_mp(this, &ReplicationEditor::_pick_new_property)); |
| 246 | add_pick_button->set_text(TTR("Add property to sync..." )); |
| 247 | hb->add_child(add_pick_button); |
| 248 | VSeparator *vs = memnew(VSeparator); |
| 249 | vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); |
| 250 | hb->add_child(vs); |
| 251 | hb->add_child(memnew(Label(TTR("Path:" )))); |
| 252 | np_line_edit = memnew(LineEdit); |
| 253 | np_line_edit->set_placeholder(":property" ); |
| 254 | np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); |
| 255 | np_line_edit->connect("text_submitted" , callable_mp(this, &ReplicationEditor::_np_text_submitted)); |
| 256 | hb->add_child(np_line_edit); |
| 257 | add_from_path_button = memnew(Button); |
| 258 | add_from_path_button->connect("pressed" , callable_mp(this, &ReplicationEditor::_add_pressed)); |
| 259 | add_from_path_button->set_text(TTR("Add from path" )); |
| 260 | hb->add_child(add_from_path_button); |
| 261 | vs = memnew(VSeparator); |
| 262 | vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); |
| 263 | hb->add_child(vs); |
| 264 | pin = memnew(Button); |
| 265 | pin->set_flat(true); |
| 266 | pin->set_toggle_mode(true); |
| 267 | hb->add_child(pin); |
| 268 | |
| 269 | tree = memnew(Tree); |
| 270 | tree->set_hide_root(true); |
| 271 | tree->set_columns(4); |
| 272 | tree->set_column_titles_visible(true); |
| 273 | tree->set_column_title(0, TTR("Properties" )); |
| 274 | tree->set_column_expand(0, true); |
| 275 | tree->set_column_title(1, TTR("Spawn" )); |
| 276 | tree->set_column_expand(1, false); |
| 277 | tree->set_column_custom_minimum_width(1, 100); |
| 278 | tree->set_column_title(2, TTR("Replicate" )); |
| 279 | tree->set_column_custom_minimum_width(2, 100); |
| 280 | tree->set_column_expand(2, false); |
| 281 | tree->set_column_expand(3, false); |
| 282 | tree->create_item(); |
| 283 | tree->connect("button_clicked" , callable_mp(this, &ReplicationEditor::_tree_button_pressed)); |
| 284 | tree->connect("item_edited" , callable_mp(this, &ReplicationEditor::_tree_item_edited)); |
| 285 | tree->set_v_size_flags(SIZE_EXPAND_FILL); |
| 286 | vb->add_child(tree); |
| 287 | |
| 288 | drop_label = memnew(Label); |
| 289 | drop_label->set_text(TTR("Add properties using the options above, or\ndrag them them from the inspector and drop them here." )); |
| 290 | drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); |
| 291 | drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); |
| 292 | tree->add_child(drop_label); |
| 293 | drop_label->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); |
| 294 | |
| 295 | SET_DRAG_FORWARDING_CDU(tree, ReplicationEditor); |
| 296 | } |
| 297 | |
| 298 | void ReplicationEditor::_bind_methods() { |
| 299 | ClassDB::bind_method(D_METHOD("_update_config" ), &ReplicationEditor::_update_config); |
| 300 | ClassDB::bind_method(D_METHOD("_update_value" , "property" , "column" , "value" ), &ReplicationEditor::_update_value); |
| 301 | } |
| 302 | |
| 303 | bool ReplicationEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { |
| 304 | Dictionary d = p_data; |
| 305 | if (!d.has("type" )) { |
| 306 | return false; |
| 307 | } |
| 308 | String t = d["type" ]; |
| 309 | if (t != "obj_property" ) { |
| 310 | return false; |
| 311 | } |
| 312 | Object *obj = d["object" ]; |
| 313 | if (!obj) { |
| 314 | return false; |
| 315 | } |
| 316 | Node *node = Object::cast_to<Node>(obj); |
| 317 | if (!node) { |
| 318 | return false; |
| 319 | } |
| 320 | |
| 321 | return true; |
| 322 | } |
| 323 | |
| 324 | void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { |
| 325 | if (current == nullptr) { |
| 326 | EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it." )); |
| 327 | return; |
| 328 | } |
| 329 | Node *root = current->get_node(current->get_root_path()); |
| 330 | if (!root) { |
| 331 | EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root." )); |
| 332 | return; |
| 333 | } |
| 334 | |
| 335 | Dictionary d = p_data; |
| 336 | if (!d.has("type" )) { |
| 337 | return; |
| 338 | } |
| 339 | String t = d["type" ]; |
| 340 | if (t != "obj_property" ) { |
| 341 | return; |
| 342 | } |
| 343 | Object *obj = d["object" ]; |
| 344 | if (!obj) { |
| 345 | return; |
| 346 | } |
| 347 | Node *node = Object::cast_to<Node>(obj); |
| 348 | if (!node) { |
| 349 | return; |
| 350 | } |
| 351 | |
| 352 | String path = root->get_path_to(node); |
| 353 | path += ":" + String(d["property" ]); |
| 354 | |
| 355 | _add_sync_property(path); |
| 356 | } |
| 357 | |
| 358 | void ReplicationEditor::_notification(int p_what) { |
| 359 | switch (p_what) { |
| 360 | case NOTIFICATION_ENTER_TREE: |
| 361 | case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { |
| 362 | add_theme_style_override("panel" , EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("panel" ), SNAME("Panel" ))); |
| 363 | add_pick_button->set_icon(get_theme_icon(SNAME("Add" ), EditorStringName(EditorIcons))); |
| 364 | pin->set_icon(get_theme_icon(SNAME("Pin" ), EditorStringName(EditorIcons))); |
| 365 | } break; |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | void ReplicationEditor::_add_pressed() { |
| 370 | if (!current) { |
| 371 | EditorNode::get_singleton()->show_warning(TTR("Please select a MultiplayerSynchronizer first." )); |
| 372 | return; |
| 373 | } |
| 374 | if (current->get_root_path().is_empty()) { |
| 375 | EditorNode::get_singleton()->show_warning(TTR("The MultiplayerSynchronizer needs a root path." )); |
| 376 | return; |
| 377 | } |
| 378 | String np_text = np_line_edit->get_text(); |
| 379 | |
| 380 | if (np_text.is_empty()) { |
| 381 | EditorNode::get_singleton()->show_warning(TTR("Property/path must not be empty." )); |
| 382 | return; |
| 383 | } |
| 384 | |
| 385 | int idx = np_text.find(":" ); |
| 386 | if (idx == -1) { |
| 387 | np_text = ".:" + np_text; |
| 388 | } else if (idx == 0) { |
| 389 | np_text = "." + np_text; |
| 390 | } |
| 391 | NodePath path = NodePath(np_text); |
| 392 | if (path.is_empty()) { |
| 393 | EditorNode::get_singleton()->show_warning(vformat(TTR("Invalid property path: '%s'" ), np_text)); |
| 394 | return; |
| 395 | } |
| 396 | |
| 397 | _add_sync_property(path); |
| 398 | } |
| 399 | |
| 400 | void ReplicationEditor::_np_text_submitted(const String &p_newtext) { |
| 401 | _add_pressed(); |
| 402 | } |
| 403 | |
| 404 | void ReplicationEditor::_tree_item_edited() { |
| 405 | TreeItem *ti = tree->get_edited(); |
| 406 | if (!ti || config.is_null()) { |
| 407 | return; |
| 408 | } |
| 409 | int column = tree->get_edited_column(); |
| 410 | ERR_FAIL_COND(column < 1 || column > 2); |
| 411 | const NodePath prop = ti->get_metadata(0); |
| 412 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
| 413 | |
| 414 | if (column == 1) { |
| 415 | undo_redo->create_action(TTR("Set spawn property" )); |
| 416 | bool value = ti->is_checked(column); |
| 417 | undo_redo->add_do_method(config.ptr(), "property_set_spawn" , prop, value); |
| 418 | undo_redo->add_undo_method(config.ptr(), "property_set_spawn" , prop, !value); |
| 419 | undo_redo->add_do_method(this, "_update_value" , prop, column, value ? 1 : 0); |
| 420 | undo_redo->add_undo_method(this, "_update_value" , prop, column, value ? 1 : 0); |
| 421 | undo_redo->commit_action(); |
| 422 | } else if (column == 2) { |
| 423 | undo_redo->create_action(TTR("Set sync property" )); |
| 424 | int value = ti->get_range(column); |
| 425 | int old_value = config->property_get_replication_mode(prop); |
| 426 | // We have a hard limit of 64 watchable properties per synchronizer. |
| 427 | if (value == SceneReplicationConfig::REPLICATION_MODE_ON_CHANGE && config->get_watch_properties().size() >= 64) { |
| 428 | EditorNode::get_singleton()->show_warning(TTR("Each MultiplayerSynchronizer can have no more than 64 watched properties." )); |
| 429 | ti->set_range(column, old_value); |
| 430 | return; |
| 431 | } |
| 432 | undo_redo->add_do_method(config.ptr(), "property_set_replication_mode" , prop, value); |
| 433 | undo_redo->add_undo_method(config.ptr(), "property_set_replication_mode" , prop, old_value); |
| 434 | undo_redo->add_do_method(this, "_update_value" , prop, column, value); |
| 435 | undo_redo->add_undo_method(this, "_update_value" , prop, column, old_value); |
| 436 | undo_redo->commit_action(); |
| 437 | } else { |
| 438 | ERR_FAIL(); |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { |
| 443 | if (p_button != MouseButton::LEFT) { |
| 444 | return; |
| 445 | } |
| 446 | |
| 447 | TreeItem *ti = Object::cast_to<TreeItem>(p_item); |
| 448 | if (!ti) { |
| 449 | return; |
| 450 | } |
| 451 | deleting = ti->get_metadata(0); |
| 452 | delete_dialog->set_text(TTR("Delete Property?" ) + "\n\"" + ti->get_text(0) + "\"" ); |
| 453 | delete_dialog->popup_centered(); |
| 454 | } |
| 455 | |
| 456 | void ReplicationEditor::_dialog_closed(bool p_confirmed) { |
| 457 | if (deleting.is_empty() || config.is_null()) { |
| 458 | return; |
| 459 | } |
| 460 | if (p_confirmed) { |
| 461 | const NodePath prop = deleting; |
| 462 | int idx = config->property_get_index(prop); |
| 463 | bool spawn = config->property_get_spawn(prop); |
| 464 | SceneReplicationConfig::ReplicationMode mode = config->property_get_replication_mode(prop); |
| 465 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
| 466 | undo_redo->create_action(TTR("Remove Property" )); |
| 467 | undo_redo->add_do_method(config.ptr(), "remove_property" , prop); |
| 468 | undo_redo->add_undo_method(config.ptr(), "add_property" , prop, idx); |
| 469 | undo_redo->add_undo_method(config.ptr(), "property_set_spawn" , prop, spawn); |
| 470 | undo_redo->add_undo_method(config.ptr(), "property_set_replication_mode" , prop, mode); |
| 471 | undo_redo->add_do_method(this, "_update_config" ); |
| 472 | undo_redo->add_undo_method(this, "_update_config" ); |
| 473 | undo_redo->commit_action(); |
| 474 | } |
| 475 | deleting = NodePath(); |
| 476 | } |
| 477 | |
| 478 | void ReplicationEditor::_update_value(const NodePath &p_prop, int p_column, int p_value) { |
| 479 | if (!tree->get_root()) { |
| 480 | return; |
| 481 | } |
| 482 | TreeItem *ti = tree->get_root()->get_first_child(); |
| 483 | while (ti) { |
| 484 | if (ti->get_metadata(0).operator NodePath() == p_prop) { |
| 485 | if (p_column == 1) { |
| 486 | ti->set_checked(p_column, p_value != 0); |
| 487 | } else if (p_column == 2) { |
| 488 | ti->set_range(p_column, p_value); |
| 489 | } |
| 490 | return; |
| 491 | } |
| 492 | ti = ti->get_next(); |
| 493 | } |
| 494 | } |
| 495 | |
| 496 | void ReplicationEditor::_update_config() { |
| 497 | deleting = NodePath(); |
| 498 | tree->clear(); |
| 499 | tree->create_item(); |
| 500 | drop_label->set_visible(true); |
| 501 | if (!config.is_valid()) { |
| 502 | return; |
| 503 | } |
| 504 | TypedArray<NodePath> props = config->get_properties(); |
| 505 | if (props.size()) { |
| 506 | drop_label->set_visible(false); |
| 507 | } |
| 508 | for (int i = 0; i < props.size(); i++) { |
| 509 | const NodePath path = props[i]; |
| 510 | _add_property(path, config->property_get_spawn(path), config->property_get_replication_mode(path)); |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) { |
| 515 | if (current == p_sync) { |
| 516 | return; |
| 517 | } |
| 518 | current = p_sync; |
| 519 | if (current) { |
| 520 | config = current->get_replication_config(); |
| 521 | } else { |
| 522 | config.unref(); |
| 523 | } |
| 524 | _update_config(); |
| 525 | } |
| 526 | |
| 527 | Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) { |
| 528 | if (!p_node || !has_theme_icon(p_node->get_class(), EditorStringName(EditorIcons))) { |
| 529 | return get_theme_icon(SNAME("ImportFail" ), EditorStringName(EditorIcons)); |
| 530 | } |
| 531 | return get_theme_icon(p_node->get_class(), EditorStringName(EditorIcons)); |
| 532 | } |
| 533 | |
| 534 | static bool can_sync(const Variant &p_var) { |
| 535 | switch (p_var.get_type()) { |
| 536 | case Variant::RID: |
| 537 | case Variant::OBJECT: |
| 538 | return false; |
| 539 | case Variant::ARRAY: { |
| 540 | const Array &arr = p_var; |
| 541 | if (arr.is_typed()) { |
| 542 | const uint32_t type = arr.get_typed_builtin(); |
| 543 | return (type != Variant::RID) && (type != Variant::OBJECT); |
| 544 | } |
| 545 | return true; |
| 546 | } |
| 547 | default: |
| 548 | return true; |
| 549 | } |
| 550 | } |
| 551 | |
| 552 | void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, SceneReplicationConfig::ReplicationMode p_mode) { |
| 553 | String prop = String(p_property); |
| 554 | TreeItem *item = tree->create_item(); |
| 555 | item->set_selectable(0, false); |
| 556 | item->set_selectable(1, false); |
| 557 | item->set_selectable(2, false); |
| 558 | item->set_selectable(3, false); |
| 559 | item->set_text(0, prop); |
| 560 | item->set_metadata(0, prop); |
| 561 | Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr; |
| 562 | Ref<Texture2D> icon = _get_class_icon(root_node); |
| 563 | if (root_node) { |
| 564 | String path = prop.substr(0, prop.find(":" )); |
| 565 | String subpath = prop.substr(path.size()); |
| 566 | Node *node = root_node->get_node_or_null(path); |
| 567 | if (!node) { |
| 568 | node = root_node; |
| 569 | } |
| 570 | item->set_text(0, String(node->get_name()) + ":" + subpath); |
| 571 | icon = _get_class_icon(node); |
| 572 | bool valid = false; |
| 573 | Variant value = node->get(subpath, &valid); |
| 574 | if (valid && !can_sync(value)) { |
| 575 | item->set_icon(0, get_theme_icon(SNAME("StatusWarning" ), EditorStringName(EditorIcons))); |
| 576 | item->set_tooltip_text(0, TTR("Property of this type not supported." )); |
| 577 | } else { |
| 578 | item->set_icon(0, icon); |
| 579 | } |
| 580 | } else { |
| 581 | item->set_icon(0, icon); |
| 582 | } |
| 583 | item->add_button(3, get_theme_icon(SNAME("Remove" ), EditorStringName(EditorIcons))); |
| 584 | item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER); |
| 585 | item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); |
| 586 | item->set_checked(1, p_spawn); |
| 587 | item->set_editable(1, true); |
| 588 | item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER); |
| 589 | item->set_cell_mode(2, TreeItem::CELL_MODE_RANGE); |
| 590 | item->set_range_config(2, 0, 2, 1); |
| 591 | item->set_text(2, "Never,Always,On Change" ); |
| 592 | item->set_range(2, (int)p_mode); |
| 593 | item->set_editable(2, true); |
| 594 | } |
| 595 | |