summaryrefslogtreecommitdiff
path: root/scene
diff options
context:
space:
mode:
Diffstat (limited to 'scene')
-rw-r--r--scene/SCsub1
-rw-r--r--scene/gui/code_edit.cpp26
-rw-r--r--scene/gui/text_edit.cpp30
-rw-r--r--scene/gui/texture_button.cpp100
-rw-r--r--scene/gui/texture_button.h8
-rw-r--r--scene/gui/tree.cpp2
-rw-r--r--scene/main/canvas_item.cpp9
-rw-r--r--scene/main/canvas_item.h2
-rw-r--r--scene/main/canvas_layer.cpp33
-rw-r--r--scene/main/canvas_layer.h4
-rw-r--r--scene/main/node.cpp12
-rw-r--r--scene/main/node.h3
-rw-r--r--scene/main/scene_tree.cpp1
-rw-r--r--scene/main/scene_tree.h2
-rw-r--r--scene/main/viewport.cpp2
-rw-r--r--scene/main/window.cpp1
-rw-r--r--scene/main/window.h1
-rw-r--r--scene/multiplayer/SCsub5
-rw-r--r--scene/multiplayer/multiplayer_spawner.cpp227
-rw-r--r--scene/multiplayer/multiplayer_spawner.h101
-rw-r--r--scene/multiplayer/multiplayer_synchronizer.cpp158
-rw-r--r--scene/multiplayer/multiplayer_synchronizer.h72
-rw-r--r--scene/multiplayer/scene_replication_interface.cpp415
-rw-r--r--scene/multiplayer/scene_replication_interface.h84
-rw-r--r--scene/multiplayer/scene_replication_state.cpp258
-rw-r--r--scene/multiplayer/scene_replication_state.h121
-rw-r--r--scene/property_utils.cpp2
-rw-r--r--scene/register_scene_types.cpp8
-rw-r--r--scene/resources/scene_replication_config.cpp187
-rw-r--r--scene/resources/scene_replication_config.h90
-rw-r--r--scene/resources/syntax_highlighter.cpp14
-rw-r--r--scene/resources/visual_shader.cpp14
32 files changed, 1864 insertions, 129 deletions
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/gui/code_edit.cpp b/scene/gui/code_edit.cpp
index 7e21a43ab6..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:
@@ -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/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/gui/texture_button.cpp b/scene/gui/texture_button.cpp
index 89a17ae854..26acfaaa70 100644
--- a/scene/gui/texture_button.cpp
+++ b/scene/gui/texture_button.cpp
@@ -37,7 +37,7 @@
Size2 TextureButton::get_minimum_size() const {
Size2 rscale = Control::get_minimum_size();
- if (!expand) {
+ if (!ignore_texture_size) {
if (normal.is_null()) {
if (pressed.is_null()) {
if (hover.is_null()) {
@@ -182,50 +182,48 @@ void TextureButton::_notification(int p_what) {
size = texdraw->get_size();
_texture_region = Rect2(Point2(), texdraw->get_size());
_tile = false;
- if (expand) {
- switch (stretch_mode) {
- case STRETCH_KEEP:
- size = texdraw->get_size();
- break;
- case STRETCH_SCALE:
- size = get_size();
- break;
- case STRETCH_TILE:
- size = get_size();
- _tile = true;
- break;
- case STRETCH_KEEP_CENTERED:
- ofs = (get_size() - texdraw->get_size()) / 2;
- size = texdraw->get_size();
- break;
- case STRETCH_KEEP_ASPECT_CENTERED:
- case STRETCH_KEEP_ASPECT: {
- Size2 _size = get_size();
- float tex_width = texdraw->get_width() * _size.height / texdraw->get_height();
- float tex_height = _size.height;
-
- if (tex_width > _size.width) {
- tex_width = _size.width;
- tex_height = texdraw->get_height() * tex_width / texdraw->get_width();
- }
+ switch (stretch_mode) {
+ case STRETCH_KEEP:
+ size = texdraw->get_size();
+ break;
+ case STRETCH_SCALE:
+ size = get_size();
+ break;
+ case STRETCH_TILE:
+ size = get_size();
+ _tile = true;
+ break;
+ case STRETCH_KEEP_CENTERED:
+ ofs = (get_size() - texdraw->get_size()) / 2;
+ size = texdraw->get_size();
+ break;
+ case STRETCH_KEEP_ASPECT_CENTERED:
+ case STRETCH_KEEP_ASPECT: {
+ Size2 _size = get_size();
+ float tex_width = texdraw->get_width() * _size.height / texdraw->get_height();
+ float tex_height = _size.height;
+
+ if (tex_width > _size.width) {
+ tex_width = _size.width;
+ tex_height = texdraw->get_height() * tex_width / texdraw->get_width();
+ }
- if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) {
- ofs.x = (_size.width - tex_width) / 2;
- ofs.y = (_size.height - tex_height) / 2;
- }
- size.width = tex_width;
- size.height = tex_height;
- } break;
- case STRETCH_KEEP_ASPECT_COVERED: {
- size = get_size();
- Size2 tex_size = texdraw->get_size();
- Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height);
- float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height;
- Size2 scaled_tex_size = tex_size * scale;
- Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f;
- _texture_region = Rect2(ofs2, size / scale);
- } break;
- }
+ if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) {
+ ofs.x = (_size.width - tex_width) / 2;
+ ofs.y = (_size.height - tex_height) / 2;
+ }
+ size.width = tex_width;
+ size.height = tex_height;
+ } break;
+ case STRETCH_KEEP_ASPECT_COVERED: {
+ size = get_size();
+ Size2 tex_size = texdraw->get_size();
+ Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height);
+ float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height;
+ Size2 scaled_tex_size = tex_size * scale;
+ Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f;
+ _texture_region = Rect2(ofs2, size / scale);
+ } break;
}
_position_rect = Rect2(ofs, size);
@@ -258,7 +256,7 @@ void TextureButton::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_disabled_texture", "texture"), &TextureButton::set_disabled_texture);
ClassDB::bind_method(D_METHOD("set_focused_texture", "texture"), &TextureButton::set_focused_texture);
ClassDB::bind_method(D_METHOD("set_click_mask", "mask"), &TextureButton::set_click_mask);
- ClassDB::bind_method(D_METHOD("set_expand", "expand"), &TextureButton::set_expand);
+ ClassDB::bind_method(D_METHOD("set_ignore_texture_size", "ignore"), &TextureButton::set_ignore_texture_size);
ClassDB::bind_method(D_METHOD("set_stretch_mode", "mode"), &TextureButton::set_stretch_mode);
ClassDB::bind_method(D_METHOD("set_flip_h", "enable"), &TextureButton::set_flip_h);
ClassDB::bind_method(D_METHOD("is_flipped_h"), &TextureButton::is_flipped_h);
@@ -271,7 +269,7 @@ void TextureButton::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_disabled_texture"), &TextureButton::get_disabled_texture);
ClassDB::bind_method(D_METHOD("get_focused_texture"), &TextureButton::get_focused_texture);
ClassDB::bind_method(D_METHOD("get_click_mask"), &TextureButton::get_click_mask);
- ClassDB::bind_method(D_METHOD("get_expand"), &TextureButton::get_expand);
+ ClassDB::bind_method(D_METHOD("get_ignore_texture_size"), &TextureButton::get_ignore_texture_size);
ClassDB::bind_method(D_METHOD("get_stretch_mode"), &TextureButton::get_stretch_mode);
ADD_GROUP("Textures", "texture_");
@@ -281,7 +279,7 @@ void TextureButton::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_disabled", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_disabled_texture", "get_disabled_texture");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_focused", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_focused_texture", "get_focused_texture");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_click_mask", PROPERTY_HINT_RESOURCE_TYPE, "BitMap"), "set_click_mask", "get_click_mask");
- ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_expand", "get_expand");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "ignore_texture_size", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_ignore_texture_size", "get_ignore_texture_size");
ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Scale,Tile,Keep,Keep Centered,Keep Aspect,Keep Aspect Centered,Keep Aspect Covered"), "set_stretch_mode", "get_stretch_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_h", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_h", "is_flipped_h");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_v", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_v", "is_flipped_v");
@@ -352,12 +350,12 @@ void TextureButton::set_focused_texture(const Ref<Texture2D> &p_focused) {
focused = p_focused;
};
-bool TextureButton::get_expand() const {
- return expand;
+bool TextureButton::get_ignore_texture_size() const {
+ return ignore_texture_size;
}
-void TextureButton::set_expand(bool p_expand) {
- expand = p_expand;
+void TextureButton::set_ignore_texture_size(bool p_ignore) {
+ ignore_texture_size = p_ignore;
update_minimum_size();
update();
}
diff --git a/scene/gui/texture_button.h b/scene/gui/texture_button.h
index 1428a79a1d..5762949acd 100644
--- a/scene/gui/texture_button.h
+++ b/scene/gui/texture_button.h
@@ -54,8 +54,8 @@ private:
Ref<Texture2D> disabled;
Ref<Texture2D> focused;
Ref<BitMap> click_mask;
- bool expand = false;
- StretchMode stretch_mode = STRETCH_SCALE;
+ bool ignore_texture_size = false;
+ StretchMode stretch_mode = STRETCH_KEEP;
Rect2 _texture_region;
Rect2 _position_rect;
@@ -85,8 +85,8 @@ public:
Ref<Texture2D> get_focused_texture() const;
Ref<BitMap> get_click_mask() const;
- bool get_expand() const;
- void set_expand(bool p_expand);
+ bool get_ignore_texture_size() const;
+ void set_ignore_texture_size(bool p_ignore);
void set_stretch_mode(StretchMode p_stretch_mode);
StretchMode get_stretch_mode() const;
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index 1b32884880..a190e08088 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -2490,7 +2490,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int
/* process selection */
if (p_double_click && (!c.editable || c.mode == TreeItem::CELL_MODE_CUSTOM || c.mode == TreeItem::CELL_MODE_ICON /*|| c.mode==TreeItem::CELL_MODE_CHECK*/)) { //it's confusing for check
-
+ // Emits the "item_activated" signal.
propagate_mouse_activated = true;
incr_search.clear();
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index a0916c6291..a62bbb146c 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -72,6 +72,15 @@ bool CanvasItem::is_visible_in_tree() const {
p = p->get_parent_item();
}
+ const Node *n = get_parent();
+ while (n) {
+ const CanvasLayer *c = Object::cast_to<CanvasLayer>(n);
+ if (c && !c->is_visible()) {
+ return false;
+ }
+ n = n->get_parent();
+ }
+
return true;
}
diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h
index 3d49d89746..08fea52c3a 100644
--- a/scene/main/canvas_item.h
+++ b/scene/main/canvas_item.h
@@ -46,6 +46,8 @@ class World2D;
class CanvasItem : public Node {
GDCLASS(CanvasItem, Node);
+ friend class CanvasLayer;
+
public:
enum TextureFilter {
TEXTURE_FILTER_PARENT_NODE,
diff --git a/scene/main/canvas_layer.cpp b/scene/main/canvas_layer.cpp
index 282ab6b497..3f3e72357b 100644
--- a/scene/main/canvas_layer.cpp
+++ b/scene/main/canvas_layer.cpp
@@ -29,6 +29,7 @@
/*************************************************************************/
#include "canvas_layer.h"
+#include "canvas_item.h"
#include "viewport.h"
void CanvasLayer::set_layer(int p_xform) {
@@ -42,6 +43,32 @@ int CanvasLayer::get_layer() const {
return layer;
}
+void CanvasLayer::set_visible(bool p_visible) {
+ if (p_visible == visible) {
+ return;
+ }
+
+ visible = p_visible;
+ emit_signal(SNAME("visibility_changed"));
+
+ for (int i = 0; i < get_child_count(); i++) {
+ CanvasItem *c = Object::cast_to<CanvasItem>(get_child(i));
+ if (c) {
+ RenderingServer::get_singleton()->canvas_item_set_visible(c->get_canvas_item(), p_visible && c->is_visible());
+
+ if (c->is_visible()) {
+ c->_propagate_visibility_changed(p_visible);
+ } else {
+ c->notification(CanvasItem::NOTIFICATION_VISIBILITY_CHANGED);
+ }
+ }
+ }
+}
+
+bool CanvasLayer::is_visible() const {
+ return visible;
+}
+
void CanvasLayer::set_transform(const Transform2D &p_xform) {
transform = p_xform;
locrotscale_dirty = true;
@@ -264,6 +291,9 @@ void CanvasLayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_layer", "layer"), &CanvasLayer::set_layer);
ClassDB::bind_method(D_METHOD("get_layer"), &CanvasLayer::get_layer);
+ ClassDB::bind_method(D_METHOD("set_visible", "visible"), &CanvasLayer::set_visible);
+ ClassDB::bind_method(D_METHOD("is_visible"), &CanvasLayer::is_visible);
+
ClassDB::bind_method(D_METHOD("set_transform", "transform"), &CanvasLayer::set_transform);
ClassDB::bind_method(D_METHOD("get_transform"), &CanvasLayer::get_transform);
@@ -289,6 +319,7 @@ void CanvasLayer::_bind_methods() {
ADD_GROUP("Layer", "");
ADD_PROPERTY(PropertyInfo(Variant::INT, "layer", PROPERTY_HINT_RANGE, "-128,128,1"), "set_layer", "get_layer");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible");
ADD_GROUP("Transform", "");
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rotation", PROPERTY_HINT_RANGE, "-1080,1080,0.1,or_lesser,or_greater,radians"), "set_rotation", "get_rotation");
@@ -299,6 +330,8 @@ void CanvasLayer::_bind_methods() {
ADD_GROUP("Follow Viewport", "follow_viewport");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "follow_viewport_enable"), "set_follow_viewport", "is_following_viewport");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "follow_viewport_scale", PROPERTY_HINT_RANGE, "0.001,1000,0.001,or_greater,or_lesser"), "set_follow_viewport_scale", "get_follow_viewport_scale");
+
+ ADD_SIGNAL(MethodInfo("visibility_changed"));
}
CanvasLayer::CanvasLayer() {
diff --git a/scene/main/canvas_layer.h b/scene/main/canvas_layer.h
index 93a0152787..b7bd793440 100644
--- a/scene/main/canvas_layer.h
+++ b/scene/main/canvas_layer.h
@@ -52,6 +52,7 @@ class CanvasLayer : public Node {
Viewport *vp = nullptr;
int sort_index = 0;
+ bool visible = true;
bool follow_viewport = false;
float follow_viewport_scale = 1.0;
@@ -69,6 +70,9 @@ public:
void set_layer(int p_xform);
int get_layer() const;
+ void set_visible(bool p_visible);
+ bool is_visible() const;
+
void set_transform(const Transform2D &p_xform);
Transform2D get_transform() const;
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index 2665a5695b..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;
@@ -1061,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;
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/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/visual_shader.cpp b/scene/resources/visual_shader.cpp
index 0c7f069004..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());