diff options
Diffstat (limited to 'scene')
44 files changed, 1895 insertions, 126 deletions
diff --git a/scene/3d/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index 3957a1653f..b6b5920f69 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -506,7 +506,7 @@ int Skeleton3D::get_bone_axis_forward_enum(int p_bone) { // Skeleton creation api void Skeleton3D::add_bone(const String &p_name) { - ERR_FAIL_COND(p_name.is_empty() || p_name.find(":") != -1 || p_name.find("/") != -1); + ERR_FAIL_COND(p_name.is_empty() || p_name.contains(":") || p_name.contains("/")); for (int i = 0; i < bones.size(); i++) { ERR_FAIL_COND(bones[i].name == p_name); diff --git a/scene/SCsub b/scene/SCsub index 92288211bb..a7b23af598 100644 --- a/scene/SCsub +++ b/scene/SCsub @@ -9,6 +9,7 @@ env.add_source_files(env.scene_sources, "*.cpp") # Chain load SCsubs SConscript("main/SCsub") +SConscript("multiplayer/SCsub") SConscript("gui/SCsub") if not env["disable_3d"]: SConscript("3d/SCsub") diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index 3b55dd4a42..3d0ac291b8 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -857,7 +857,7 @@ void AnimationNodeBlendTree::add_node(const StringName &p_name, Ref<AnimationNod ERR_FAIL_COND(nodes.has(p_name)); ERR_FAIL_COND(p_node.is_null()); ERR_FAIL_COND(p_name == SceneStringNames::get_singleton()->output); - ERR_FAIL_COND(String(p_name).find("/") != -1); + ERR_FAIL_COND(String(p_name).contains("/")); Node n; n.node = p_node; diff --git a/scene/animation/animation_node_state_machine.cpp b/scene/animation/animation_node_state_machine.cpp index a23e1b689c..a68f7e037d 100644 --- a/scene/animation/animation_node_state_machine.cpp +++ b/scene/animation/animation_node_state_machine.cpp @@ -50,7 +50,7 @@ bool AnimationNodeStateMachineTransition::has_auto_advance() const { void AnimationNodeStateMachineTransition::set_advance_condition(const StringName &p_condition) { String cs = p_condition; - ERR_FAIL_COND(cs.find("/") != -1 || cs.find(":") != -1); + ERR_FAIL_COND(cs.contains("/") || cs.contains(":")); advance_condition = p_condition; if (!cs.is_empty()) { advance_condition_name = "conditions/" + cs; @@ -536,7 +536,7 @@ Variant AnimationNodeStateMachine::get_parameter_default_value(const StringName void AnimationNodeStateMachine::add_node(const StringName &p_name, Ref<AnimationNode> p_node, const Vector2 &p_position) { ERR_FAIL_COND(states.has(p_name)); ERR_FAIL_COND(p_node.is_null()); - ERR_FAIL_COND(String(p_name).find("/") != -1); + ERR_FAIL_COND(String(p_name).contains("/")); State state; state.node = p_node; @@ -553,7 +553,7 @@ void AnimationNodeStateMachine::add_node(const StringName &p_name, Ref<Animation void AnimationNodeStateMachine::replace_node(const StringName &p_name, Ref<AnimationNode> p_node) { ERR_FAIL_COND(states.has(p_name) == false); ERR_FAIL_COND(p_node.is_null()); - ERR_FAIL_COND(String(p_name).find("/") != -1); + ERR_FAIL_COND(String(p_name).contains("/")); { Ref<AnimationNode> node = states[p_name].node; diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index 128c6cae8b..4a3fd72080 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -1171,7 +1171,7 @@ void AnimationPlayer::_animation_process(double p_delta) { Error AnimationPlayer::add_animation(const StringName &p_name, const Ref<Animation> &p_animation) { #ifdef DEBUG_ENABLED - ERR_FAIL_COND_V_MSG(String(p_name).find("/") != -1 || String(p_name).find(":") != -1 || String(p_name).find(",") != -1 || String(p_name).find("[") != -1, ERR_INVALID_PARAMETER, "Invalid animation name: " + String(p_name) + "."); + ERR_FAIL_COND_V_MSG(String(p_name).contains("/") || String(p_name).contains(":") || String(p_name).contains(",") || String(p_name).contains("["), ERR_INVALID_PARAMETER, "Invalid animation name: " + String(p_name) + "."); #endif ERR_FAIL_COND_V(p_animation.is_null(), ERR_INVALID_PARAMETER); @@ -1213,7 +1213,7 @@ void AnimationPlayer::_unref_anim(const Ref<Animation> &p_anim) { void AnimationPlayer::rename_animation(const StringName &p_name, const StringName &p_new_name) { ERR_FAIL_COND(!animation_set.has(p_name)); - ERR_FAIL_COND(String(p_new_name).find("/") != -1 || String(p_new_name).find(":") != -1); + ERR_FAIL_COND(String(p_new_name).contains("/") || String(p_new_name).contains(":")); ERR_FAIL_COND(animation_set.has(p_new_name)); stop(); diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index d153b882f9..f2f69fc439 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -314,7 +314,7 @@ void AnimationNode::add_input(const String &p_name) { //root nodes can't add inputs ERR_FAIL_COND(Object::cast_to<AnimationRootNode>(this) != nullptr); Input input; - ERR_FAIL_COND(p_name.find(".") != -1 || p_name.find("/") != -1); + ERR_FAIL_COND(p_name.contains(".") || p_name.contains("/")); input.name = p_name; inputs.push_back(input); emit_changed(); @@ -322,7 +322,7 @@ void AnimationNode::add_input(const String &p_name) { void AnimationNode::set_input_name(int p_input, const String &p_name) { ERR_FAIL_INDEX(p_input, inputs.size()); - ERR_FAIL_COND(p_name.find(".") != -1 || p_name.find("/") != -1); + ERR_FAIL_COND(p_name.contains(".") || p_name.contains("/")); inputs.write[p_input].name = p_name; emit_changed(); } diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index ec451b07cf..da2ef6c5ec 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -403,7 +403,7 @@ bool BaseButton::_is_focus_owner_in_shorcut_context() const { } Node *ctx_node = get_shortcut_context(); - Control *vp_focus = get_focus_owner(); + Control *vp_focus = get_viewport() ? get_viewport()->gui_get_focus_owner() : nullptr; // If the context is valid and the viewport focus is valid, check if the context is the focus or is a parent of it. return ctx_node && vp_focus && (ctx_node == vp_focus || ctx_node->is_ancestor_of(vp_focus)); diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 22def607ed..8cb8a78e8d 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -34,14 +34,6 @@ #include "core/string/string_builder.h" #include "core/string/ustring.h" -static bool _is_whitespace(char32_t c) { - return c == '\t' || c == ' '; -} - -static bool _is_char(char32_t c) { - return !is_symbol(c); -} - void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: @@ -232,7 +224,7 @@ void CodeEdit::_notification(int p_what) { int begin = 0; int end = 0; - if (line.find(String::chr(0xFFFF)) != -1) { + if (line.contains(String::chr(0xFFFF))) { begin = font->get_string_size(line.substr(0, line.find(String::chr(0xFFFF))), font_size).x; end = font->get_string_size(line.substr(0, line.rfind(String::chr(0xFFFF))), font_size).x; } @@ -607,9 +599,9 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; - if (has_string_delimiter(chr) && cc > 0 && _is_char(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + if (has_string_delimiter(chr) && cc > 0 && !is_symbol(get_line(cl)[cc - 1]) && post_brace_pair == -1) { insert_text_at_caret(chr); - } else if (cc < get_line(cl).length() && _is_char(get_line(cl)[cc])) { + } else if (cc < get_line(cl).length() && !is_symbol(get_line(cl)[cc])) { insert_text_at_caret(chr); } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); @@ -1001,7 +993,7 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } /* Make sure this is the last char, trailing whitespace or comments are okay. */ - if (should_indent && (!_is_whitespace(c) && is_in_comment(cl, cc) == -1)) { + if (should_indent && (!is_whitespace(c) && is_in_comment(cl, cc) == -1)) { should_indent = false; } } @@ -1817,7 +1809,7 @@ void CodeEdit::request_code_completion(bool p_force) { String line = get_line(get_caret_line()); int ofs = CLAMP(get_caret_column(), 0, line.length()); - if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { emit_signal(SNAME("code_completion_requested")); } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) { emit_signal(SNAME("code_completion_requested")); @@ -1926,7 +1918,7 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (merge_text) { for (; caret_col < line.length(); caret_col++) { - if (!_is_char(line[caret_col])) { + if (is_symbol(line[caret_col])) { break; } } @@ -2562,7 +2554,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c region = E->value(); in_region = true; for (int i = E->key() - 2; i >= 0; i--) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2581,7 +2573,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c } for (int i = end_col; i < line.length(); i++) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2797,11 +2789,11 @@ void CodeEdit::_filter_code_completion_candidates_impl() { while (ofs > 0 && line[ofs] == ' ') { ofs--; } - prev_is_word = _is_char(line[ofs]); + prev_is_word = !is_symbol(line[ofs]); /* Otherwise get current word and set cofs to the start. */ } else { int start_cofs = cofs; - while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || _is_char(line[cofs - 1]))) { + while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || !is_symbol(line[cofs - 1]))) { cofs--; } string_to_complete = line.substr(cofs, start_cofs - cofs); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index b32b32e3b9..fdae8e2f1f 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -2599,11 +2599,6 @@ Control::MouseFilter Control::get_mouse_filter() const { return data.mouse_filter; } -Control *Control::get_focus_owner() const { - ERR_FAIL_COND_V(!is_inside_tree(), nullptr); - return get_viewport()->gui_get_focus_owner(); -} - void Control::warp_mouse(const Point2 &p_to_pos) { ERR_FAIL_COND(!is_inside_tree()); get_viewport()->warp_mouse(get_global_transform().xform(p_to_pos)); @@ -2893,7 +2888,6 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("release_focus"), &Control::release_focus); ClassDB::bind_method(D_METHOD("find_prev_valid_focus"), &Control::find_prev_valid_focus); ClassDB::bind_method(D_METHOD("find_next_valid_focus"), &Control::find_next_valid_focus); - ClassDB::bind_method(D_METHOD("get_focus_owner"), &Control::get_focus_owner); ClassDB::bind_method(D_METHOD("set_h_size_flags", "flags"), &Control::set_h_size_flags); ClassDB::bind_method(D_METHOD("get_h_size_flags"), &Control::get_h_size_flags); diff --git a/scene/gui/control.h b/scene/gui/control.h index bf79f790e7..962135280f 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -462,8 +462,6 @@ public: void set_focus_previous(const NodePath &p_prev); NodePath get_focus_previous() const; - Control *get_focus_owner() const; - void set_mouse_filter(MouseFilter p_filter); MouseFilter get_mouse_filter() const; diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index f7805136f9..a985a9d031 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -98,7 +98,13 @@ void MenuButton::pressed() { popup->set_position(gp); popup->set_parent_rect(Rect2(Point2(gp - popup->get_position()), size)); - popup->take_mouse_focus(); + // If not triggered by the mouse, start the popup with its first item selected. + if (popup->get_item_count() > 0 && + ((get_action_mode() == ActionMode::ACTION_MODE_BUTTON_PRESS && Input::get_singleton()->is_action_just_pressed("ui_accept")) || + (get_action_mode() == ActionMode::ACTION_MODE_BUTTON_RELEASE && Input::get_singleton()->is_action_just_released("ui_accept")))) { + popup->set_current_index(0); + } + popup->popup(); } diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index c80de04c01..9984ab240a 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -181,7 +181,14 @@ void OptionButton::pressed() { Size2 size = get_size() * get_viewport()->get_canvas_transform().get_scale(); popup->set_position(get_screen_position() + Size2(0, size.height * get_global_transform().get_scale().y)); popup->set_size(Size2(size.width, 0)); - popup->set_current_index(current); + + // If not triggered by the mouse, start the popup with the checked item selected. + if (popup->get_item_count() > 0 && + ((get_action_mode() == ActionMode::ACTION_MODE_BUTTON_PRESS && Input::get_singleton()->is_action_just_pressed("ui_accept")) || + (get_action_mode() == ActionMode::ACTION_MODE_BUTTON_RELEASE && Input::get_singleton()->is_action_just_released("ui_accept")))) { + popup->set_current_index(current > -1 ? current : 0); + } + popup->popup(); } diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index fc853a3df4..812339dc19 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -192,7 +192,7 @@ void PopupMenu::_activate_submenu(int p_over) { Popup *submenu_popup = Object::cast_to<Popup>(n); ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[p_over].submenu + "."); if (submenu_popup->is_visible()) { - return; //already visible! + return; // Already visible. } Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); @@ -223,24 +223,33 @@ void PopupMenu::_activate_submenu(int p_over) { submenu_popup->set_close_on_parent_focus(false); submenu_popup->set_position(submenu_pos); submenu_popup->set_as_minsize(); // Shrink the popup size to its contents. - submenu_popup->popup(); - // Set autohide areas PopupMenu *submenu_pum = Object::cast_to<PopupMenu>(submenu_popup); - if (submenu_pum) { - submenu_pum->take_mouse_focus(); - // Make the position of the parent popup relative to submenu popup - this_rect.position = this_rect.position - submenu_pum->get_position(); - - // Autohide area above the submenu item - submenu_pum->clear_autohide_areas(); - submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); - - // If there is an area below the submenu item, add an autohide area there. - if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) { - int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; - submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from)); - } + if (!submenu_pum) { + submenu_popup->popup(); + return; + } + + // If not triggered by the mouse, start the popup with its first item selected. + if (submenu_pum->get_item_count() > 0 && Input::get_singleton()->is_action_just_pressed("ui_accept")) { + submenu_pum->set_current_index(0); + } + + submenu_pum->popup(); + + // Set autohide areas. + + // Make the position of the parent popup relative to submenu popup. + this_rect.position = this_rect.position - submenu_pum->get_position(); + + // Autohide area above the submenu item. + submenu_pum->clear_autohide_areas(); + submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); + + // If there is an area below the submenu item, add an autohide area there. + if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) { + int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; + submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from)); } } @@ -1747,6 +1756,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("get_item_tooltip", "index"), &PopupMenu::get_item_tooltip); ClassDB::bind_method(D_METHOD("get_item_shortcut", "index"), &PopupMenu::get_item_shortcut); + ClassDB::bind_method(D_METHOD("set_current_index", "index"), &PopupMenu::set_current_index); ClassDB::bind_method(D_METHOD("get_current_index"), &PopupMenu::get_current_index); ClassDB::bind_method(D_METHOD("set_item_count", "count"), &PopupMenu::set_item_count); ClassDB::bind_method(D_METHOD("get_item_count"), &PopupMenu::get_item_count); diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 8f4c063c58..4865b9770e 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -421,9 +421,24 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> Line &l = p_frame->lines.write[p_line]; + uint16_t autowrap_flags = TextServer::BREAK_MANDATORY; + switch (autowrap_mode) { + case AUTOWRAP_WORD_SMART: + autowrap_flags = TextServer::BREAK_WORD_BOUND_ADAPTIVE | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_WORD: + autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_ARBITRARY: + autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_OFF: + break; + } + // Clear cache. l.text_buf->clear(); - l.text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_TRIM_EDGE_SPACES); + l.text_buf->set_flags(autowrap_flags | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_TRIM_EDGE_SPACES); l.char_offset = *r_char_offset; l.char_count = 0; @@ -4077,6 +4092,19 @@ String RichTextLabel::get_language() const { return language; } +void RichTextLabel::set_autowrap_mode(RichTextLabel::AutowrapMode p_mode) { + if (autowrap_mode != p_mode) { + autowrap_mode = p_mode; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} + +RichTextLabel::AutowrapMode RichTextLabel::get_autowrap_mode() const { + return autowrap_mode; +} + void RichTextLabel::set_percent_visible(float p_percent) { if (percent_visible != p_percent) { if (p_percent < 0 || p_percent >= 1) { @@ -4188,6 +4216,9 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_language", "language"), &RichTextLabel::set_language); ClassDB::bind_method(D_METHOD("get_language"), &RichTextLabel::get_language); + ClassDB::bind_method(D_METHOD("set_autowrap_mode", "autowrap_mode"), &RichTextLabel::set_autowrap_mode); + ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &RichTextLabel::get_autowrap_mode); + ClassDB::bind_method(D_METHOD("set_meta_underline", "enable"), &RichTextLabel::set_meta_underline); ClassDB::bind_method(D_METHOD("is_meta_underlined"), &RichTextLabel::is_meta_underlined); @@ -4280,6 +4311,8 @@ void RichTextLabel::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "set_language", "get_language"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Off,Arbitrary,Word,Word (Smart)"), "set_autowrap_mode", "get_autowrap_mode"); + ADD_GROUP("Structured Text", "structured_text_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); @@ -4288,6 +4321,11 @@ void RichTextLabel::_bind_methods() { ADD_SIGNAL(MethodInfo("meta_hover_started", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); ADD_SIGNAL(MethodInfo("meta_hover_ended", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); + BIND_ENUM_CONSTANT(AUTOWRAP_OFF); + BIND_ENUM_CONSTANT(AUTOWRAP_ARBITRARY); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD_SMART); + BIND_ENUM_CONSTANT(LIST_NUMBERS); BIND_ENUM_CONSTANT(LIST_LETTERS); BIND_ENUM_CONSTANT(LIST_ROMAN); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 2b24da4c37..e79244f2e4 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -39,6 +39,13 @@ class RichTextLabel : public Control { GDCLASS(RichTextLabel, Control); public: + enum AutowrapMode { + AUTOWRAP_OFF, + AUTOWRAP_ARBITRARY, + AUTOWRAP_WORD, + AUTOWRAP_WORD_SMART + }; + enum ListType { LIST_NUMBERS, LIST_LETTERS, @@ -346,6 +353,8 @@ private: VScrollBar *vscroll = nullptr; + AutowrapMode autowrap_mode = AUTOWRAP_WORD_SMART; + bool scroll_visible = false; bool scroll_follow = false; bool scroll_following = false; @@ -574,6 +583,9 @@ public: void set_language(const String &p_language); String get_language() const; + void set_autowrap_mode(AutowrapMode p_mode); + AutowrapMode get_autowrap_mode() const; + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); Control::StructuredTextParser get_structured_text_bidi_override() const; @@ -603,6 +615,7 @@ public: ~RichTextLabel(); }; +VARIANT_ENUM_CAST(RichTextLabel::AutowrapMode); VARIANT_ENUM_CAST(RichTextLabel::ListType); VARIANT_ENUM_CAST(RichTextLabel::ItemType); VARIANT_ENUM_CAST(RichTextLabel::VisibleCharactersBehavior); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 0ee4a6af4e..bb259843b8 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -43,18 +43,6 @@ #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { - return !is_symbol(c); -} - -static bool _is_whitespace(char32_t c) { - return c == '\t' || c == ' '; -} - -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - /////////////////////////////////////////////////////////////////////////////// /// TEXT /// /////////////////////////////////////////////////////////////////////////////// @@ -820,8 +808,8 @@ void TextEdit::_notification(int p_what) { int xpos = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * j)) + tabs; bool out_of_bounds = (xpos >= xmargin_end + minimap_width); - bool is_whitespace = _is_whitespace(str[j]); - if (!is_whitespace) { + bool whitespace = is_whitespace(str[j]); + if (!whitespace) { characters++; if (j < str.length() - 1 && color == previous_color && !out_of_bounds) { @@ -843,7 +831,7 @@ void TextEdit::_notification(int p_what) { if (characters > 0) { previous_color.a *= 0.6; // take one for zero indexing, and if we hit whitespace / the end of a word. - int chars = MAX(0, (j - (characters - 1)) - (is_whitespace ? 1 : 0)) + 1; + int chars = MAX(0, (j - (characters - 1)) - (whitespace ? 1 : 0)) + 1; int char_x_ofs = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * chars)) + tabs; if (rtl) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(size.width - char_x_ofs - minimap_char_size.x * characters, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); @@ -1144,7 +1132,7 @@ void TextEdit::_notification(int p_what) { } if (!clipped && lookup_symbol_word.length() != 0) { // Highlight word - if (_is_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '.') { + if (is_ascii_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '_' || lookup_symbol_word[0] == '.') { int highlighted_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); while (highlighted_word_col != -1) { Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_word_col + start, highlighted_word_col + lookup_symbol_word.length() + start); @@ -3037,7 +3025,7 @@ int TextEdit::get_first_non_whitespace_column(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); int col = 0; - while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) { + while (col < text[p_line].length() && is_whitespace(text[p_line][col])) { col++; } return col; @@ -3622,9 +3610,9 @@ Point2i TextEdit::search(const String &p_key, uint32_t p_search_flags, int p_fro if (pos != -1 && (p_search_flags & SEARCH_WHOLE_WORDS)) { // Validate for whole words. - if (pos > 0 && _is_text_char(text_line[pos - 1])) { + if (pos > 0 && !is_symbol(text_line[pos - 1])) { is_match = false; - } else if (pos + p_key.length() < text_line.length() && _is_text_char(text_line[pos + p_key.length()])) { + } else if (pos + p_key.length() < text_line.length() && !is_symbol(text_line[pos + p_key.length()])) { is_match = false; } } @@ -5779,9 +5767,9 @@ int TextEdit::_get_column_pos_of_word(const String &p_key, const String &p_searc if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { p_from_column = col; - if (col > 0 && _is_text_char(p_search[col - 1])) { + if (col > 0 && !is_symbol(p_search[col - 1])) { col = -1; - } else if ((col + p_key.length()) < p_search.length() && _is_text_char(p_search[col + p_key.length()])) { + } else if ((col + p_key.length()) < p_search.length() && !is_symbol(p_search[col + p_key.length()])) { col = -1; } } diff --git a/scene/main/node.cpp b/scene/main/node.cpp index a2415442f8..6b9d8ab211 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -32,6 +32,7 @@ #include "core/core_string_names.h" #include "core/io/resource_loader.h" +#include "core/multiplayer/multiplayer_api.h" #include "core/object/message_queue.h" #include "core/string/print_string.h" #include "instance_placeholder.h" @@ -110,9 +111,6 @@ void Node::_notification(int p_notification) { memdelete(data.path_cache); data.path_cache = nullptr; } - if (data.scene_file_path.length()) { - get_multiplayer()->scene_enter_exit_notify(data.scene_file_path, this, false); - } } break; case NOTIFICATION_PATH_RENAMED: { if (data.path_cache) { @@ -141,12 +139,6 @@ void Node::_notification(int p_notification) { } GDVIRTUAL_CALL(_ready); - - if (data.scene_file_path.length()) { - ERR_FAIL_COND(!is_inside_tree()); - get_multiplayer()->scene_enter_exit_notify(data.scene_file_path, this, true); - } - } break; case NOTIFICATION_POSTINITIALIZE: { data.in_constructor = false; @@ -211,6 +203,12 @@ void Node::_propagate_enter_tree() { data.tree->node_added(this); + if (data.parent) { + Variant c = this; + const Variant *cptr = &c; + data.parent->emit_signal(SNAME("child_entered_tree"), &cptr, 1); + } + data.blocked++; //block while adding children @@ -281,6 +279,12 @@ void Node::_propagate_exit_tree() { data.tree->node_removed(this); } + if (data.parent) { + Variant c = this; + const Variant *cptr = &c; + data.parent->emit_signal(SNAME("child_exited_tree"), &cptr, 1); + } + // exit groups for (KeyValue<StringName, GroupData> &E : data.grouped) { @@ -1049,7 +1053,7 @@ void Node::_generate_serial_child_name(const Node *p_child, StringName &name) co String nums; for (int i = name_string.length() - 1; i >= 0; i--) { char32_t n = name_string[i]; - if (n >= '0' && n <= '9') { + if (is_digit(n)) { nums = String::chr(name_string[i]) + nums; } else { break; @@ -2865,6 +2869,8 @@ void Node::_bind_methods() { ADD_SIGNAL(MethodInfo("tree_entered")); ADD_SIGNAL(MethodInfo("tree_exiting")); ADD_SIGNAL(MethodInfo("tree_exited")); + ADD_SIGNAL(MethodInfo("child_entered_tree", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node"))); + ADD_SIGNAL(MethodInfo("child_exited_tree", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node"))); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_name", "get_name"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "scene_file_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_scene_file_path", "get_scene_file_path"); diff --git a/scene/main/node.h b/scene/main/node.h index a1fc672a15..0ac10f4381 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -212,7 +212,6 @@ protected: static String _get_name_num_separator(); friend class SceneState; - friend class MultiplayerReplicator; void _add_child_nocheck(Node *p_child, const StringName &p_name); void _set_owner_nocheck(Node *p_owner); @@ -467,7 +466,7 @@ public: bool is_displayed_folded() const; /* NETWORK */ - void set_multiplayer_authority(int p_peer_id, bool p_recursive = true); + virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true); int get_multiplayer_authority() const; bool is_multiplayer_authority() const; diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 45f04b28b9..0e4a6a4b5c 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -36,6 +36,7 @@ #include "core/io/dir_access.h" #include "core/io/marshalls.h" #include "core/io/resource_loader.h" +#include "core/multiplayer/multiplayer_api.h" #include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h index 1dff1dab4f..a5cd52b4ca 100644 --- a/scene/main/scene_tree.h +++ b/scene/main/scene_tree.h @@ -31,7 +31,6 @@ #ifndef SCENE_TREE_H #define SCENE_TREE_H -#include "core/multiplayer/multiplayer_api.h" #include "core/os/main_loop.h" #include "core/os/thread_safe.h" #include "core/templates/self_list.h" @@ -46,6 +45,7 @@ class Node; class Window; class Material; class Mesh; +class MultiplayerAPI; class SceneDebugger; class Tween; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 09880ad6cf..522997cdf5 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -1231,7 +1231,7 @@ void Viewport::_gui_show_tooltip() { base_tooltip->set_anchors_and_offsets_preset(Control::PRESET_WIDE); - panel->set_transient(false); + panel->set_transient(true); panel->set_flag(Window::FLAG_NO_FOCUS, true); panel->set_wrap_controls(true); panel->add_child(base_tooltip); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index fbc0bc5301..f2ebe50fa3 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -1614,6 +1614,7 @@ void Window::_bind_methods() { BIND_ENUM_CONSTANT(MODE_MINIMIZED); BIND_ENUM_CONSTANT(MODE_MAXIMIZED); BIND_ENUM_CONSTANT(MODE_FULLSCREEN); + BIND_ENUM_CONSTANT(MODE_EXCLUSIVE_FULLSCREEN); BIND_ENUM_CONSTANT(FLAG_RESIZE_DISABLED); BIND_ENUM_CONSTANT(FLAG_BORDERLESS); diff --git a/scene/main/window.h b/scene/main/window.h index 2dd1dd6601..f37689f905 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -46,6 +46,7 @@ public: MODE_MINIMIZED = DisplayServer::WINDOW_MODE_MINIMIZED, MODE_MAXIMIZED = DisplayServer::WINDOW_MODE_MAXIMIZED, MODE_FULLSCREEN = DisplayServer::WINDOW_MODE_FULLSCREEN, + MODE_EXCLUSIVE_FULLSCREEN = DisplayServer::WINDOW_MODE_EXCLUSIVE_FULLSCREEN, }; enum Flags { diff --git a/scene/multiplayer/SCsub b/scene/multiplayer/SCsub new file mode 100644 index 0000000000..fc61250247 --- /dev/null +++ b/scene/multiplayer/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.scene_sources, "*.cpp") diff --git a/scene/multiplayer/multiplayer_spawner.cpp b/scene/multiplayer/multiplayer_spawner.cpp new file mode 100644 index 0000000000..4f2a9d9e83 --- /dev/null +++ b/scene/multiplayer/multiplayer_spawner.cpp @@ -0,0 +1,227 @@ +/*************************************************************************/ +/* multiplayer_spawner.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_spawner.h" + +#include "core/io/marshalls.h" +#include "core/multiplayer/multiplayer_api.h" +#include "scene/main/window.h" +#include "scene/scene_string_names.h" + +void MultiplayerSpawner::_bind_methods() { + ClassDB::bind_method(D_METHOD("spawn", "data"), &MultiplayerSpawner::spawn, DEFVAL(Variant())); + + ClassDB::bind_method(D_METHOD("get_spawnable_scenes"), &MultiplayerSpawner::get_spawnable_scenes); + ClassDB::bind_method(D_METHOD("set_spawnable_scenes", "scenes"), &MultiplayerSpawner::set_spawnable_scenes); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "replication", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_spawnable_scenes", "get_spawnable_scenes"); + + ClassDB::bind_method(D_METHOD("get_spawn_path"), &MultiplayerSpawner::get_spawn_path); + ClassDB::bind_method(D_METHOD("set_spawn_path", "path"), &MultiplayerSpawner::set_spawn_path); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "spawn_path", PROPERTY_HINT_NONE, ""), "set_spawn_path", "get_spawn_path"); + + ClassDB::bind_method(D_METHOD("get_spawn_limit"), &MultiplayerSpawner::get_spawn_limit); + ClassDB::bind_method(D_METHOD("set_spawn_limit", "limit"), &MultiplayerSpawner::set_spawn_limit); + ADD_PROPERTY(PropertyInfo(Variant::INT, "spawn_limit", PROPERTY_HINT_RANGE, "0,1024,1,or_greater"), "set_spawn_limit", "get_spawn_limit"); + + ClassDB::bind_method(D_METHOD("set_auto_spawning", "enabled"), &MultiplayerSpawner::set_auto_spawning); + ClassDB::bind_method(D_METHOD("is_auto_spawning"), &MultiplayerSpawner::is_auto_spawning); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_spawn"), "set_auto_spawning", "is_auto_spawning"); + + GDVIRTUAL_BIND(_spawn_custom, "data"); + + ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); + ADD_SIGNAL(MethodInfo("spawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); +} + +void MultiplayerSpawner::_update_spawn_node() { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return; + } +#endif + if (spawn_node.is_valid()) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)); + if (node && node->is_connected("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added))) { + node->disconnect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added)); + } + } + Node *node = spawn_path.is_empty() && is_inside_tree() ? nullptr : get_node_or_null(spawn_path); + if (node) { + spawn_node = node->get_instance_id(); + if (auto_spawn) { + node->connect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added)); + } + } else { + spawn_node = ObjectID(); + } +} + +void MultiplayerSpawner::_notification(int p_what) { + if (p_what == NOTIFICATION_POST_ENTER_TREE) { + _update_spawn_node(); + } else if (p_what == NOTIFICATION_EXIT_TREE) { + _update_spawn_node(); + const ObjectID *oid = nullptr; + while ((oid = tracked_nodes.next(oid))) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(*oid)); + ERR_CONTINUE(!node); + node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit)); + // This is unlikely, but might still crash the engine. + if (node->is_connected(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready))) { + node->disconnect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready)); + } + get_multiplayer()->despawn(node, this); + } + tracked_nodes.clear(); + } +} + +void MultiplayerSpawner::_node_added(Node *p_node) { + if (!get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority()) { + return; + } + if (tracked_nodes.has(p_node->get_instance_id())) { + return; + } + const Node *parent = get_spawn_node(); + if (!parent || p_node->get_parent() != parent) { + return; + } + int id = get_scene_id(p_node->get_scene_file_path()); + if (id == INVALID_ID) { + return; + } + const String name = p_node->get_name(); + ERR_FAIL_COND_MSG(name.validate_node_name() != name, vformat("Unable to auto-spawn node with reserved name: %s. Make sure to add your replicated scenes via 'add_child(node, true)' to produce valid names.", name)); + _track(p_node, Variant(), id); +} + +void MultiplayerSpawner::set_auto_spawning(bool p_enabled) { + auto_spawn = p_enabled; + _update_spawn_node(); +} + +bool MultiplayerSpawner::is_auto_spawning() const { + return auto_spawn; +} + +TypedArray<PackedScene> MultiplayerSpawner::get_spawnable_scenes() { + return spawnable_scenes; +} + +void MultiplayerSpawner::set_spawnable_scenes(TypedArray<PackedScene> p_scenes) { + spawnable_scenes = p_scenes; +} + +NodePath MultiplayerSpawner::get_spawn_path() const { + return spawn_path; +} + +void MultiplayerSpawner::set_spawn_path(const NodePath &p_path) { + spawn_path = p_path; + _update_spawn_node(); +} + +void MultiplayerSpawner::_track(Node *p_node, const Variant &p_argument, int p_scene_id) { + ObjectID oid = p_node->get_instance_id(); + if (!tracked_nodes.has(oid)) { + tracked_nodes[oid] = SpawnInfo(p_argument.duplicate(true), p_scene_id); + p_node->connect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit), varray(p_node->get_instance_id()), CONNECT_ONESHOT); + p_node->connect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready), varray(p_node->get_instance_id()), CONNECT_ONESHOT); + } +} + +void MultiplayerSpawner::_node_ready(ObjectID p_id) { + get_multiplayer()->spawn(ObjectDB::get_instance(p_id), this); +} + +void MultiplayerSpawner::_node_exit(ObjectID p_id) { + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id)); + ERR_FAIL_COND(!node); + if (tracked_nodes.has(p_id)) { + tracked_nodes.erase(p_id); + get_multiplayer()->despawn(node, this); + } +} + +int MultiplayerSpawner::get_scene_id(const String &p_scene) const { + for (int i = 0; i < spawnable_scenes.size(); i++) { + Ref<PackedScene> ps = spawnable_scenes[i]; + ERR_CONTINUE(ps.is_null()); + if (ps->get_path() == p_scene) { + return i; + } + } + return INVALID_ID; +} + +int MultiplayerSpawner::get_spawn_id(const ObjectID &p_id) const { + const SpawnInfo *info = tracked_nodes.getptr(p_id); + return info ? info->id : INVALID_ID; +} + +const Variant MultiplayerSpawner::get_spawn_argument(const ObjectID &p_id) const { + const SpawnInfo *info = tracked_nodes.getptr(p_id); + return info ? info->args : Variant(); +} + +Node *MultiplayerSpawner::instantiate_scene(int p_id) { + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + ERR_FAIL_INDEX_V(p_id, spawnable_scenes.size(), nullptr); + Ref<PackedScene> scene = spawnable_scenes[p_id]; + ERR_FAIL_COND_V(scene.is_null(), nullptr); + return scene->instantiate(); +} + +Node *MultiplayerSpawner::instantiate_custom(const Variant &p_data) { + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + Object *obj = nullptr; + Node *node = nullptr; + if (GDVIRTUAL_CALL(_spawn_custom, p_data, obj)) { + node = Object::cast_to<Node>(obj); + } + return node; +} + +Node *MultiplayerSpawner::spawn(const Variant &p_data) { + ERR_FAIL_COND_V(!is_inside_tree() || !get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority(), nullptr); + ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!"); + ERR_FAIL_COND_V_MSG(!GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom), nullptr, "Custom spawn requires the '_spawn_custom' virtual method to be implemented via script."); + + Node *parent = get_spawn_node(); + ERR_FAIL_COND_V_MSG(!parent, nullptr, "Cannot find spawn node."); + + Node *node = instantiate_custom(p_data); + ERR_FAIL_COND_V_MSG(!node, nullptr, "The '_spawn_custom' implementation must return a valid Node."); + + _track(node, p_data); + parent->add_child(node, true); + return node; +} diff --git a/scene/multiplayer/multiplayer_spawner.h b/scene/multiplayer/multiplayer_spawner.h new file mode 100644 index 0000000000..63948e39a5 --- /dev/null +++ b/scene/multiplayer/multiplayer_spawner.h @@ -0,0 +1,101 @@ +/*************************************************************************/ +/* multiplayer_spawner.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_SPAWNER_H +#define MULTIPLAYER_SPAWNER_H + +#include "scene/main/node.h" + +#include "core/variant/typed_array.h" +#include "scene/resources/packed_scene.h" +#include "scene/resources/scene_replication_config.h" + +class MultiplayerSpawner : public Node { + GDCLASS(MultiplayerSpawner, Node); + +public: + enum { + INVALID_ID = 0xFF, + }; + +private: + TypedArray<PackedScene> spawnable_scenes; + Set<ResourceUID::ID> spawnable_ids; + NodePath spawn_path; + + struct SpawnInfo { + Variant args; + int id = INVALID_ID; + SpawnInfo(Variant p_args, int p_id) { + id = p_id; + args = p_args; + } + SpawnInfo() {} + }; + + ObjectID spawn_node; + HashMap<ObjectID, SpawnInfo> tracked_nodes; + bool auto_spawn = false; + uint32_t spawn_limit = 0; + + void _update_spawn_node(); + void _track(Node *p_node, const Variant &p_argument, int p_scene_id = INVALID_ID); + void _node_added(Node *p_node); + void _node_exit(ObjectID p_id); + void _node_ready(ObjectID p_id); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + Node *get_spawn_node() const { return spawn_node.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)) : nullptr; } + TypedArray<PackedScene> get_spawnable_scenes(); + void set_spawnable_scenes(TypedArray<PackedScene> p_scenes); + NodePath get_spawn_path() const; + void set_spawn_path(const NodePath &p_path); + uint32_t get_spawn_limit() const { return spawn_limit; } + void set_spawn_limit(uint32_t p_limit) { spawn_limit = p_limit; } + bool is_auto_spawning() const; + void set_auto_spawning(bool p_enabled); + + const Variant get_spawn_argument(const ObjectID &p_id) const; + int get_spawn_id(const ObjectID &p_id) const; + int get_scene_id(const String &p_path) const; + Node *spawn(const Variant &p_data = Variant()); + Node *instantiate_custom(const Variant &p_data); + Node *instantiate_scene(int p_idx); + + GDVIRTUAL1R(Object *, _spawn_custom, const Variant &); + + MultiplayerSpawner() {} +}; + +#endif // MULTIPLAYER_SPAWNER_H diff --git a/scene/multiplayer/multiplayer_synchronizer.cpp b/scene/multiplayer/multiplayer_synchronizer.cpp new file mode 100644 index 0000000000..fbe1b99cc9 --- /dev/null +++ b/scene/multiplayer/multiplayer_synchronizer.cpp @@ -0,0 +1,158 @@ +/*************************************************************************/ +/* multiplayer_synchronizer.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_synchronizer.h" + +#include "core/config/engine.h" +#include "core/multiplayer/multiplayer_api.h" + +Object *MultiplayerSynchronizer::_get_prop_target(Object *p_obj, const NodePath &p_path) { + if (p_path.get_name_count() == 0) { + return p_obj; + } + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V_MSG(!node || !node->has_node(p_path), nullptr, vformat("Node '%s' not found.", p_path)); + return node->get_node(p_path); +} + +void MultiplayerSynchronizer::_stop() { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (node) { + get_multiplayer()->replication_stop(node, this); + } +} + +void MultiplayerSynchronizer::_start() { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (node) { + get_multiplayer()->replication_start(node, this); + } +} + +Error MultiplayerSynchronizer::get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs) { + ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER); + r_variant.resize(p_properties.size()); + r_variant_ptrs.resize(r_variant.size()); + int i = 0; + for (const NodePath &prop : p_properties) { + bool valid = false; + const Object *obj = _get_prop_target(p_obj, prop); + ERR_FAIL_COND_V(!obj, FAILED); + r_variant.write[i] = obj->get(prop.get_concatenated_subnames(), &valid); + r_variant_ptrs.write[i] = &r_variant[i]; + ERR_FAIL_COND_V_MSG(!valid, ERR_INVALID_DATA, vformat("Property '%s' not found.", prop)); + i++; + } + return OK; +} + +Error MultiplayerSynchronizer::set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state) { + ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER); + int i = 0; + for (const NodePath &prop : p_properties) { + Object *obj = _get_prop_target(p_obj, prop); + ERR_FAIL_COND_V(!obj, FAILED); + obj->set(prop.get_concatenated_subnames(), p_state[i]); + i += 1; + } + return OK; +} + +void MultiplayerSynchronizer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_root_path", "path"), &MultiplayerSynchronizer::set_root_path); + ClassDB::bind_method(D_METHOD("get_root_path"), &MultiplayerSynchronizer::get_root_path); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path"); + + ClassDB::bind_method(D_METHOD("set_replication_interval", "milliseconds"), &MultiplayerSynchronizer::set_replication_interval); + ClassDB::bind_method(D_METHOD("get_replication_interval"), &MultiplayerSynchronizer::get_replication_interval); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001"), "set_replication_interval", "get_replication_interval"); + + ClassDB::bind_method(D_METHOD("set_replication_config", "config"), &MultiplayerSynchronizer::set_replication_config); + ClassDB::bind_method(D_METHOD("get_replication_config"), &MultiplayerSynchronizer::get_replication_config); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig"), "set_replication_config", "get_replication_config"); +} + +void MultiplayerSynchronizer::_notification(int p_what) { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return; + } +#endif + if (root_path.is_empty()) { + return; + } + if (p_what == NOTIFICATION_ENTER_TREE) { + _start(); + } else if (p_what == NOTIFICATION_EXIT_TREE) { + _stop(); + } +} + +void MultiplayerSynchronizer::set_replication_interval(double p_interval) { + ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)"); + interval_msec = uint64_t(p_interval * 1000); +} + +double MultiplayerSynchronizer::get_replication_interval() const { + return double(interval_msec) / 1000.0; +} + +uint64_t MultiplayerSynchronizer::get_replication_interval_msec() const { + return interval_msec; +} + +void MultiplayerSynchronizer::set_replication_config(Ref<SceneReplicationConfig> p_config) { + replication_config = p_config; +} + +Ref<SceneReplicationConfig> MultiplayerSynchronizer::get_replication_config() { + return replication_config; +} + +void MultiplayerSynchronizer::set_root_path(const NodePath &p_path) { + _stop(); + root_path = p_path; + _start(); +} + +NodePath MultiplayerSynchronizer::get_root_path() const { + return root_path; +} + +void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_recursive) { + Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; + if (!node) { + Node::set_multiplayer_authority(p_peer_id, p_recursive); + return; + } + get_multiplayer()->replication_stop(node, this); + Node::set_multiplayer_authority(p_peer_id, p_recursive); + get_multiplayer()->replication_start(node, this); +} diff --git a/scene/multiplayer/multiplayer_synchronizer.h b/scene/multiplayer/multiplayer_synchronizer.h new file mode 100644 index 0000000000..e856745379 --- /dev/null +++ b/scene/multiplayer/multiplayer_synchronizer.h @@ -0,0 +1,72 @@ +/*************************************************************************/ +/* multiplayer_synchronizer.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_SYNCHRONIZER_H +#define MULTIPLAYER_SYNCHRONIZER_H + +#include "scene/main/node.h" + +#include "scene/resources/scene_replication_config.h" + +class MultiplayerSynchronizer : public Node { + GDCLASS(MultiplayerSynchronizer, Node); + +private: + Ref<SceneReplicationConfig> replication_config; + NodePath root_path; + uint64_t interval_msec = 0; + + static Object *_get_prop_target(Object *p_obj, const NodePath &p_prop); + void _start(); + void _stop(); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + static Error get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs); + static Error set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state); + + void set_replication_interval(double p_interval); + double get_replication_interval() const; + uint64_t get_replication_interval_msec() const; + + void set_replication_config(Ref<SceneReplicationConfig> p_config); + Ref<SceneReplicationConfig> get_replication_config(); + + void set_root_path(const NodePath &p_path); + NodePath get_root_path() const; + virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true) override; + + MultiplayerSynchronizer() {} +}; + +#endif // MULTIPLAYER_SYNCHRONIZER_H diff --git a/scene/multiplayer/scene_replication_interface.cpp b/scene/multiplayer/scene_replication_interface.cpp new file mode 100644 index 0000000000..7155935084 --- /dev/null +++ b/scene/multiplayer/scene_replication_interface.cpp @@ -0,0 +1,415 @@ +/*************************************************************************/ +/* scene_replication_interface.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 "scene_replication_interface.h" + +#include "core/io/marshalls.h" +#include "scene/main/node.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" + +#define MAKE_ROOM(m_amount) \ + if (packet_cache.size() < m_amount) \ + packet_cache.resize(m_amount); + +MultiplayerReplicationInterface *SceneReplicationInterface::_create(MultiplayerAPI *p_multiplayer) { + return memnew(SceneReplicationInterface(p_multiplayer)); +} + +void SceneReplicationInterface::make_default() { + MultiplayerAPI::create_default_replication_interface = _create; +} + +void SceneReplicationInterface::_free_remotes(int p_id) { + const HashMap<uint32_t, ObjectID> remotes = rep_state->peer_get_remotes(p_id); + const uint32_t *k = nullptr; + while ((k = remotes.next(k))) { + Node *node = rep_state->get_node(remotes.get(*k)); + ERR_CONTINUE(!node); + node->queue_delete(); + } +} + +void SceneReplicationInterface::on_peer_change(int p_id, bool p_connected) { + if (p_connected) { + rep_state->on_peer_change(p_id, p_connected); + for (const ObjectID &oid : rep_state->get_spawned_nodes()) { + _send_spawn(rep_state->get_node(oid), rep_state->get_spawner(oid), p_id); + } + for (const ObjectID &oid : rep_state->get_path_only_nodes()) { + Node *node = rep_state->get_node(oid); + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_CONTINUE(!node || !sync); + if (sync->is_multiplayer_authority()) { + rep_state->peer_add_node(p_id, oid); + } + } + } else { + _free_remotes(p_id); + rep_state->on_peer_change(p_id, p_connected); + } +} + +void SceneReplicationInterface::on_reset() { + for (int pid : rep_state->get_peers()) { + _free_remotes(pid); + } + rep_state->reset(); +} + +void SceneReplicationInterface::on_network_process() { + uint64_t msec = OS::get_singleton()->get_ticks_msec(); + for (int peer : rep_state->get_peers()) { + _send_sync(peer, msec); + } +} + +Error SceneReplicationInterface::on_spawn(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!spawner, ERR_INVALID_PARAMETER); + Error err = rep_state->config_add_spawn(node, spawner); + ERR_FAIL_COND_V(err != OK, err); + return _send_spawn(node, spawner, 0); +} + +Error SceneReplicationInterface::on_despawn(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!p_obj || !spawner, ERR_INVALID_PARAMETER); + Error err = rep_state->config_del_spawn(node, spawner); + ERR_FAIL_COND_V(err != OK, err); + return _send_despawn(node, 0); +} + +Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!sync, ERR_INVALID_PARAMETER); + rep_state->config_add_sync(node, sync); + // Try to apply initial state if spawning (hack to apply if before ready). + if (pending_spawn == p_obj->get_instance_id()) { + pending_spawn = ObjectID(); // Make sure this only happens once. + const List<NodePath> props = sync->get_replication_config()->get_spawn_properties(); + Vector<Variant> vars; + vars.resize(props.size()); + int consumed; + Error err = MultiplayerAPI::decode_and_decompress_variants(vars, pending_buffer, pending_buffer_size, consumed); + ERR_FAIL_COND_V(err, err); + err = MultiplayerSynchronizer::set_state(props, node, vars); + ERR_FAIL_COND_V(err, err); + } else if (multiplayer->has_multiplayer_peer() && sync->is_multiplayer_authority()) { + // Either it's a spawn or a static sync, in any case add it to the list of known nodes. + rep_state->peer_add_node(0, p_obj->get_instance_id()); + } + return OK; +} + +Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_config) { + Node *node = Object::cast_to<Node>(p_obj); + ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER); + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object()); + ERR_FAIL_COND_V(!p_obj || !sync, ERR_INVALID_PARAMETER); + return rep_state->config_del_sync(node, sync); +} + +Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable) { + ERR_FAIL_COND_V(!p_buffer || p_size < 1, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(!multiplayer, ERR_UNCONFIGURED); + ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED); + Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer(); + peer->set_target_peer(p_peer); + peer->set_transfer_channel(0); + peer->set_transfer_mode(p_reliable ? Multiplayer::TRANSFER_MODE_RELIABLE : Multiplayer::TRANSFER_MODE_UNRELIABLE); + return peer->put_packet(p_buffer, p_size); +} + +Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer) { + ERR_FAIL_COND_V(p_peer < 0, ERR_BUG); + ERR_FAIL_COND_V(!multiplayer, ERR_BUG); + ERR_FAIL_COND_V(!p_spawner || !p_node, ERR_BUG); + + const ObjectID oid = p_node->get_instance_id(); + uint32_t nid = rep_state->ensure_net_id(oid); + + // Prepare custom arg and scene_id + uint8_t scene_id = p_spawner->get_spawn_id(oid); + bool is_custom = scene_id == MultiplayerSpawner::INVALID_ID; + Variant spawn_arg = p_spawner->get_spawn_argument(oid); + int spawn_arg_size = 0; + if (is_custom) { + Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, nullptr, spawn_arg_size, false); + ERR_FAIL_COND_V(err, err); + } + + // Prepare spawn state. + int state_size = 0; + Vector<Variant> state_vars; + Vector<const Variant *> state_varp; + MultiplayerSynchronizer *synchronizer = rep_state->get_synchronizer(oid); + if (synchronizer && synchronizer->get_replication_config().is_valid()) { + const List<NodePath> props = synchronizer->get_replication_config()->get_spawn_properties(); + Error err = MultiplayerSynchronizer::get_state(props, p_node, state_vars, state_varp); + ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to retrieve spawn state."); + err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), nullptr, state_size); + ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to encode spawn state."); + } + + // Prepare simplified path. + const Node *root_node = multiplayer->get_root_node(); + ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED); + NodePath rel_path = (root_node->get_path()).rel_path_to(p_spawner->get_path()); + + int path_id = 0; + multiplayer->send_confirm_path(p_spawner, rel_path, p_peer, path_id); + + // Encode name and parent ID. + CharString cname = p_node->get_name().operator String().utf8(); + int nlen = encode_cstring(cname.get_data(), nullptr); + MAKE_ROOM(1 + 1 + 4 + 4 + 4 + nlen + (is_custom ? 4 + spawn_arg_size : 0) + state_size); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = (uint8_t)MultiplayerAPI::NETWORK_COMMAND_SPAWN; + ptr[1] = scene_id; + int ofs = 2; + ofs += encode_uint32(path_id, &ptr[ofs]); + ofs += encode_uint32(nid, &ptr[ofs]); + ofs += encode_uint32(nlen, &ptr[ofs]); + ofs += encode_cstring(cname.get_data(), &ptr[ofs]); + // Write args + if (is_custom) { + ofs += encode_uint32(spawn_arg_size, &ptr[ofs]); + Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, &ptr[ofs], spawn_arg_size, false); + ERR_FAIL_COND_V(err, err); + ofs += spawn_arg_size; + } + // Write state. + if (state_size) { + Error err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), &ptr[ofs], state_size); + ERR_FAIL_COND_V(err, err); + ofs += state_size; + } + Error err = _send_raw(ptr, ofs, p_peer, true); + ERR_FAIL_COND_V(err, err); + return rep_state->peer_add_node(p_peer, oid); +} + +Error SceneReplicationInterface::_send_despawn(Node *p_node, int p_peer) { + const ObjectID oid = p_node->get_instance_id(); + MAKE_ROOM(5); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = (uint8_t)MultiplayerAPI::NETWORK_COMMAND_DESPAWN; + int ofs = 1; + uint32_t nid = rep_state->get_net_id(oid); + ofs += encode_uint32(nid, &ptr[ofs]); + Error err = _send_raw(ptr, ofs, p_peer, true); + ERR_FAIL_COND_V(err, err); + return rep_state->peer_del_node(p_peer, oid); +} + +Error SceneReplicationInterface::on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 14, ERR_INVALID_DATA, "Invalid spawn packet received"); + int ofs = 1; // The spawn/despawn command. + uint8_t scene_id = p_buffer[ofs]; + ofs += 1; + uint32_t node_target = decode_uint32(&p_buffer[ofs]); + ofs += 4; + MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(multiplayer->get_cached_node(p_from, node_target)); + ERR_FAIL_COND_V(!spawner, ERR_DOES_NOT_EXIST); + ERR_FAIL_COND_V(p_from != spawner->get_multiplayer_authority(), ERR_UNAUTHORIZED); + + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + uint32_t name_len = decode_uint32(&p_buffer[ofs]); + ofs += 4; + ERR_FAIL_COND_V_MSG(name_len > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA, vformat("Invalid spawn packet size: %d, wants: %d", p_buffer_len, ofs + name_len)); + ERR_FAIL_COND_V_MSG(name_len < 1, ERR_INVALID_DATA, "Zero spawn name size."); + + // We need to make sure no trickery happens here, but we want to allow autogenerated ("@") node names. + const String name = String::utf8((const char *)&p_buffer[ofs], name_len); + ERR_FAIL_COND_V_MSG(name.validate_node_name() != name, ERR_INVALID_DATA, vformat("Invalid node name received: '%s'. Make sure to add nodes via 'add_child(node, true)' remotely.", name)); + ofs += name_len; + + // Check that we can spawn. + Node *parent = spawner->get_node_or_null(spawner->get_spawn_path()); + ERR_FAIL_COND_V(!parent, ERR_UNCONFIGURED); + ERR_FAIL_COND_V(parent->has_node(name), ERR_INVALID_DATA); + + Node *node = nullptr; + if (scene_id == MultiplayerSpawner::INVALID_ID) { + // Custom spawn. + ERR_FAIL_COND_V(p_buffer_len - ofs < 4, ERR_INVALID_DATA); + uint32_t arg_size = decode_uint32(&p_buffer[ofs]); + ofs += 4; + ERR_FAIL_COND_V(arg_size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA); + Variant v; + Error err = MultiplayerAPI::decode_and_decompress_variant(v, &p_buffer[ofs], arg_size, nullptr, false); + ERR_FAIL_COND_V(err != OK, err); + ofs += arg_size; + node = spawner->instantiate_custom(v); + } else { + // Scene based spawn. + node = spawner->instantiate_scene(scene_id); + } + ERR_FAIL_COND_V(!node, ERR_UNAUTHORIZED); + node->set_name(name); + rep_state->peer_add_remote(p_from, net_id, node, spawner); + // The initial state will be applied during the sync config (i.e. before _ready). + int state_len = p_buffer_len - ofs; + if (state_len) { + pending_spawn = node->get_instance_id(); + pending_buffer = &p_buffer[ofs]; + pending_buffer_size = state_len; + } + parent->add_child(node); + pending_spawn = ObjectID(); + pending_buffer = nullptr; + pending_buffer_size = 0; + return OK; +} + +Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 5, ERR_INVALID_DATA, "Invalid spawn packet received"); + int ofs = 1; // The spawn/despawn command. + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + Node *node = nullptr; + Error err = rep_state->peer_del_remote(p_from, net_id, &node); + ERR_FAIL_COND_V(err != OK, err); + ERR_FAIL_COND_V(!node, ERR_BUG); + node->queue_delete(); + return OK; +} + +void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) { + const Set<ObjectID> &known = rep_state->get_known_nodes(p_peer); + if (known.is_empty()) { + return; + } + MAKE_ROOM(sync_mtu); + uint8_t *ptr = packet_cache.ptrw(); + ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC; + int ofs = 1; + ofs += encode_uint16(rep_state->peer_sync_next(p_peer), &ptr[1]); + // Can only send updates for already notified nodes. + // This is a lazy implementation, we could optimize much more here with by grouping by replication config. + for (const ObjectID &oid : known) { + if (!rep_state->update_sync_time(oid, p_msec)) { + continue; // nothing to sync. + } + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_CONTINUE(!sync); + Node *node = rep_state->get_node(oid); + ERR_CONTINUE(!node); + int size; + Vector<Variant> vars; + Vector<const Variant *> varp; + const List<NodePath> props = sync->get_replication_config()->get_sync_properties(); + Error err = MultiplayerSynchronizer::get_state(props, node, vars, varp); + ERR_CONTINUE_MSG(err != OK, "Unable to retrieve sync state."); + err = MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), nullptr, size); + ERR_CONTINUE_MSG(err != OK, "Unable to encode sync state."); + // TODO Handle single state above MTU. + ERR_CONTINUE_MSG(size > 3 + 4 + 4 + sync_mtu, vformat("Node states bigger then MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path())); + if (ofs + 4 + 4 + size > sync_mtu) { + // Send what we got, and reset write. + _send_raw(packet_cache.ptr(), ofs, p_peer, false); + ofs = 3; + } + if (size) { + uint32_t net_id = rep_state->get_net_id(oid); + if (net_id == 0) { + // First time path based ID. + const Node *root_node = multiplayer->get_root_node(); + ERR_FAIL_COND(!root_node); + NodePath rel_path = (root_node->get_path()).rel_path_to(sync->get_path()); + int path_id = 0; + multiplayer->send_confirm_path(sync, rel_path, p_peer, path_id); + net_id = path_id; + rep_state->set_net_id(oid, net_id | 0x80000000); + } + ofs += encode_uint32(rep_state->get_net_id(oid), &ptr[ofs]); + ofs += encode_uint32(size, &ptr[ofs]); + MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), &ptr[ofs], size); + ofs += size; + } + } + if (ofs > 3) { + // Got some left over to send. + _send_raw(packet_cache.ptr(), ofs, p_peer, false); + } +} + +Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) { + ERR_FAIL_COND_V_MSG(p_buffer_len < 11, ERR_INVALID_DATA, "Invalid sync packet received"); + uint16_t time = decode_uint16(&p_buffer[1]); + int ofs = 3; + rep_state->peer_sync_recv(p_from, time); + while (ofs + 8 < p_buffer_len) { + uint32_t net_id = decode_uint32(&p_buffer[ofs]); + ofs += 4; + uint32_t size = decode_uint32(&p_buffer[ofs]); + ofs += 4; + Node *node = nullptr; + if (net_id & 0x80000000) { + MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_cached_node(p_from, net_id & 0x7FFFFFFF)); + ERR_FAIL_COND_V(!sync || sync->get_multiplayer_authority() != p_from, ERR_UNAUTHORIZED); + node = sync->get_node(sync->get_root_path()); + } else { + node = rep_state->peer_get_remote(p_from, net_id); + } + if (!node) { + // Not received yet. + ofs += size; + continue; + } + const ObjectID oid = node->get_instance_id(); + if (!rep_state->update_last_node_sync(oid, time)) { + // State is too old. + ofs += size; + continue; + } + MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid); + ERR_FAIL_COND_V(!sync, ERR_BUG); + ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_BUG); + const List<NodePath> props = sync->get_replication_config()->get_sync_properties(); + Vector<Variant> vars; + vars.resize(props.size()); + int consumed; + Error err = MultiplayerAPI::decode_and_decompress_variants(vars, &p_buffer[ofs], size, consumed); + ERR_FAIL_COND_V(err, err); + err = MultiplayerSynchronizer::set_state(props, node, vars); + ERR_FAIL_COND_V(err, err); + ofs += size; + } + return OK; +} diff --git a/scene/multiplayer/scene_replication_interface.h b/scene/multiplayer/scene_replication_interface.h new file mode 100644 index 0000000000..855878d029 --- /dev/null +++ b/scene/multiplayer/scene_replication_interface.h @@ -0,0 +1,84 @@ +/*************************************************************************/ +/* scene_replication_interface.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 SCENE_TREE_REPLICATOR_INTERFACE_H +#define SCENE_TREE_REPLICATOR_INTERFACE_H + +#include "core/multiplayer/multiplayer_api.h" + +#include "scene/multiplayer/scene_replication_state.h" + +class SceneReplicationInterface : public MultiplayerReplicationInterface { + GDCLASS(SceneReplicationInterface, MultiplayerReplicationInterface); + +private: + void _send_sync(int p_peer, uint64_t p_msec); + Error _send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer); + Error _send_despawn(Node *p_node, int p_peer); + Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable); + + void _free_remotes(int p_peer); + + Ref<SceneReplicationState> rep_state; + MultiplayerAPI *multiplayer; + PackedByteArray packet_cache; + int sync_mtu = 1350; // Highly dependent on underlying protocol. + + // An hack to apply the initial state before ready. + ObjectID pending_spawn; + const uint8_t *pending_buffer = nullptr; + int pending_buffer_size = 0; + +protected: + static MultiplayerReplicationInterface *_create(MultiplayerAPI *p_multiplayer); + +public: + static void make_default(); + + virtual void on_reset() override; + virtual void on_peer_change(int p_id, bool p_connected) override; + + virtual Error on_spawn(Object *p_obj, Variant p_config) override; + virtual Error on_despawn(Object *p_obj, Variant p_config) override; + virtual Error on_replication_start(Object *p_obj, Variant p_config) override; + virtual Error on_replication_stop(Object *p_obj, Variant p_config) override; + virtual void on_network_process() override; + + virtual Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + virtual Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + virtual Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) override; + + SceneReplicationInterface(MultiplayerAPI *p_multiplayer) { + rep_state.instantiate(); + multiplayer = p_multiplayer; + } +}; + +#endif // SCENE_TREE_REPLICATOR_INTERFACE_H diff --git a/scene/multiplayer/scene_replication_state.cpp b/scene/multiplayer/scene_replication_state.cpp new file mode 100644 index 0000000000..b8dadeff24 --- /dev/null +++ b/scene/multiplayer/scene_replication_state.cpp @@ -0,0 +1,258 @@ +/*************************************************************************/ +/* scene_replication_state.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 "scene/multiplayer/scene_replication_state.h" + +#include "core/multiplayer/multiplayer_api.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" +#include "scene/scene_string_names.h" + +SceneReplicationState::TrackedNode &SceneReplicationState::_track(const ObjectID &p_id) { + if (!tracked_nodes.has(p_id)) { + tracked_nodes[p_id] = TrackedNode(p_id); + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id)); + node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack), varray(p_id), Node::CONNECT_ONESHOT); + } + return tracked_nodes[p_id]; +} + +void SceneReplicationState::_untrack(const ObjectID &p_id) { + if (tracked_nodes.has(p_id)) { + uint32_t net_id = tracked_nodes[p_id].net_id; + uint32_t peer = tracked_nodes[p_id].remote_peer; + tracked_nodes.erase(p_id); + // If it was spawned by a remote, remove it from the received nodes. + if (peer && peers_info.has(peer)) { + peers_info[peer].recv_nodes.erase(net_id); + } + // If we spawned or synced it, we need to remove it from any peer it was sent to. + if (net_id || peer == 0) { + const int *k = nullptr; + while ((k = peers_info.next(k))) { + peers_info.get(*k).known_nodes.erase(p_id); + } + } + } +} + +const HashMap<uint32_t, ObjectID> SceneReplicationState::peer_get_remotes(int p_peer) const { + return peers_info.has(p_peer) ? peers_info[p_peer].recv_nodes : HashMap<uint32_t, ObjectID>(); +} + +bool SceneReplicationState::update_last_node_sync(const ObjectID &p_id, uint16_t p_time) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, false); + if (p_time <= tnode->last_sync && tnode->last_sync - p_time < 32767) { + return false; + } + tnode->last_sync = p_time; + return true; +} + +bool SceneReplicationState::update_sync_time(const ObjectID &p_id, uint64_t p_msec) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, false); + MultiplayerSynchronizer *sync = get_synchronizer(p_id); + if (!sync) { + return false; + } + if (tnode->last_sync_msec == p_msec) { + return true; + } + if (p_msec >= tnode->last_sync_msec + sync->get_replication_interval_msec()) { + tnode->last_sync_msec = p_msec; + return true; + } + return false; +} + +const Set<ObjectID> SceneReplicationState::get_known_nodes(int p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), Set<ObjectID>()); + return peers_info[p_peer].known_nodes; +} + +uint32_t SceneReplicationState::get_net_id(const ObjectID &p_id) const { + const TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, 0); + return tnode->net_id; +} + +void SceneReplicationState::set_net_id(const ObjectID &p_id, uint32_t p_net_id) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND(!tnode); + tnode->net_id = p_net_id; +} + +uint32_t SceneReplicationState::ensure_net_id(const ObjectID &p_id) { + TrackedNode *tnode = tracked_nodes.getptr(p_id); + ERR_FAIL_COND_V(!tnode, 0); + if (tnode->net_id == 0) { + tnode->net_id = ++last_net_id; + } + return tnode->net_id; +} + +void SceneReplicationState::on_peer_change(int p_peer, bool p_connected) { + if (p_connected) { + peers_info[p_peer] = PeerInfo(); + known_peers.insert(p_peer); + } else { + peers_info.erase(p_peer); + known_peers.erase(p_peer); + } +} + +void SceneReplicationState::reset() { + peers_info.clear(); + known_peers.clear(); + // Tracked nodes are cleared on deletion, here we only reset the ids so they can be later re-assigned. + const ObjectID *oid = nullptr; + while ((oid = tracked_nodes.next(oid))) { + TrackedNode &tobj = tracked_nodes[*oid]; + tobj.net_id = 0; + tobj.remote_peer = 0; + tobj.last_sync = 0; + } +} + +Error SceneReplicationState::config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner) { + const ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.spawner != ObjectID(), ERR_ALREADY_IN_USE); + tobj.spawner = p_spawner->get_instance_id(); + spawned_nodes.insert(oid); + // The spawner may be notified after the synchronizer. + path_only_nodes.erase(oid); + return OK; +} + +Error SceneReplicationState::config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner) { + const ObjectID oid = p_node->get_instance_id(); + ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.spawner != p_spawner->get_instance_id(), ERR_INVALID_PARAMETER); + tobj.spawner = ObjectID(); + spawned_nodes.erase(oid); + return OK; +} + +Error SceneReplicationState::config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync) { + const ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.synchronizer != ObjectID(), ERR_ALREADY_IN_USE); + tobj.synchronizer = p_sync->get_instance_id(); + // If it doesn't have a spawner, we might need to assign ID for this node using it's path. + if (tobj.spawner.is_null()) { + path_only_nodes.insert(oid); + } + return OK; +} + +Error SceneReplicationState::config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync) { + const ObjectID oid = p_node->get_instance_id(); + ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER); + TrackedNode &tobj = _track(oid); + ERR_FAIL_COND_V(tobj.synchronizer != p_sync->get_instance_id(), ERR_INVALID_PARAMETER); + tobj.synchronizer = ObjectID(); + if (path_only_nodes.has(oid)) { + p_node->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack)); + _untrack(oid); + path_only_nodes.erase(oid); + } + return OK; +} + +Error SceneReplicationState::peer_add_node(int p_peer, const ObjectID &p_id) { + if (p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER); + peers_info[p_peer].known_nodes.insert(p_id); + } else { + const int *pid = nullptr; + while ((pid = peers_info.next(pid))) { + peers_info.get(*pid).known_nodes.insert(p_id); + } + } + return OK; +} + +Error SceneReplicationState::peer_del_node(int p_peer, const ObjectID &p_id) { + if (p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER); + peers_info[p_peer].known_nodes.erase(p_id); + } else { + const int *pid = nullptr; + while ((pid = peers_info.next(pid))) { + peers_info.get(*pid).known_nodes.erase(p_id); + } + } + return OK; +} + +Node *SceneReplicationState::peer_get_remote(int p_peer, uint32_t p_net_id) { + PeerInfo *info = peers_info.getptr(p_peer); + return info && info->recv_nodes.has(p_net_id) ? Object::cast_to<Node>(ObjectDB::get_instance(info->recv_nodes[p_net_id])) : nullptr; +} + +Error SceneReplicationState::peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner) { + ERR_FAIL_COND_V(!p_node || !p_spawner, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAVAILABLE); + PeerInfo &pinfo = peers_info[p_peer]; + ObjectID oid = p_node->get_instance_id(); + TrackedNode &tobj = _track(oid); + tobj.spawner = p_spawner->get_instance_id(); + tobj.net_id = p_net_id; + tobj.remote_peer = p_peer; + tobj.last_sync = pinfo.last_recv_sync; + // Also track as a remote. + ERR_FAIL_COND_V(pinfo.recv_nodes.has(p_net_id), ERR_ALREADY_IN_USE); + pinfo.recv_nodes[p_net_id] = oid; + return OK; +} + +Error SceneReplicationState::peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAUTHORIZED); + PeerInfo &info = peers_info[p_peer]; + ERR_FAIL_COND_V(!info.recv_nodes.has(p_net_id), ERR_UNAUTHORIZED); + *r_node = Object::cast_to<Node>(ObjectDB::get_instance(info.recv_nodes[p_net_id])); + info.recv_nodes.erase(p_net_id); + return OK; +} + +uint16_t SceneReplicationState::peer_sync_next(int p_peer) { + ERR_FAIL_COND_V(!peers_info.has(p_peer), 0); + PeerInfo &info = peers_info[p_peer]; + return ++info.last_sent_sync; +} + +void SceneReplicationState::peer_sync_recv(int p_peer, uint16_t p_time) { + ERR_FAIL_COND(!peers_info.has(p_peer)); + peers_info[p_peer].last_recv_sync = p_time; +} diff --git a/scene/multiplayer/scene_replication_state.h b/scene/multiplayer/scene_replication_state.h new file mode 100644 index 0000000000..18e4d9fa39 --- /dev/null +++ b/scene/multiplayer/scene_replication_state.h @@ -0,0 +1,121 @@ +/*************************************************************************/ +/* scene_replication_state.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 SCENE_REPLICATON_STATE_H +#define SCENE_REPLICATON_STATE_H + +#include "core/object/ref_counted.h" + +class MultiplayerSpawner; +class MultiplayerSynchronizer; +class Node; + +class SceneReplicationState : public RefCounted { +private: + struct TrackedNode { + ObjectID id; + uint32_t net_id = 0; + uint32_t remote_peer = 0; + ObjectID spawner; + ObjectID synchronizer; + uint16_t last_sync = 0; + uint64_t last_sync_msec = 0; + + bool operator==(const ObjectID &p_other) { return id == p_other; } + + Node *get_node() const { return id.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(id)) : nullptr; } + MultiplayerSpawner *get_spawner() const { return spawner.is_valid() ? Object::cast_to<MultiplayerSpawner>(ObjectDB::get_instance(spawner)) : nullptr; } + MultiplayerSynchronizer *get_synchronizer() const { return synchronizer.is_valid() ? Object::cast_to<MultiplayerSynchronizer>(ObjectDB::get_instance(synchronizer)) : nullptr; } + TrackedNode() {} + TrackedNode(const ObjectID &p_id) { id = p_id; } + TrackedNode(const ObjectID &p_id, uint32_t p_net_id) { + id = p_id; + net_id = p_net_id; + } + }; + + struct PeerInfo { + Set<ObjectID> known_nodes; + HashMap<uint32_t, ObjectID> recv_nodes; + uint16_t last_sent_sync = 0; + uint16_t last_recv_sync = 0; + }; + + Set<int> known_peers; + uint32_t last_net_id = 0; + HashMap<ObjectID, TrackedNode> tracked_nodes; + HashMap<int, PeerInfo> peers_info; + Set<ObjectID> spawned_nodes; + Set<ObjectID> path_only_nodes; + + TrackedNode &_track(const ObjectID &p_id); + void _untrack(const ObjectID &p_id); + bool is_tracked(const ObjectID &p_id) const { return tracked_nodes.has(p_id); } + +public: + const Set<int> get_peers() const { return known_peers; } + const Set<ObjectID> get_spawned_nodes() const { return spawned_nodes; } + const Set<ObjectID> get_path_only_nodes() const { return path_only_nodes; } + + MultiplayerSynchronizer *get_synchronizer(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_synchronizer() : nullptr; } + MultiplayerSpawner *get_spawner(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_spawner() : nullptr; } + Node *get_node(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_node() : nullptr; } + bool update_last_node_sync(const ObjectID &p_id, uint16_t p_time); + bool update_sync_time(const ObjectID &p_id, uint64_t p_msec); + + const Set<ObjectID> get_known_nodes(int p_peer); + uint32_t get_net_id(const ObjectID &p_id) const; + void set_net_id(const ObjectID &p_id, uint32_t p_net_id); + uint32_t ensure_net_id(const ObjectID &p_id); + + void reset(); + void on_peer_change(int p_peer, bool p_connected); + + Error config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner); + Error config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner); + + Error config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync); + Error config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync); + + Error peer_add_node(int p_peer, const ObjectID &p_id); + Error peer_del_node(int p_peer, const ObjectID &p_id); + + const HashMap<uint32_t, ObjectID> peer_get_remotes(int p_peer) const; + Node *peer_get_remote(int p_peer, uint32_t p_net_id); + Error peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner); + Error peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node); + + uint16_t peer_sync_next(int p_peer); + void peer_sync_recv(int p_peer, uint16_t p_time); + + SceneReplicationState() {} +}; + +#endif // SCENE_REPLICATON_STATE_H diff --git a/scene/property_utils.cpp b/scene/property_utils.cpp index 2540a633a9..a9b7e9acbe 100644 --- a/scene/property_utils.cpp +++ b/scene/property_utils.cpp @@ -130,7 +130,7 @@ Variant PropertyUtils::get_property_default_value(const Object *p_object, const if (p != -1 && p < prop_str.length() - 1) { bool all_digits = true; for (int i = p + 1; i < prop_str.length(); i++) { - if (prop_str[i] < '0' || prop_str[i] > '9') { + if (!is_digit(prop_str[i])) { all_digits = false; break; } diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 838cef824b..f19b018982 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -134,6 +134,9 @@ #include "scene/main/timer.h" #include "scene/main/viewport.h" #include "scene/main/window.h" +#include "scene/multiplayer/multiplayer_spawner.h" +#include "scene/multiplayer/multiplayer_synchronizer.h" +#include "scene/multiplayer/scene_replication_interface.h" #include "scene/resources/audio_stream_sample.h" #include "scene/resources/bit_map.h" #include "scene/resources/box_shape_3d.h" @@ -301,6 +304,8 @@ void register_scene_types() { GDREGISTER_CLASS(SubViewport); GDREGISTER_CLASS(ViewportTexture); GDREGISTER_CLASS(HTTPRequest); + GDREGISTER_CLASS(MultiplayerSpawner); + GDREGISTER_CLASS(MultiplayerSynchronizer); GDREGISTER_CLASS(Timer); GDREGISTER_CLASS(CanvasLayer); GDREGISTER_CLASS(CanvasModulate); @@ -822,6 +827,8 @@ void register_scene_types() { GDREGISTER_CLASS(Font); GDREGISTER_CLASS(Curve); + GDREGISTER_CLASS(SceneReplicationConfig); + GDREGISTER_CLASS(TextLine); GDREGISTER_CLASS(TextParagraph); @@ -1050,6 +1057,7 @@ void register_scene_types() { } SceneDebugger::initialize(); + SceneReplicationInterface::make_default(); NativeExtensionManager::get_singleton()->initialize_extensions(NativeExtension::INITIALIZATION_LEVEL_SCENE); } diff --git a/scene/resources/box_shape_3d.cpp b/scene/resources/box_shape_3d.cpp index b97d378e02..a1ec9a2230 100644 --- a/scene/resources/box_shape_3d.cpp +++ b/scene/resources/box_shape_3d.cpp @@ -95,5 +95,5 @@ void BoxShape3D::_bind_methods() { BoxShape3D::BoxShape3D() : Shape3D(PhysicsServer3D::get_singleton()->shape_create(PhysicsServer3D::SHAPE_BOX)) { - set_size(Vector3(2, 2, 2)); + set_size(Vector3(1, 1, 1)); } diff --git a/scene/resources/capsule_shape_3d.h b/scene/resources/capsule_shape_3d.h index 967a413da4..4c039ab326 100644 --- a/scene/resources/capsule_shape_3d.h +++ b/scene/resources/capsule_shape_3d.h @@ -35,8 +35,8 @@ class CapsuleShape3D : public Shape3D { GDCLASS(CapsuleShape3D, Shape3D); - float radius = 1.0; - float height = 3.0; + float radius = 0.5; + float height = 2.0; protected: static void _bind_methods(); diff --git a/scene/resources/cylinder_shape_3d.h b/scene/resources/cylinder_shape_3d.h index 0211f2b08f..65427423c8 100644 --- a/scene/resources/cylinder_shape_3d.h +++ b/scene/resources/cylinder_shape_3d.h @@ -35,7 +35,7 @@ class CylinderShape3D : public Shape3D { GDCLASS(CylinderShape3D, Shape3D); - float radius = 1.0; + float radius = 0.5; float height = 2.0; protected: diff --git a/scene/resources/primitive_meshes.h b/scene/resources/primitive_meshes.h index aa9682bd80..3fc5fd4a16 100644 --- a/scene/resources/primitive_meshes.h +++ b/scene/resources/primitive_meshes.h @@ -106,8 +106,8 @@ class CapsuleMesh : public PrimitiveMesh { GDCLASS(CapsuleMesh, PrimitiveMesh); private: - float radius = 1.0; - float height = 3.0; + float radius = 0.5; + float height = 2.0; int radial_segments = 64; int rings = 8; @@ -138,7 +138,7 @@ class BoxMesh : public PrimitiveMesh { GDCLASS(BoxMesh, PrimitiveMesh); private: - Vector3 size = Vector3(2.0, 2.0, 2.0); + Vector3 size = Vector3(1, 1, 1); int subdivide_w = 0; int subdivide_h = 0; int subdivide_d = 0; @@ -171,8 +171,8 @@ class CylinderMesh : public PrimitiveMesh { GDCLASS(CylinderMesh, PrimitiveMesh); private: - float top_radius = 1.0; - float bottom_radius = 1.0; + float top_radius = 0.5; + float bottom_radius = 0.5; float height = 2.0; int radial_segments = 64; int rings = 4; diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 1b81455d4c..6bb710b1d9 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -430,7 +430,7 @@ Error ResourceLoaderText::load() { } } - if (path.find("://") == -1 && path.is_relative_path()) { + if (!path.contains("://") && path.is_relative_path()) { // path is relative to file being loaded, so convert to a resource path path = ProjectSettings::get_singleton()->localize_path(local_path.get_base_dir().plus_file(path)); } @@ -774,7 +774,7 @@ void ResourceLoaderText::get_dependencies(FileAccess *p_f, List<String> *p_depen } } - if (!using_uid && path.find("://") == -1 && path.is_relative_path()) { + if (!using_uid && !path.contains("://") && path.is_relative_path()) { // path is relative to file being loaded, so convert to a resource path path = ProjectSettings::get_singleton()->localize_path(local_path.get_base_dir().plus_file(path)); } diff --git a/scene/resources/scene_replication_config.cpp b/scene/resources/scene_replication_config.cpp new file mode 100644 index 0000000000..2acc0f1922 --- /dev/null +++ b/scene/resources/scene_replication_config.cpp @@ -0,0 +1,187 @@ +/*************************************************************************/ +/* scene_replication_config.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 "scene_replication_config.h" + +#include "core/multiplayer/multiplayer_api.h" +#include "scene/main/node.h" + +bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_value) { + String name = p_name; + + if (name.begins_with("properties/")) { + int idx = name.get_slicec('/', 1).to_int(); + String what = name.get_slicec('/', 2); + + if (properties.size() == idx && what == "path") { + ERR_FAIL_COND_V(p_value.get_type() != Variant::NODE_PATH, false); + NodePath path = p_value; + ERR_FAIL_COND_V(path.is_empty() || path.get_subname_count() == 0, false); + add_property(path); + return true; + } + ERR_FAIL_COND_V(p_value.get_type() != Variant::BOOL, false); + ERR_FAIL_INDEX_V(idx, properties.size(), false); + ReplicationProperty &prop = properties[idx]; + if (what == "sync") { + prop.sync = p_value; + sync_props.push_back(prop.name); + return true; + } else if (what == "spawn") { + prop.spawn = p_value; + spawn_props.push_back(prop.name); + return true; + } + } + return false; +} + +bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) const { + String name = p_name; + + if (name.begins_with("properties/")) { + int idx = name.get_slicec('/', 1).to_int(); + String what = name.get_slicec('/', 2); + ERR_FAIL_INDEX_V(idx, properties.size(), false); + const ReplicationProperty &prop = properties[idx]; + if (what == "path") { + r_ret = prop.name; + return true; + } else if (what == "sync") { + r_ret = prop.sync; + return true; + } else if (what == "spawn") { + r_ret = prop.spawn; + return true; + } + } + return false; +} + +void SceneReplicationConfig::_get_property_list(List<PropertyInfo> *p_list) const { + for (int i = 0; i < properties.size(); i++) { + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/spawn", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + } +} + +TypedArray<NodePath> SceneReplicationConfig::get_properties() const { + TypedArray<NodePath> paths; + for (const ReplicationProperty &prop : properties) { + paths.push_back(prop.name); + } + return paths; +} + +void SceneReplicationConfig::add_property(const NodePath &p_path, int p_index) { + ERR_FAIL_COND(properties.find(p_path)); + + if (p_index < 0 || p_index == properties.size()) { + properties.push_back(ReplicationProperty(p_path)); + return; + } + + ERR_FAIL_INDEX(p_index, properties.size()); + + List<ReplicationProperty>::Element *I = properties.front(); + int c = 0; + while (c < p_index) { + I = I->next(); + c++; + } + properties.insert_before(I, ReplicationProperty(p_path)); +} + +void SceneReplicationConfig::remove_property(const NodePath &p_path) { + properties.erase(p_path); +} + +int SceneReplicationConfig::property_get_index(const NodePath &p_path) const { + for (int i = 0; i < properties.size(); i++) { + if (properties[i].name == p_path) { + return i; + } + } + ERR_FAIL_V(-1); +} + +bool SceneReplicationConfig::property_get_spawn(const NodePath &p_path) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND_V(!E, false); + return E->get().spawn; +} + +void SceneReplicationConfig::property_set_spawn(const NodePath &p_path, bool p_enabled) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND(!E); + if (E->get().spawn == p_enabled) { + return; + } + E->get().spawn = p_enabled; + spawn_props.clear(); + for (const ReplicationProperty &prop : properties) { + if (prop.spawn) { + spawn_props.push_back(p_path); + } + } +} + +bool SceneReplicationConfig::property_get_sync(const NodePath &p_path) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND_V(!E, false); + return E->get().sync; +} + +void SceneReplicationConfig::property_set_sync(const NodePath &p_path, bool p_enabled) { + List<ReplicationProperty>::Element *E = properties.find(p_path); + ERR_FAIL_COND(!E); + if (E->get().sync == p_enabled) { + return; + } + E->get().sync = p_enabled; + sync_props.clear(); + for (const ReplicationProperty &prop : properties) { + if (prop.sync) { + sync_props.push_back(p_path); + } + } +} + +void SceneReplicationConfig::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_properties"), &SceneReplicationConfig::get_properties); + ClassDB::bind_method(D_METHOD("add_property", "path", "index"), &SceneReplicationConfig::add_property, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("remove_property", "path"), &SceneReplicationConfig::remove_property); + ClassDB::bind_method(D_METHOD("property_get_index", "path"), &SceneReplicationConfig::property_get_index); + ClassDB::bind_method(D_METHOD("property_get_spawn", "path"), &SceneReplicationConfig::property_get_spawn); + ClassDB::bind_method(D_METHOD("property_set_spawn", "path", "enabled"), &SceneReplicationConfig::property_set_spawn); + ClassDB::bind_method(D_METHOD("property_get_sync", "path"), &SceneReplicationConfig::property_get_sync); + ClassDB::bind_method(D_METHOD("property_set_sync", "path", "enabled"), &SceneReplicationConfig::property_set_sync); +} diff --git a/scene/resources/scene_replication_config.h b/scene/resources/scene_replication_config.h new file mode 100644 index 0000000000..b791be9414 --- /dev/null +++ b/scene/resources/scene_replication_config.h @@ -0,0 +1,90 @@ +/*************************************************************************/ +/* scene_replication_config.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 SCENE_REPLICATION_CONFIG_H +#define SCENE_REPLICATION_CONFIG_H + +#include "core/io/resource.h" + +#include "core/variant/typed_array.h" + +class SceneReplicationConfig : public Resource { + GDCLASS(SceneReplicationConfig, Resource); + OBJ_SAVE_TYPE(SceneReplicationConfig); + RES_BASE_EXTENSION("repl"); + +private: + struct ReplicationProperty { + NodePath name; + bool spawn = true; + bool sync = true; + + bool operator==(const ReplicationProperty &p_to) { + return name == p_to.name; + } + + ReplicationProperty() {} + + ReplicationProperty(const NodePath &p_name) { + name = p_name; + } + }; + + List<ReplicationProperty> properties; + List<NodePath> spawn_props; + List<NodePath> sync_props; + +protected: + static void _bind_methods(); + + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + +public: + TypedArray<NodePath> get_properties() const; + + void add_property(const NodePath &p_path, int p_index = -1); + void remove_property(const NodePath &p_path); + + int property_get_index(const NodePath &p_path) const; + bool property_get_spawn(const NodePath &p_path); + void property_set_spawn(const NodePath &p_path, bool p_enabled); + + bool property_get_sync(const NodePath &p_path); + void property_set_sync(const NodePath &p_path, bool p_enabled); + + const List<NodePath> &get_spawn_properties() { return spawn_props; } + const List<NodePath> &get_sync_properties() { return sync_props; } + + SceneReplicationConfig() {} +}; + +#endif // SCENE_REPLICATION_CONFIG_H diff --git a/scene/resources/syntax_highlighter.cpp b/scene/resources/syntax_highlighter.cpp index e0aa21ac37..f1eddd8ffc 100644 --- a/scene/resources/syntax_highlighter.cpp +++ b/scene/resources/syntax_highlighter.cpp @@ -116,14 +116,6 @@ void SyntaxHighlighter::_bind_methods() { //////////////////////////////////////////////////////////////////////////////// -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - -static bool _is_hex_symbol(char32_t c) { - return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); -} - Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { Dictionary color_map; @@ -166,7 +158,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { color = font_color; bool is_char = !is_symbol(str[j]); bool is_a_symbol = is_symbol(str[j]); - bool is_number = (str[j] >= '0' && str[j] <= '9'); + bool is_number = is_digit(str[j]); /* color regions */ if (is_a_symbol || in_region != -1) { @@ -304,7 +296,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { } // Allow ABCDEF in hex notation. - if (is_hex_notation && (_is_hex_symbol(str[j]) || is_number)) { + if (is_hex_notation && (is_hex_digit(str[j]) || is_number)) { is_number = true; } else { is_hex_notation = false; @@ -321,7 +313,7 @@ Dictionary CodeHighlighter::_get_line_syntax_highlighting_impl(int p_line) { } } - if (!in_word && _is_char(str[j]) && !is_number) { + if (!in_word && (is_ascii_char(str[j]) || is_underscore(str[j])) && !is_number) { in_word = true; } diff --git a/scene/resources/theme.cpp b/scene/resources/theme.cpp index 8da287042e..f962e55666 100644 --- a/scene/resources/theme.cpp +++ b/scene/resources/theme.cpp @@ -46,7 +46,7 @@ int Theme::fallback_font_size = 16; bool Theme::_set(const StringName &p_name, const Variant &p_value) { String sname = p_name; - if (sname.find("/") != -1) { + if (sname.contains("/")) { String type = sname.get_slicec('/', 1); String theme_type = sname.get_slicec('/', 0); String name = sname.get_slicec('/', 2); @@ -78,7 +78,7 @@ bool Theme::_set(const StringName &p_name, const Variant &p_value) { bool Theme::_get(const StringName &p_name, Variant &r_ret) const { String sname = p_name; - if (sname.find("/") != -1) { + if (sname.contains("/")) { String type = sname.get_slicec('/', 1); String theme_type = sname.get_slicec('/', 0); String name = sname.get_slicec('/', 2); diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index f41058d59c..dd2f3d2202 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -1106,10 +1106,6 @@ String VisualShader::generate_preview_shader(Type p_type, int p_node, int p_port return final_code; } -#define IS_INITIAL_CHAR(m_d) (((m_d) >= 'a' && (m_d) <= 'z') || ((m_d) >= 'A' && (m_d) <= 'Z')) - -#define IS_SYMBOL_CHAR(m_d) (((m_d) >= 'a' && (m_d) <= 'z') || ((m_d) >= 'A' && (m_d) <= 'Z') || ((m_d) >= '0' && (m_d) <= '9') || (m_d) == '_') - String VisualShader::validate_port_name(const String &p_port_name, VisualShaderNode *p_node, int p_port_id, bool p_output) const { String name = p_port_name; @@ -1117,7 +1113,7 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN return String(); } - while (name.length() && !IS_INITIAL_CHAR(name[0])) { + while (name.length() && !is_ascii_char(name[0])) { name = name.substr(1, name.length() - 1); } @@ -1125,7 +1121,7 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN String valid_name; for (int i = 0; i < name.length(); i++) { - if (IS_SYMBOL_CHAR(name[i])) { + if (is_ascii_identifier_char(name[i])) { valid_name += String::chr(name[i]); } else if (name[i] == ' ') { valid_name += "_"; @@ -1162,14 +1158,14 @@ String VisualShader::validate_port_name(const String &p_port_name, VisualShaderN String VisualShader::validate_uniform_name(const String &p_name, const Ref<VisualShaderNodeUniform> &p_uniform) const { String name = p_name; //validate name first - while (name.length() && !IS_INITIAL_CHAR(name[0])) { + while (name.length() && !is_ascii_char(name[0])) { name = name.substr(1, name.length() - 1); } if (!name.is_empty()) { String valid_name; for (int i = 0; i < name.length(); i++) { - if (IS_SYMBOL_CHAR(name[i])) { + if (is_ascii_identifier_char(name[i])) { valid_name += String::chr(name[i]); } else if (name[i] == ' ') { valid_name += "_"; @@ -1206,7 +1202,7 @@ String VisualShader::validate_uniform_name(const String &p_name, const Ref<Visua if (exists) { //remove numbers, put new and try again attempt++; - while (name.length() && name[name.length() - 1] >= '0' && name[name.length() - 1] <= '9') { + while (name.length() && is_digit(name[name.length() - 1])) { name = name.substr(0, name.length() - 1); } ERR_FAIL_COND_V(name.is_empty(), String()); @@ -3312,7 +3308,7 @@ String VisualShaderNodeOutput::generate_code(Shader::Mode p_mode, VisualShader:: if (ports[idx].mode == shader_mode && ports[idx].shader_type == shader_type) { if (!p_input_vars[count].is_empty()) { String s = ports[idx].string; - if (s.find(":") != -1) { + if (s.contains(":")) { code += " " + s.get_slicec(':', 0) + " = " + p_input_vars[count] + "." + s.get_slicec(':', 1) + ";\n"; } else { code += " " + s + " = " + p_input_vars[count] + ";\n"; |