diff --git a/doc/classes/DataBuffer.xml b/doc/classes/DataBuffer.xml
new file mode 100644
index 000000000000..554564cdf402
--- /dev/null
+++ b/doc/classes/DataBuffer.xml
@@ -0,0 +1,324 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/classes/Interpolator.xml b/doc/classes/Interpolator.xml
new file mode 100644
index 000000000000..5020ff0b0e69
--- /dev/null
+++ b/doc/classes/Interpolator.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/classes/NetworkedController.xml b/doc/classes/NetworkedController.xml
new file mode 100644
index 000000000000..67a075a401c3
--- /dev/null
+++ b/doc/classes/NetworkedController.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 7c6d6d1c1013..e29d2cdf344d 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -193,6 +193,10 @@
+
+
+
+
Background color for the boot splash.
diff --git a/doc/classes/SceneDiff.xml b/doc/classes/SceneDiff.xml
new file mode 100644
index 000000000000..877c08a6b26a
--- /dev/null
+++ b/doc/classes/SceneDiff.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/classes/SceneSynchronizer.xml b/doc/classes/SceneSynchronizer.xml
new file mode 100644
index 000000000000..353f89ec1117
--- /dev/null
+++ b/doc/classes/SceneSynchronizer.xml
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/network_synchronizer/SCsub b/modules/network_synchronizer/SCsub
new file mode 100644
index 000000000000..2574314086d8
--- /dev/null
+++ b/modules/network_synchronizer/SCsub
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+
+Import("env")
+Import("env_modules")
+
+env_network_synchronizer = env_modules.Clone()
+
+env_network_synchronizer.add_source_files(env.modules_sources, "*.cpp")
diff --git a/modules/network_synchronizer/bit_array.cpp b/modules/network_synchronizer/bit_array.cpp
new file mode 100644
index 000000000000..f22a1da0c227
--- /dev/null
+++ b/modules/network_synchronizer/bit_array.cpp
@@ -0,0 +1,134 @@
+/*************************************************************************/
+/* bit_array.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "bit_array.h"
+
+#include "core/math/math_funcs.h"
+#include "core/string/ustring.h"
+
+BitArray::BitArray(uint32_t p_initial_size_in_bit) {
+ resize_in_bits(p_initial_size_in_bit);
+}
+
+BitArray::BitArray(const Vector &p_bytes) :
+ bytes(p_bytes) {
+}
+
+void BitArray::resize_in_bytes(int p_bytes) {
+ ERR_FAIL_COND_MSG(p_bytes < 0, "Bytes count can't be negative");
+ bytes.resize(p_bytes);
+}
+
+int BitArray::size_in_bytes() const {
+ return bytes.size();
+}
+
+void BitArray::resize_in_bits(int p_bits) {
+ ERR_FAIL_COND_MSG(p_bits < 0, "Bits count can't be negative");
+ const int min_size = Math::ceil((static_cast(p_bits)) / 8);
+ bytes.resize(min_size);
+}
+
+int BitArray::size_in_bits() const {
+ return bytes.size() * 8;
+}
+
+void BitArray::store_bits(int p_bit_offset, uint64_t p_value, int p_bits) {
+ ERR_FAIL_COND_MSG(p_bit_offset < 0, "Offset can't be negative");
+ ERR_FAIL_COND_MSG(p_bits <= 0, "The number of bits should be more than 0");
+ ERR_FAIL_INDEX_MSG(p_bit_offset + p_bits - 1, size_in_bits(), "The bit array size is `" + itos(size_in_bits()) + "` while you are trying to write `" + itos(p_bits) + "` starting from `" + itos(p_bit_offset) + "`.");
+
+ int bits = p_bits;
+ int bit_offset = p_bit_offset;
+ uint64_t val = p_value;
+
+ while (bits > 0) {
+ const int bits_to_write = MIN(bits, 8 - bit_offset % 8);
+ const int bits_to_jump = bit_offset % 8;
+ const int bits_to_skip = 8 - (bits_to_write + bits_to_jump);
+ const int byte_offset = bit_offset / 8;
+
+ // Clear the bits that we have to write
+ //const uint8_t byte_clear = ~(((0xFF >> bits_to_jump) << (bits_to_jump + bits_to_skip)) >> bits_to_skip);
+ uint8_t byte_clear = 0xFF >> bits_to_jump;
+ byte_clear = byte_clear << (bits_to_jump + bits_to_skip);
+ byte_clear = ~(byte_clear >> bits_to_skip);
+ bytes.write[byte_offset] &= byte_clear;
+
+ // Now we can continue to write bits
+ bytes.write[byte_offset] |= (val & 0xFF) << bits_to_jump;
+
+ bits -= bits_to_write;
+ bit_offset += bits_to_write;
+
+ val >>= bits_to_write;
+ }
+}
+
+uint64_t BitArray::read_bits(int p_bit_offset, int p_bits) const {
+ ERR_FAIL_COND_V_MSG(p_bits <= 0, 0, "The number of bits should be more than 0");
+ ERR_FAIL_INDEX_V_MSG(p_bit_offset + p_bits - 1, size_in_bits(), 0, "The bit array size is `" + itos(size_in_bits()) + "` while you are trying to read `" + itos(p_bits) + "` starting from `" + itos(p_bit_offset) + "`.");
+
+ int bits = p_bits;
+ int bit_offset = p_bit_offset;
+ uint64_t val = 0;
+
+ const uint8_t *bytes_ptr = bytes.ptr();
+
+ int val_bits_to_jump = 0;
+ while (bits > 0) {
+ const int bits_to_read = MIN(bits, 8 - bit_offset % 8);
+ const int bits_to_jump = bit_offset % 8;
+ const int bits_to_skip = 8 - (bits_to_read + bits_to_jump);
+ const int byte_offset = bit_offset / 8;
+
+ uint8_t byte_mask = 0xFF >> bits_to_jump;
+ byte_mask = byte_mask << (bits_to_skip + bits_to_jump);
+ byte_mask = byte_mask >> bits_to_skip;
+ const uint64_t byte_val = static_cast((bytes_ptr[byte_offset] & byte_mask) >> bits_to_jump);
+ val |= byte_val << val_bits_to_jump;
+
+ bits -= bits_to_read;
+ bit_offset += bits_to_read;
+ val_bits_to_jump += bits_to_read;
+ }
+
+ return val;
+}
+
+void BitArray::zero() {
+ if (bytes.size() > 0) {
+ memset(bytes.ptrw(), 0, sizeof(uint8_t) * bytes.size());
+ }
+}
diff --git a/modules/network_synchronizer/bit_array.h b/modules/network_synchronizer/bit_array.h
new file mode 100644
index 000000000000..53f01b154b10
--- /dev/null
+++ b/modules/network_synchronizer/bit_array.h
@@ -0,0 +1,69 @@
+/*************************************************************************/
+/* bit_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "core/templates/vector.h"
+
+#ifndef BITARRAY_H
+#define BITARRAY_H
+
+class BitArray {
+ Vector bytes;
+
+public:
+ BitArray() = default;
+ BitArray(uint32_t p_initial_size_in_bit);
+ BitArray(const Vector &p_bytes);
+
+ const Vector &get_bytes() const {
+ return bytes;
+ }
+
+ Vector &get_bytes_mut() {
+ return bytes;
+ }
+
+ void resize_in_bytes(int p_bits);
+ int size_in_bytes() const;
+
+ void resize_in_bits(int p_bits);
+ int size_in_bits() const;
+
+ void store_bits(int p_bit_offset, uint64_t p_value, int p_bits);
+ uint64_t read_bits(int p_bit_offset, int p_bits) const;
+
+ // Puts all the bytes to 0.
+ void zero();
+};
+
+#endif
diff --git a/modules/network_synchronizer/config.py b/modules/network_synchronizer/config.py
new file mode 100644
index 000000000000..d3d2b347870c
--- /dev/null
+++ b/modules/network_synchronizer/config.py
@@ -0,0 +1,18 @@
+def can_build(env, platform):
+ return True
+
+
+def configure(env):
+ pass
+
+
+def get_doc_classes():
+ return []
+
+
+def get_doc_path():
+ return ""
+
+
+def is_enabled():
+ return True
diff --git a/modules/network_synchronizer/data_buffer.cpp b/modules/network_synchronizer/data_buffer.cpp
new file mode 100644
index 000000000000..b5fd14f30720
--- /dev/null
+++ b/modules/network_synchronizer/data_buffer.cpp
@@ -0,0 +1,942 @@
+/*************************************************************************/
+/* data_buffer.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "data_buffer.h"
+
+#include "core/io/marshalls.h"
+
+// TODO improve the allocation mechanism.
+
+void DataBuffer::_bind_methods() {
+ BIND_ENUM_CONSTANT(DATA_TYPE_BOOL);
+ BIND_ENUM_CONSTANT(DATA_TYPE_INT);
+ BIND_ENUM_CONSTANT(DATA_TYPE_REAL);
+ BIND_ENUM_CONSTANT(DATA_TYPE_UNIT_REAL);
+ BIND_ENUM_CONSTANT(DATA_TYPE_VECTOR2);
+ BIND_ENUM_CONSTANT(DATA_TYPE_NORMALIZED_VECTOR2);
+ BIND_ENUM_CONSTANT(DATA_TYPE_VECTOR3);
+ BIND_ENUM_CONSTANT(DATA_TYPE_NORMALIZED_VECTOR3);
+
+ BIND_ENUM_CONSTANT(COMPRESSION_LEVEL_0);
+ BIND_ENUM_CONSTANT(COMPRESSION_LEVEL_1);
+ BIND_ENUM_CONSTANT(COMPRESSION_LEVEL_2);
+ BIND_ENUM_CONSTANT(COMPRESSION_LEVEL_3);
+
+ ClassDB::bind_method(D_METHOD("size"), &DataBuffer::size);
+
+ ClassDB::bind_method(D_METHOD("add_bool", "value"), &DataBuffer::add_bool);
+ ClassDB::bind_method(D_METHOD("add_int", "value", "compression_level"), &DataBuffer::add_int, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_real", "value", "compression_level"), &DataBuffer::add_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_positive_unit_real", "value", "compression_level"), &DataBuffer::add_positive_unit_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_unit_real", "value", "compression_level"), &DataBuffer::add_unit_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_vector2", "value", "compression_level"), &DataBuffer::add_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_normalized_vector2", "value", "compression_level"), &DataBuffer::add_normalized_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_vector3", "value", "compression_level"), &DataBuffer::add_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_normalized_vector3", "value", "compression_level"), &DataBuffer::add_normalized_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("add_variant", "value"), &DataBuffer::add_variant);
+
+ ClassDB::bind_method(D_METHOD("read_bool"), &DataBuffer::read_bool);
+ ClassDB::bind_method(D_METHOD("read_int", "compression_level"), &DataBuffer::read_int, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_real", "compression_level"), &DataBuffer::read_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_unit_real", "compression_level"), &DataBuffer::read_unit_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_vector2", "compression_level"), &DataBuffer::read_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_normalized_vector2", "compression_level"), &DataBuffer::read_normalized_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_vector3", "compression_level"), &DataBuffer::read_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_normalized_vector3", "compression_level"), &DataBuffer::read_normalized_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_variant"), &DataBuffer::read_variant);
+
+ ClassDB::bind_method(D_METHOD("skip_bool"), &DataBuffer::skip_bool);
+ ClassDB::bind_method(D_METHOD("skip_int", "compression_level"), &DataBuffer::skip_int, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_real", "compression_level"), &DataBuffer::skip_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_unit_real", "compression_level"), &DataBuffer::skip_unit_real, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_vector2", "compression_level"), &DataBuffer::skip_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_normalized_vector2", "compression_level"), &DataBuffer::skip_normalized_vector2, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_vector3", "compression_level"), &DataBuffer::skip_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("skip_normalized_vector3", "compression_level"), &DataBuffer::skip_normalized_vector3, DEFVAL(COMPRESSION_LEVEL_1));
+
+ ClassDB::bind_method(D_METHOD("get_bool_size"), &DataBuffer::get_bool_size);
+ ClassDB::bind_method(D_METHOD("get_int_size", "compression_level"), &DataBuffer::get_int_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_real_size", "compression_level"), &DataBuffer::get_real_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_unit_real_size", "compression_level"), &DataBuffer::get_unit_real_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_vector2_size", "compression_level"), &DataBuffer::get_vector2_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_normalized_vector2_size", "compression_level"), &DataBuffer::get_normalized_vector2_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_vector3_size", "compression_level"), &DataBuffer::get_vector3_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("get_normalized_vector3_size", "compression_level"), &DataBuffer::get_normalized_vector3_size, DEFVAL(COMPRESSION_LEVEL_1));
+
+ ClassDB::bind_method(D_METHOD("read_bool_size"), &DataBuffer::read_bool_size);
+ ClassDB::bind_method(D_METHOD("read_int_size", "compression_level"), &DataBuffer::read_int_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_real_size", "compression_level"), &DataBuffer::read_real_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_unit_real_size", "compression_level"), &DataBuffer::read_unit_real_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_vector2_size", "compression_level"), &DataBuffer::read_vector2_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_normalized_vector2_size", "compression_level"), &DataBuffer::read_normalized_vector2_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_vector3_size", "compression_level"), &DataBuffer::read_vector3_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_normalized_vector3_size", "compression_level"), &DataBuffer::read_normalized_vector3_size, DEFVAL(COMPRESSION_LEVEL_1));
+ ClassDB::bind_method(D_METHOD("read_variant_size"), &DataBuffer::read_variant_size);
+
+ ClassDB::bind_method(D_METHOD("begin_read"), &DataBuffer::begin_read);
+ ClassDB::bind_method(D_METHOD("begin_write", "meta_size"), &DataBuffer::begin_write);
+ ClassDB::bind_method(D_METHOD("dry"), &DataBuffer::dry);
+}
+
+DataBuffer::DataBuffer(const DataBuffer &p_other) :
+ Object(),
+ metadata_size(p_other.metadata_size),
+ bit_offset(p_other.bit_offset),
+ bit_size(p_other.bit_size),
+ is_reading(p_other.is_reading),
+ buffer(p_other.buffer) {}
+
+DataBuffer::DataBuffer(const BitArray &p_buffer) :
+ Object(),
+ bit_size(p_buffer.size_in_bits()),
+ is_reading(true),
+ buffer(p_buffer) {}
+
+void DataBuffer::begin_write(int p_metadata_size) {
+ CRASH_COND_MSG(p_metadata_size < 0, "Metadata size can't be negative");
+ metadata_size = p_metadata_size;
+ bit_size = 0;
+ bit_offset = 0;
+ is_reading = false;
+}
+
+void DataBuffer::dry() {
+ buffer.resize_in_bits(metadata_size + bit_size);
+}
+
+void DataBuffer::seek(int p_bits) {
+ ERR_FAIL_INDEX(p_bits, metadata_size + bit_size + 1);
+ bit_offset = p_bits;
+}
+
+void DataBuffer::shrink_to(int p_metadata_bit_size, int p_bit_size) {
+ CRASH_COND_MSG(p_metadata_bit_size < 0, "Metadata size can't be negative");
+ ERR_FAIL_COND_MSG(p_bit_size < 0, "Bit size can't be negative");
+ ERR_FAIL_COND_MSG(buffer.size_in_bits() < (p_metadata_bit_size + p_bit_size), "The buffer is smaller than the new given size.");
+ metadata_size = p_metadata_bit_size;
+ bit_size = p_bit_size;
+}
+
+int DataBuffer::get_metadata_size() const {
+ return metadata_size;
+}
+
+int DataBuffer::size() const {
+ return bit_size;
+}
+
+int DataBuffer::total_size() const {
+ return bit_size + metadata_size;
+}
+
+int DataBuffer::get_bit_offset() const {
+ return bit_offset;
+}
+
+void DataBuffer::skip(int p_bits) {
+ ERR_FAIL_COND((metadata_size + bit_size) < (bit_offset + p_bits));
+ bit_offset += p_bits;
+}
+
+void DataBuffer::begin_read() {
+ bit_offset = 0;
+ is_reading = true;
+}
+
+bool DataBuffer::add_bool(bool p_input) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const int bits = get_bit_taken(DATA_TYPE_BOOL, COMPRESSION_LEVEL_0);
+
+ make_room_in_bits(bits);
+ buffer.store_bits(bit_offset, p_input, bits);
+ bit_offset += bits;
+
+#ifdef DEBUG_ENABLED
+ // Can't never happen because the buffer size is correctly handled.
+ CRASH_COND((metadata_size + bit_size) > buffer.size_in_bits() && bit_offset > buffer.size_in_bits());
+#endif
+
+ return p_input;
+}
+
+bool DataBuffer::read_bool() {
+ ERR_FAIL_COND_V(is_reading == false, false);
+
+ const int bits = get_bit_taken(DATA_TYPE_BOOL, COMPRESSION_LEVEL_0);
+ const bool d = buffer.read_bits(bit_offset, bits);
+ bit_offset += bits;
+ return d;
+}
+
+int64_t DataBuffer::add_int(int64_t p_input, CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const int bits = get_bit_taken(DATA_TYPE_INT, p_compression_level);
+
+ int64_t value = p_input;
+
+ // Clamp the value to the max that the bit can store.
+ if (bits == 8) {
+ value = CLAMP(value, INT8_MIN, INT8_MAX);
+ } else if (bits == 16) {
+ value = CLAMP(value, INT16_MIN, INT16_MAX);
+ } else if (bits == 32) {
+ value = CLAMP(value, INT32_MIN, INT32_MAX);
+ } else {
+ // Nothing to do here
+ }
+
+ make_room_in_bits(bits);
+ buffer.store_bits(bit_offset, value, bits);
+ bit_offset += bits;
+
+#ifdef DEBUG_ENABLED
+ // Can't never happen because the buffer size is correctly handled.
+ CRASH_COND((metadata_size + bit_size) > buffer.size_in_bits() && bit_offset > buffer.size_in_bits());
+#endif
+
+ if (bits == 8) {
+ return static_cast(value);
+ } else if (bits == 16) {
+ return static_cast(value);
+ } else if (bits == 32) {
+ return static_cast(value);
+ } else {
+ return value;
+ }
+}
+
+int64_t DataBuffer::read_int(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, 0);
+
+ const int bits = get_bit_taken(DATA_TYPE_INT, p_compression_level);
+
+ const uint64_t value = buffer.read_bits(bit_offset, bits);
+ bit_offset += bits;
+
+ if (bits == 8) {
+ return static_cast(value);
+ } else if (bits == 16) {
+ return static_cast(value);
+ } else if (bits == 32) {
+ return static_cast(value);
+ } else {
+ return static_cast(value);
+ }
+}
+
+double DataBuffer::add_real(double p_input, CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ // Clamp the input value according to the compression level
+ // Minifloat (compression level 0) have a special bias
+ const int exponent_bits = get_exponent_bits(p_compression_level);
+ const int mantissa_bits = get_mantissa_bits(p_compression_level);
+ const double bias = p_compression_level == COMPRESSION_LEVEL_3 ? Math::pow(2.0, exponent_bits) - 3 : Math::pow(2.0, exponent_bits - 1) - 1;
+ const double max_value = (2.0 - Math::pow(2.0, -(mantissa_bits - 1))) * Math::pow(2.0, bias);
+ const double clamped_input = CLAMP(p_input, -max_value, max_value);
+
+ // Split number according to IEEE 754 binary format.
+ // Mantissa floating point value represented in range (-1;-0.5], [0.5; 1).
+ int exponent;
+ double mantissa = frexp(clamped_input, &exponent);
+
+ // Extract sign.
+ const bool sign = mantissa < 0;
+ mantissa = Math::abs(mantissa);
+
+ // Round mantissa into the specified number of bits (like float -> double conversion).
+ double mantissa_scale = Math::pow(2.0, mantissa_bits);
+ if (exponent <= 0) {
+ // Subnormal value, apply exponent to mantissa and reduce power of scale by one.
+ mantissa *= Math::pow(2.0, exponent);
+ exponent = 0;
+ mantissa_scale /= 2.0;
+ }
+ mantissa = Math::round(mantissa * mantissa_scale) / mantissa_scale; // Round to specified number of bits.
+ if (mantissa < 0.5 && mantissa != 0) {
+ // Check underflow, extract exponent from mantissa.
+ exponent += ilogb(mantissa) + 1;
+ mantissa /= Math::pow(2.0, exponent);
+ } else if (mantissa == 1) {
+ // Check overflow, increment the exponent.
+ ++exponent;
+ mantissa = 0.5;
+ }
+ // Convert the mantissa to an integer that represents the offset index (IEE 754 floating point representation) to send over network safely.
+ const uint64_t integer_mantissa = exponent <= 0 ? mantissa * mantissa_scale * Math::pow(2.0, exponent) : (mantissa - 0.5) * mantissa_scale;
+
+ make_room_in_bits(mantissa_bits + exponent_bits);
+ buffer.store_bits(bit_offset, sign, 1);
+ bit_offset += 1;
+ buffer.store_bits(bit_offset, integer_mantissa, mantissa_bits - 1);
+ bit_offset += mantissa_bits - 1;
+ // Send unsigned value (just shift it by bias) to avoid sign issues.
+ buffer.store_bits(bit_offset, exponent + bias, exponent_bits);
+ bit_offset += exponent_bits;
+
+ return ldexp(sign ? -mantissa : mantissa, exponent);
+}
+
+double DataBuffer::read_real(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, 0.0);
+
+ const bool sign = buffer.read_bits(bit_offset, 1);
+ bit_offset += 1;
+
+ const int mantissa_bits = get_mantissa_bits(p_compression_level);
+ const uint64_t integer_mantissa = buffer.read_bits(bit_offset, mantissa_bits - 1);
+ bit_offset += mantissa_bits - 1;
+
+ const int exponent_bits = get_exponent_bits(p_compression_level);
+ const double bias = p_compression_level == COMPRESSION_LEVEL_3 ? Math::pow(2.0, exponent_bits) - 3 : Math::pow(2.0, exponent_bits - 1) - 1;
+ int exponent = static_cast(buffer.read_bits(bit_offset, exponent_bits)) - static_cast(bias);
+ bit_offset += exponent_bits;
+
+ // Convert integer mantissa into the floating point representation
+ // When the index of the mantissa and exponent are 0, then this is a special case and the mantissa is 0.
+ const double mantissa_scale = Math::pow(2.0, exponent <= 0 ? mantissa_bits - 1 : mantissa_bits);
+ const double mantissa = exponent <= 0 ? integer_mantissa / mantissa_scale / Math::pow(2.0, exponent) : integer_mantissa / mantissa_scale + 0.5;
+
+ return ldexp(sign ? -mantissa : mantissa, exponent);
+}
+
+real_t DataBuffer::add_positive_unit_real(real_t p_input, CompressionLevel p_compression_level) {
+#ifdef DEBUG_ENABLED
+ ERR_FAIL_COND_V_MSG(p_input < 0 || p_input > 1, p_input, "Value must be between zero and one.");
+#endif
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const int bits = get_bit_taken(DATA_TYPE_UNIT_REAL, p_compression_level);
+
+ const double max_value = static_cast(~(UINT64_MAX << bits));
+
+ const uint64_t compressed_val = compress_unit_float(p_input, max_value);
+
+ make_room_in_bits(bits);
+ buffer.store_bits(bit_offset, compressed_val, bits);
+ bit_offset += bits;
+
+#ifdef DEBUG_ENABLED
+ // Can't never happen because the buffer size is correctly handled.
+ CRASH_COND((metadata_size + bit_size) > buffer.size_in_bits() && bit_offset > buffer.size_in_bits());
+#endif
+
+ return decompress_unit_float(compressed_val, max_value);
+}
+
+real_t DataBuffer::read_positive_unit_real(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, 0.0);
+
+ const int bits = get_bit_taken(DATA_TYPE_UNIT_REAL, p_compression_level);
+
+ const double max_value = static_cast(~(UINT64_MAX << bits));
+
+ const uint64_t compressed_val = buffer.read_bits(bit_offset, bits);
+ bit_offset += bits;
+
+ return decompress_unit_float(compressed_val, max_value);
+}
+
+real_t DataBuffer::add_unit_real(real_t p_input, CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const real_t added_real = add_positive_unit_real(ABS(p_input), p_compression_level);
+
+ const int bits_for_sign = 1;
+ const uint32_t is_negative = p_input < 0.0;
+ make_room_in_bits(bits_for_sign);
+ buffer.store_bits(bit_offset, is_negative, bits_for_sign);
+ bit_offset += bits_for_sign;
+
+#ifdef DEBUG_ENABLED
+ // Can't never happen because the buffer size is correctly handled.
+ CRASH_COND((metadata_size + bit_size) > buffer.size_in_bits() && bit_offset > buffer.size_in_bits());
+#endif
+
+ return is_negative ? -added_real : added_real;
+}
+
+real_t DataBuffer::read_unit_real(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, 0.0);
+
+ const real_t value = read_positive_unit_real(p_compression_level);
+
+ const int bits_for_sign = 1;
+ const bool is_negative = buffer.read_bits(bit_offset, bits_for_sign);
+ bit_offset += bits_for_sign;
+
+ return is_negative ? -value : value;
+}
+
+Vector2 DataBuffer::add_vector2(Vector2 p_input, CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+#ifndef REAL_T_IS_DOUBLE
+ // Fallback to compression level 1 if real_t is float
+ if (p_compression_level == DataBuffer::COMPRESSION_LEVEL_0) {
+ WARN_PRINT_ONCE("Compression level 0 is not supported for Vector2 for a binary compiled with single precision float. Falling back to compression level 1");
+ p_compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+#endif
+
+ Vector2 r;
+ r[0] = add_real(p_input[0], p_compression_level);
+ r[1] = add_real(p_input[1], p_compression_level);
+ return r;
+}
+
+Vector2 DataBuffer::read_vector2(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, Vector2());
+
+#ifndef REAL_T_IS_DOUBLE
+ // Fallback to compression level 1 if real_t is float
+ if (p_compression_level == DataBuffer::COMPRESSION_LEVEL_0) {
+ WARN_PRINT_ONCE("Compression level 0 is not supported for Vector2 for a binary compiled with single precision float. Falling back to compression level 1");
+ p_compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+#endif
+
+ Vector2 r;
+ r[0] = read_real(p_compression_level);
+ r[1] = read_real(p_compression_level);
+ return r;
+}
+
+Vector2 DataBuffer::add_normalized_vector2(Vector2 p_input, CompressionLevel p_compression_level) {
+#ifdef DEBUG_ENABLED
+ ERR_FAIL_COND_V(p_input.is_normalized() == false, p_input);
+#endif
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const int bits = get_bit_taken(DATA_TYPE_NORMALIZED_VECTOR2, p_compression_level);
+ const int bits_for_the_angle = bits - 1;
+ const int bits_for_zero = 1;
+
+ const double angle = p_input.angle();
+ const uint32_t is_not_zero = p_input.length_squared() > CMP_EPSILON;
+
+ const double max_value = static_cast(~(UINT64_MAX << bits_for_the_angle));
+
+ const uint64_t compressed_angle = compress_unit_float((angle + Math_PI) / Math_TAU, max_value);
+
+ make_room_in_bits(bits);
+ buffer.store_bits(bit_offset, is_not_zero, bits_for_zero);
+ buffer.store_bits(bit_offset + 1, compressed_angle, bits_for_the_angle);
+ bit_offset += bits;
+
+ const real_t decompressed_angle = (decompress_unit_float(compressed_angle, max_value) * Math_TAU) - Math_PI;
+ const real_t x = Math::cos(decompressed_angle);
+ const real_t y = Math::sin(decompressed_angle);
+
+#ifdef DEBUG_ENABLED
+ // Can't never happen because the buffer size is correctly handled.
+ CRASH_COND((metadata_size + bit_size) > buffer.size_in_bits() && bit_offset > buffer.size_in_bits());
+#endif
+
+ return Vector2(x, y) * is_not_zero;
+}
+
+Vector2 DataBuffer::read_normalized_vector2(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, Vector2());
+
+ const int bits = get_bit_taken(DATA_TYPE_NORMALIZED_VECTOR2, p_compression_level);
+ const int bits_for_the_angle = bits - 1;
+ const int bits_for_zero = 1;
+
+ const double max_value = static_cast(~(UINT64_MAX << bits_for_the_angle));
+
+ const real_t is_not_zero = buffer.read_bits(bit_offset, bits_for_zero);
+ const uint64_t compressed_angle = buffer.read_bits(bit_offset + 1, bits_for_the_angle);
+ bit_offset += bits;
+
+ const real_t decompressed_angle = (decompress_unit_float(compressed_angle, max_value) * Math_TAU) - Math_PI;
+ const real_t x = Math::cos(decompressed_angle);
+ const real_t y = Math::sin(decompressed_angle);
+
+ return Vector2(x, y) * is_not_zero;
+}
+
+Vector3 DataBuffer::add_vector3(Vector3 p_input, CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+#ifndef REAL_T_IS_DOUBLE
+ // Fallback to compression level 1 if real_t is float
+ if (p_compression_level == DataBuffer::COMPRESSION_LEVEL_0) {
+ WARN_PRINT_ONCE("Compression level 0 is not supported for Vector3 for a binary compiled with single precision float. Falling back to compression level 1");
+ p_compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+#endif
+
+ Vector3 r;
+ r[0] = add_real(p_input[0], p_compression_level);
+ r[1] = add_real(p_input[1], p_compression_level);
+ r[2] = add_real(p_input[2], p_compression_level);
+ return r;
+}
+
+Vector3 DataBuffer::read_vector3(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, Vector3());
+
+#ifndef REAL_T_IS_DOUBLE
+ // Fallback to compression level 1 if real_t is float
+ if (p_compression_level == DataBuffer::COMPRESSION_LEVEL_0) {
+ WARN_PRINT_ONCE("Compression level 0 is not supported for Vector3 for a binary compiled with single precision float. Falling back to compression level 1");
+ p_compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+#endif
+
+ Vector3 r;
+ r[0] = read_real(p_compression_level);
+ r[1] = read_real(p_compression_level);
+ r[2] = read_real(p_compression_level);
+ return r;
+}
+
+Vector3 DataBuffer::add_normalized_vector3(Vector3 p_input, CompressionLevel p_compression_level) {
+#ifdef DEBUG_ENABLED
+ ERR_FAIL_COND_V(p_input.is_normalized() == false, p_input);
+#endif
+ ERR_FAIL_COND_V(is_reading == true, p_input);
+
+ const real_t x_axis = add_unit_real(p_input.x, p_compression_level);
+ const real_t y_axis = add_unit_real(p_input.y, p_compression_level);
+ const real_t z_axis = add_unit_real(p_input.z, p_compression_level);
+
+ return Vector3(x_axis, y_axis, z_axis);
+}
+
+Vector3 DataBuffer::read_normalized_vector3(CompressionLevel p_compression_level) {
+ ERR_FAIL_COND_V(is_reading == false, Vector3());
+
+ const real_t x_axis = read_unit_real(p_compression_level);
+ const real_t y_axis = read_unit_real(p_compression_level);
+ const real_t z_axis = read_unit_real(p_compression_level);
+
+ return Vector3(x_axis, y_axis, z_axis);
+}
+
+Variant DataBuffer::add_variant(const Variant &p_input) {
+ // TODO consider to use a method similar to `_encode_and_compress_variant`
+ // to compress the encoded data a bit.
+
+ // Get the variant size.
+ int len = 0;
+
+ const Error len_err = encode_variant(
+ p_input,
+ nullptr,
+ len,
+ false);
+
+ ERR_FAIL_COND_V_MSG(
+ len_err != OK,
+ Variant(),
+ "Was not possible encode the variant.");
+
+ // Variant encoding pads the data to byte, so doesn't make sense write it
+ // unpadded.
+ make_room_pad_to_next_byte();
+ make_room_in_bits(len * 8);
+
+#ifdef DEBUG_ENABLED
+ // This condition is always false thanks to the `make_room_pad_to_next_byte`.
+ // so it's safe to assume we are starting from the begin of the byte.
+ CRASH_COND((bit_offset % 8) != 0);
+#endif
+
+ const Error write_err = encode_variant(
+ p_input,
+ buffer.get_bytes_mut().ptrw() + (bit_offset / 8),
+ len,
+ false);
+
+ ERR_FAIL_COND_V_MSG(
+ write_err != OK,
+ Variant(),
+ "Was not possible encode the variant.");
+
+ bit_offset += len * 8;
+
+ return p_input;
+}
+
+Variant DataBuffer::read_variant() {
+ Variant ret;
+
+ int len = 0;
+
+ // The Variant is always written starting from the beginning of the byte.
+ const bool success = pad_to_next_byte();
+ ERR_FAIL_COND_V_MSG(success == false, Variant(), "Padding failed.");
+
+#ifdef DEBUG_ENABLED
+ // This condition is always false thanks to the `pad_to_next_byte`; So is
+ // safe to assume we are starting from the begin of the byte.
+ CRASH_COND((bit_offset % 8) != 0);
+#endif
+
+ const Error read_err = decode_variant(
+ ret,
+ buffer.get_bytes().ptr() + (bit_offset / 8),
+ buffer.size_in_bytes() - (bit_offset / 8),
+ &len,
+ false);
+
+ ERR_FAIL_COND_V_MSG(
+ read_err != OK,
+ Variant(),
+ "Was not possible decode the variant.");
+
+ bit_offset += len * 8;
+
+ return ret;
+}
+
+void DataBuffer::zero() {
+ buffer.zero();
+}
+
+void DataBuffer::skip_bool() {
+ const int bits = get_bool_size();
+ skip(bits);
+}
+
+void DataBuffer::skip_int(CompressionLevel p_compression) {
+ const int bits = get_int_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_real(CompressionLevel p_compression) {
+ const int bits = get_real_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_unit_real(CompressionLevel p_compression) {
+ const int bits = get_unit_real_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_vector2(CompressionLevel p_compression) {
+ const int bits = get_vector2_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_normalized_vector2(CompressionLevel p_compression) {
+ const int bits = get_normalized_vector2_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_vector3(CompressionLevel p_compression) {
+ const int bits = get_vector3_size(p_compression);
+ skip(bits);
+}
+
+void DataBuffer::skip_normalized_vector3(CompressionLevel p_compression) {
+ const int bits = get_normalized_vector3_size(p_compression);
+ skip(bits);
+}
+
+int DataBuffer::get_bool_size() const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_BOOL, COMPRESSION_LEVEL_0);
+}
+
+int DataBuffer::get_int_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_INT, p_compression);
+}
+
+int DataBuffer::get_real_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_REAL, p_compression);
+}
+
+int DataBuffer::get_unit_real_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_UNIT_REAL, p_compression);
+}
+
+int DataBuffer::get_vector2_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_VECTOR2, p_compression);
+}
+
+int DataBuffer::get_normalized_vector2_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_NORMALIZED_VECTOR2, p_compression);
+}
+
+int DataBuffer::get_vector3_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_VECTOR3, p_compression);
+}
+
+int DataBuffer::get_normalized_vector3_size(CompressionLevel p_compression) const {
+ return DataBuffer::get_bit_taken(DATA_TYPE_NORMALIZED_VECTOR3, p_compression);
+}
+
+int DataBuffer::read_bool_size() {
+ const int bits = get_bool_size();
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_int_size(CompressionLevel p_compression) {
+ const int bits = get_int_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_real_size(CompressionLevel p_compression) {
+ const int bits = get_real_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_unit_real_size(CompressionLevel p_compression) {
+ const int bits = get_unit_real_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_vector2_size(CompressionLevel p_compression) {
+ const int bits = get_vector2_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_normalized_vector2_size(CompressionLevel p_compression) {
+ const int bits = get_normalized_vector2_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_vector3_size(CompressionLevel p_compression) {
+ const int bits = get_vector3_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_normalized_vector3_size(CompressionLevel p_compression) {
+ const int bits = get_normalized_vector3_size(p_compression);
+ skip(bits);
+ return bits;
+}
+
+int DataBuffer::read_variant_size() {
+ int len = 0;
+
+ Variant ret;
+
+ // The Variant is always written starting from the beginning of the byte.
+ const bool success = pad_to_next_byte();
+ ERR_FAIL_COND_V_MSG(success == false, Variant(), "Padding failed.");
+
+#ifdef DEBUG_ENABLED
+ // This condition is always false thanks to the `pad_to_next_byte`; So is
+ // safe to assume we are starting from the begin of the byte.
+ CRASH_COND((bit_offset % 8) != 0);
+#endif
+
+ const Error read_err = decode_variant(
+ ret,
+ buffer.get_bytes().ptr() + (bit_offset / 8),
+ buffer.size_in_bytes() - (bit_offset / 8),
+ &len,
+ false);
+
+ ERR_FAIL_COND_V_MSG(
+ read_err != OK,
+ 0,
+ "Was not possible to decode the variant, error: " + itos(read_err));
+
+ bit_offset += len * 8;
+
+ return len * 8;
+}
+
+int DataBuffer::get_bit_taken(DataType p_data_type, CompressionLevel p_compression) {
+ switch (p_data_type) {
+ case DATA_TYPE_BOOL:
+ // No matter what, 1 bit.
+ return 1;
+ case DATA_TYPE_INT: {
+ switch (p_compression) {
+ case COMPRESSION_LEVEL_0:
+ return 64;
+ case COMPRESSION_LEVEL_1:
+ return 32;
+ case COMPRESSION_LEVEL_2:
+ return 16;
+ case COMPRESSION_LEVEL_3:
+ return 8;
+ default:
+ // Unreachable
+ CRASH_NOW_MSG("Compression level not supported!");
+ }
+ } break;
+ case DATA_TYPE_REAL: {
+ return get_mantissa_bits(p_compression) +
+ get_exponent_bits(p_compression);
+ } break;
+ case DATA_TYPE_POSITIVE_UNIT_REAL: {
+ switch (p_compression) {
+ case COMPRESSION_LEVEL_0:
+ return 10;
+ case COMPRESSION_LEVEL_1:
+ return 8;
+ case COMPRESSION_LEVEL_2:
+ return 6;
+ case COMPRESSION_LEVEL_3:
+ return 4;
+ default:
+ // Unreachable
+ CRASH_NOW_MSG("Compression level not supported!");
+ }
+ } break;
+ case DATA_TYPE_UNIT_REAL: {
+ return get_bit_taken(DATA_TYPE_POSITIVE_UNIT_REAL, p_compression) + 1;
+ } break;
+ case DATA_TYPE_VECTOR2: {
+ return get_bit_taken(DATA_TYPE_REAL, p_compression) * 2;
+ } break;
+ case DATA_TYPE_NORMALIZED_VECTOR2: {
+ // +1 bit to know if the vector is 0 or a direction
+ switch (p_compression) {
+ case CompressionLevel::COMPRESSION_LEVEL_0:
+ return 11 + 1;
+ case CompressionLevel::COMPRESSION_LEVEL_1:
+ return 10 + 1;
+ case CompressionLevel::COMPRESSION_LEVEL_2:
+ return 9 + 1;
+ case CompressionLevel::COMPRESSION_LEVEL_3:
+ return 8 + 1;
+ }
+ } break;
+ case DATA_TYPE_VECTOR3: {
+ return get_bit_taken(DATA_TYPE_REAL, p_compression) * 3;
+ } break;
+ case DATA_TYPE_NORMALIZED_VECTOR3: {
+ switch (p_compression) {
+ case CompressionLevel::COMPRESSION_LEVEL_0:
+ return 11 * 3;
+ case CompressionLevel::COMPRESSION_LEVEL_1:
+ return 10 * 3;
+ case CompressionLevel::COMPRESSION_LEVEL_2:
+ return 8 * 3;
+ case CompressionLevel::COMPRESSION_LEVEL_3:
+ return 6 * 3;
+ }
+ } break;
+ case DATA_TYPE_VARIANT: {
+ ERR_FAIL_V_MSG(0, "The variant size is dynamic and can't be know at compile time.");
+ }
+ default:
+ // Unreachable
+ CRASH_NOW_MSG("Input type not supported!");
+ }
+
+ // Unreachable
+ CRASH_NOW_MSG("It was not possible to obtain the bit taken by this input data.");
+ return 0; // Useless, but MS CI is too noisy.
+}
+
+int DataBuffer::get_mantissa_bits(CompressionLevel p_compression) {
+ // https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats
+ switch (p_compression) {
+ case CompressionLevel::COMPRESSION_LEVEL_0:
+ return 53; // Binary64 format
+ case CompressionLevel::COMPRESSION_LEVEL_1:
+ return 24; // Binary32 format
+ case CompressionLevel::COMPRESSION_LEVEL_2:
+ return 11; // Binary16 format
+ case CompressionLevel::COMPRESSION_LEVEL_3:
+ return 4; // https://en.wikipedia.org/wiki/Minifloat
+ }
+
+ // Unreachable
+ CRASH_NOW_MSG("Unknown compression level.");
+ return 0; // Useless, but MS CI is too noisy.
+}
+
+int DataBuffer::get_exponent_bits(CompressionLevel p_compression) {
+ // https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats
+ switch (p_compression) {
+ case CompressionLevel::COMPRESSION_LEVEL_0:
+ return 11; // Binary64 format
+ case CompressionLevel::COMPRESSION_LEVEL_1:
+ return 8; // Binary32 format
+ case CompressionLevel::COMPRESSION_LEVEL_2:
+ return 5; // Binary16 format
+ case CompressionLevel::COMPRESSION_LEVEL_3:
+ return 4; // https://en.wikipedia.org/wiki/Minifloat
+ }
+
+ // Unreachable
+ CRASH_NOW_MSG("Unknown compression level.");
+ return 0; // Useless, but MS CI is too noisy.
+}
+
+uint64_t DataBuffer::compress_unit_float(double p_value, double p_scale_factor) {
+ return Math::round(MIN(p_value * p_scale_factor, p_scale_factor));
+}
+
+double DataBuffer::decompress_unit_float(uint64_t p_value, double p_scale_factor) {
+ return static_cast(p_value) / p_scale_factor;
+}
+
+void DataBuffer::make_room_in_bits(int p_dim) {
+ const int array_min_dim = bit_offset + p_dim;
+ if (array_min_dim > buffer.size_in_bits()) {
+ buffer.resize_in_bits(array_min_dim);
+ }
+
+ if (array_min_dim > metadata_size) {
+ const int new_bit_size = array_min_dim - metadata_size;
+ if (new_bit_size > bit_size) {
+ bit_size = new_bit_size;
+ }
+ }
+}
+
+void DataBuffer::make_room_pad_to_next_byte() {
+ const int bits_to_next_byte = ((bit_offset + 7) & ~7) - bit_offset;
+ make_room_in_bits(bits_to_next_byte);
+ bit_offset += bits_to_next_byte;
+}
+
+bool DataBuffer::pad_to_next_byte() {
+ const int bits_to_next_byte = ((bit_offset + 7) & ~7) - bit_offset;
+ ERR_FAIL_COND_V_MSG(
+ bit_offset + bits_to_next_byte > buffer.size_in_bits(),
+ false,
+ "");
+ bit_offset += bits_to_next_byte;
+ return true;
+}
diff --git a/modules/network_synchronizer/data_buffer.h b/modules/network_synchronizer/data_buffer.h
new file mode 100644
index 000000000000..01c0f12a5cf3
--- /dev/null
+++ b/modules/network_synchronizer/data_buffer.h
@@ -0,0 +1,342 @@
+/*************************************************************************/
+/* data_buffer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "core/object/class_db.h"
+
+#include "bit_array.h"
+
+#ifndef INPUT_BUFFER_H
+#define INPUT_BUFFER_H
+
+class DataBuffer : public Object {
+ GDCLASS(DataBuffer, Object);
+
+public:
+ enum DataType {
+ DATA_TYPE_BOOL,
+ DATA_TYPE_INT,
+ DATA_TYPE_REAL,
+ DATA_TYPE_POSITIVE_UNIT_REAL,
+ DATA_TYPE_UNIT_REAL,
+ DATA_TYPE_VECTOR2,
+ DATA_TYPE_NORMALIZED_VECTOR2,
+ DATA_TYPE_VECTOR3,
+ DATA_TYPE_NORMALIZED_VECTOR3,
+ // The only dynamic sized value.
+ DATA_TYPE_VARIANT
+ };
+
+ /// Compression level for the stored input data.
+ ///
+ /// Depending on the data type and the compression level used the amount of
+ /// bits used and loss change.
+ ///
+ ///
+ /// ## Bool
+ /// Always use 1 bit
+ ///
+ ///
+ /// ## Int
+ /// COMPRESSION_LEVEL_0: 64 bits are used - Stores integers -9223372036854775808 / 9223372036854775807
+ /// COMPRESSION_LEVEL_1: 32 bits are used - Stores integers -2147483648 / 2147483647
+ /// COMPRESSION_LEVEL_2: 16 bits are used - Stores integers -32768 / 32767
+ /// COMPRESSION_LEVEL_3: 8 bits are used - Stores integers -128 / 127
+ ///
+ ///
+ /// ## Real
+ /// Precision depends on an integer range
+ /// COMPRESSION_LEVEL_0: 64 bits are used - Double precision. Up to 16 precision is 0.00000000000000177636 in worst case. Up to 512 precision is 0.00000000000005684342 in worst case. Up to 1024 precision is 0.00000000000011368684 in worst case.
+ /// COMPRESSION_LEVEL_1: 32 bits are used - Single precision (float). Up to 16 precision is 0.00000095367431640625 in worst case. Up to 512 precision is 0.000030517578125 in worst case. Up to 1024 precision is 0.00006103515625 in worst case.
+ /// COMPRESSION_LEVEL_2: 16 bits are used - Half precision. Up to 16 precision is 0.0078125 in worst case. Up to 512 precision is 0.25 in worst case. Up to 1024 precision is 0.5.
+ /// COMPRESSION_LEVEL_3: 8 bits are used - Minifloat: Up to 2 precision is 0.125. Up to 4 precision is 0.25. Up to 8 precision is 0.5.
+ ///
+ /// To get the exact precision for the stored number, you need to find the lower power of two relative to the number and divide it by 2^mantissa_bits.
+ /// To get the mantissa or exponent bits for a specific compression level, you can use the get_mantissa_bits and get_exponent_bits functions.
+ ///
+ ///
+ /// ## Positive unit real
+ /// COMPRESSION_LEVEL_0: 10 bits are used - Max loss ~0.005%
+ /// COMPRESSION_LEVEL_1: 8 bits are used - Max loss ~0.020%
+ /// COMPRESSION_LEVEL_2: 6 bits are used - Max loss ~0.793%
+ /// COMPRESSION_LEVEL_3: 4 bits are used - Max loss ~3.333%
+ ///
+ ///
+ /// ## Unit real (uses one extra bit for the sign)
+ /// COMPRESSION_LEVEL_0: 11 bits are used - Max loss ~0.005%
+ /// COMPRESSION_LEVEL_1: 9 bits are used - Max loss ~0.020%
+ /// COMPRESSION_LEVEL_2: 7 bits are used - Max loss ~0.793%
+ /// COMPRESSION_LEVEL_3: 5 bits are used - Max loss ~3.333%
+ ///
+ ///
+ /// ## Vector2
+ /// COMPRESSION_LEVEL_0: 2 * 64 bits are used - Double precision (will fallback to level 1 if REAL_T_IS_DOUBLE is not defined)
+ /// COMPRESSION_LEVEL_1: 2 * 32 bits are used - Single precision
+ /// COMPRESSION_LEVEL_2: 2 * 16 bits are used - Half precision
+ /// COMPRESSION_LEVEL_3: 2 * 8 bits are used - Minifloat
+ ///
+ /// For floating point precision, check the Real compression section.
+ ///
+ ///
+ /// ## Normalized Vector2
+ /// COMPRESSION_LEVEL_0: 12 bits are used - Max loss 0.17°
+ /// COMPRESSION_LEVEL_1: 11 bits are used - Max loss 0.35°
+ /// COMPRESSION_LEVEL_2: 10 bits are used - Max loss 0.7°
+ /// COMPRESSION_LEVEL_3: 9 bits are used - Max loss 1.1°
+ ///
+ ///
+ /// ## Vector3
+ /// COMPRESSION_LEVEL_0: 3 * 64 bits are used - Double precision (will fallback to level 1 if REAL_T_IS_DOUBLE is not defined)
+ /// COMPRESSION_LEVEL_1: 3 * 32 bits are used - Single precision
+ /// COMPRESSION_LEVEL_2: 3 * 16 bits are used - Half precision
+ /// COMPRESSION_LEVEL_3: 3 * 8 bits are used - Minifloat
+ ///
+ /// For floating point precision, check the Real compression section.
+ ///
+ ///
+ /// ## Normalized Vector3
+ /// COMPRESSION_LEVEL_0: 11 * 3 bits are used - Max loss ~0.005% per axis
+ /// COMPRESSION_LEVEL_1: 9 * 3 bits are used - Max loss ~0.020% per axis
+ /// COMPRESSION_LEVEL_2: 7 * 3 bits are used - Max loss ~0.793% per axis
+ /// COMPRESSION_LEVEL_3: 5 * 3 bits are used - Max loss ~3.333% per axis
+ ///
+ /// ## Variant
+ /// It's dynamic sized. It's not possible to compress it.
+ enum CompressionLevel {
+ COMPRESSION_LEVEL_0,
+ COMPRESSION_LEVEL_1,
+ COMPRESSION_LEVEL_2,
+ COMPRESSION_LEVEL_3
+ };
+
+private:
+ int metadata_size = 0;
+ int bit_offset = 0;
+ int bit_size = 0;
+ bool is_reading = false;
+ BitArray buffer;
+
+public:
+ static void _bind_methods();
+
+ DataBuffer() = default;
+ DataBuffer(const DataBuffer &p_other);
+ DataBuffer(const BitArray &p_buffer);
+
+ const BitArray &get_buffer() const {
+ return buffer;
+ }
+
+ BitArray &get_buffer_mut() {
+ return buffer;
+ }
+
+ /// Begin write.
+ void begin_write(int p_metadata_size);
+
+ /// Make sure the buffer takes least space possible.
+ void dry();
+
+ /// Seek the offset to a specific bit. Seek to a bit greater than the actual
+ /// size is not allowed.
+ void seek(int p_bits);
+
+ /// Set the bit size and the metadata size.
+ void shrink_to(int p_metadata_bit_size, int p_bit_size);
+
+ /// Returns the metadata size in bits.
+ int get_metadata_size() const;
+ /// Returns the buffer size in bits
+ int size() const;
+ /// Total size in bits.
+ int total_size() const;
+
+ /// Returns the bit offset.
+ int get_bit_offset() const;
+
+ /// Skip n bits.
+ void skip(int p_bits);
+
+ /// Begin read.
+ void begin_read();
+
+ /// Add a boolean to the buffer.
+ /// Returns the same data.
+ bool add_bool(bool p_input);
+
+ /// Parse the next data as boolean.
+ bool read_bool();
+
+ /// Add the next data as int.
+ int64_t add_int(int64_t p_input, CompressionLevel p_compression_level);
+
+ /// Parse the next data as int.
+ int64_t read_int(CompressionLevel p_compression_level);
+
+ /// Add a real into the buffer. Depending on the compression level is possible
+ /// to store different range level.
+ /// The fractional part has a precision of ~0.3%
+ ///
+ /// Returns the compressed value so both the client and the peers can use
+ /// the same data.
+ double add_real(double p_input, CompressionLevel p_compression_level);
+
+ /// Parse the following data as a real.
+ double read_real(CompressionLevel p_compression_level);
+
+ /// Add a positive unit real into the buffer.
+ ///
+ /// **Note:** Not unitary values lead to unexpected behaviour.
+ ///
+ /// Returns the compressed value so both the client and the peers can use
+ /// the same data.
+ real_t add_positive_unit_real(real_t p_input, CompressionLevel p_compression_level);
+
+ /// Parse the following data as a positive unit real.
+ real_t read_positive_unit_real(CompressionLevel p_compression_level);
+
+ /// Add a unit real into the buffer.
+ ///
+ /// **Note:** Not unitary values lead to unexpected behaviour.
+ ///
+ /// Returns the compressed value so both the client and the peers can use
+ /// the same data.
+ real_t add_unit_real(real_t p_input, CompressionLevel p_compression_level);
+
+ /// Parse the following data as an unit real.
+ real_t read_unit_real(CompressionLevel p_compression_level);
+
+ /// Add a vector2 into the buffer.
+ /// Note: This kind of vector occupies more space than the normalized verison.
+ /// Consider use a normalized vector to save bandwidth if possible.
+ ///
+ /// Returns the decompressed vector so both the client and the peers can use
+ /// the same data.
+ Vector2 add_vector2(Vector2 p_input, CompressionLevel p_compression_level);
+
+ /// Parse next data as vector from the input buffer.
+ Vector2 read_vector2(CompressionLevel p_compression_level);
+
+ /// Add a normalized vector2 into the buffer.
+ /// Note: The compression algorithm rely on the fact that this is a
+ /// normalized vector. The behaviour is unexpected for not normalized vectors.
+ ///
+ /// Returns the decompressed vector so both the client and the peers can use
+ /// the same data.
+ Vector2 add_normalized_vector2(Vector2 p_input, CompressionLevel p_compression_level);
+
+ /// Parse next data as normalized vector from the input buffer.
+ Vector2 read_normalized_vector2(CompressionLevel p_compression_level);
+
+ /// Add a vector3 into the buffer.
+ /// Note: This kind of vector occupies more space than the normalized verison.
+ /// Consider use a normalized vector to save bandwidth if possible.
+ ///
+ /// Returns the decompressed vector so both the client and the peers can use
+ /// the same data.
+ Vector3 add_vector3(Vector3 p_input, CompressionLevel p_compression_level);
+
+ /// Parse next data as vector3 from the input buffer.
+ Vector3 read_vector3(CompressionLevel p_compression_level);
+
+ /// Add a normalized vector3 into the buffer.
+ /// Note: The compression algorithm rely on the fact that this is a
+ /// normalized vector. The behaviour is unexpected for not normalized vectors.
+ ///
+ /// Returns the decompressed vector so both the client and the peers can use
+ /// the same data.
+ Vector3 add_normalized_vector3(Vector3 p_input, CompressionLevel p_compression_level);
+
+ /// Parse next data as normalized vector3 from the input buffer.
+ Vector3 read_normalized_vector3(CompressionLevel p_compression_level);
+
+ /// Add a variant. This is the only supported dynamic sized value.
+ Variant add_variant(const Variant &p_input);
+
+ /// Parse the next data as Variant and returns it.
+ Variant read_variant();
+
+ /// Puts all the bytes to 0.
+ void zero();
+
+ /** Skips the amount of bits a type takes. */
+
+ void skip_bool();
+ void skip_int(CompressionLevel p_compression);
+ void skip_real(CompressionLevel p_compression);
+ void skip_unit_real(CompressionLevel p_compression);
+ void skip_vector2(CompressionLevel p_compression);
+ void skip_normalized_vector2(CompressionLevel p_compression);
+ void skip_vector3(CompressionLevel p_compression);
+ void skip_normalized_vector3(CompressionLevel p_compression);
+
+ /** Just returns the size of a specific type. */
+
+ int get_bool_size() const;
+ int get_int_size(CompressionLevel p_compression) const;
+ int get_real_size(CompressionLevel p_compression) const;
+ int get_unit_real_size(CompressionLevel p_compression) const;
+ int get_vector2_size(CompressionLevel p_compression) const;
+ int get_normalized_vector2_size(CompressionLevel p_compression) const;
+ int get_vector3_size(CompressionLevel p_compression) const;
+ int get_normalized_vector3_size(CompressionLevel p_compression) const;
+
+ /** Read the size and pass to the next parameter. */
+
+ int read_bool_size();
+ int read_int_size(CompressionLevel p_compression);
+ int read_real_size(CompressionLevel p_compression);
+ int read_unit_real_size(CompressionLevel p_compression);
+ int read_vector2_size(CompressionLevel p_compression);
+ int read_normalized_vector2_size(CompressionLevel p_compression);
+ int read_vector3_size(CompressionLevel p_compression);
+ int read_normalized_vector3_size(CompressionLevel p_compression);
+ int read_variant_size();
+
+ static int get_bit_taken(DataType p_data_type, CompressionLevel p_compression);
+ static int get_mantissa_bits(CompressionLevel p_compression);
+ static int get_exponent_bits(CompressionLevel p_compression);
+
+private:
+ static uint64_t compress_unit_float(double p_value, double p_scale_factor);
+ static double decompress_unit_float(uint64_t p_value, double p_scale_factor);
+
+ void make_room_in_bits(int p_dim);
+ void make_room_pad_to_next_byte();
+ bool pad_to_next_byte();
+};
+
+VARIANT_ENUM_CAST(DataBuffer::DataType)
+VARIANT_ENUM_CAST(DataBuffer::CompressionLevel)
+
+#endif
diff --git a/modules/network_synchronizer/interpolator.cpp b/modules/network_synchronizer/interpolator.cpp
new file mode 100644
index 000000000000..2b3f2a87cfb0
--- /dev/null
+++ b/modules/network_synchronizer/interpolator.cpp
@@ -0,0 +1,415 @@
+/*************************************************************************/
+/* interpolator.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+// TODO write unit tests to make sure all cases are covered.
+
+#include "interpolator.h"
+
+#include "core/string/ustring.h"
+
+void Interpolator::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("register_variable", "default", "fallback"), &Interpolator::register_variable);
+ ClassDB::bind_method(D_METHOD("set_variable_default", "var_id", "default"), &Interpolator::set_variable_default);
+ ClassDB::bind_method(D_METHOD("set_variable_custom_interpolator", "var_id", "object", "function_name"), &Interpolator::set_variable_custom_interpolator);
+
+ ClassDB::bind_method(D_METHOD("epoch_insert", "var_id", "value"), &Interpolator::epoch_insert);
+ ClassDB::bind_method(D_METHOD("pop_epoch", "epoch"), &Interpolator::pop_epoch);
+ ClassDB::bind_method(D_METHOD("get_last_pop_epoch"), &Interpolator::get_last_pop_epoch);
+
+ // TODO used to do the tests.
+ //ClassDB::bind_method(D_METHOD("terminate_init"), &Interpolator::terminate_init);
+ //ClassDB::bind_method(D_METHOD("begin_write", "epoch"), &Interpolator::begin_write);
+ //ClassDB::bind_method(D_METHOD("end_write"), &Interpolator::end_write);
+
+ BIND_ENUM_CONSTANT(FALLBACK_INTERPOLATE);
+ BIND_ENUM_CONSTANT(FALLBACK_DEFAULT);
+ BIND_ENUM_CONSTANT(FALLBACK_NEW_OR_NEAREST);
+ BIND_ENUM_CONSTANT(FALLBACK_OLD_OR_NEAREST);
+}
+
+void Interpolator::clear() {
+ epochs.clear();
+ buffer.clear();
+
+ write_position = UINT32_MAX;
+}
+
+void Interpolator::reset() {
+ variables.clear();
+ epochs.clear();
+ buffer.clear();
+
+ init_phase = true;
+ write_position = UINT32_MAX;
+ last_pop_epoch = 0;
+}
+
+int Interpolator::register_variable(const Variant &p_default, Fallback p_fallback) {
+ ERR_FAIL_COND_V_MSG(init_phase == false, -1, "You cannot add another variable at this point.");
+ const uint32_t id = variables.size();
+ variables.push_back(VariableInfo{ p_default, p_fallback, ObjectID(), StringName() });
+ return id;
+}
+
+void Interpolator::set_variable_default(int p_var_id, const Variant &p_default) {
+ ERR_FAIL_INDEX(p_var_id, int(variables.size()));
+ ERR_FAIL_COND(variables[p_var_id].default_value.get_type() != p_default.get_type());
+ variables[p_var_id].default_value = p_default;
+}
+
+void Interpolator::set_variable_custom_interpolator(int p_var_id, Object *p_object, const StringName &p_function_name) {
+ ERR_FAIL_COND_MSG(init_phase == false, "You cannot add another variable at this point.");
+ ERR_FAIL_INDEX_MSG(p_var_id, int(variables.size()), "The variable_id passed is unknown.");
+ variables[p_var_id].fallback = FALLBACK_CUSTOM_INTERPOLATOR;
+ variables[p_var_id].custom_interpolator_object = p_object->get_instance_id();
+ variables[p_var_id].custom_interpolator_function = p_function_name;
+}
+
+void Interpolator::terminate_init() {
+ init_phase = false;
+}
+
+uint32_t Interpolator::known_epochs_count() const {
+ return epochs.size();
+}
+
+void Interpolator::begin_write(uint32_t p_epoch) {
+ ERR_FAIL_COND_MSG(write_position != UINT32_MAX, "You can't call this function twice.");
+ ERR_FAIL_COND_MSG(init_phase, "You cannot write data while the buffer is not fully initialized, call `terminate_init`.");
+
+ // Make room for this epoch.
+ // Insert the epoch sorted in the buffer.
+ write_position = UINT32_MAX;
+ for (uint32_t i = 0; i < epochs.size(); i += 1) {
+ if (epochs[i] >= p_epoch) {
+ write_position = i;
+ break;
+ }
+ }
+
+ if (write_position < UINT32_MAX) {
+ if (epochs[write_position] == p_epoch) {
+ // This epoch already exists, nothing to do.
+ return;
+ } else {
+ // Make room.
+ epochs.push_back(UINT32_MAX);
+ buffer.push_back(Array());
+ // Sort the epochs.
+ for (int i = epochs.size() - 2; i >= int(write_position); i -= 1) {
+ epochs[uint32_t(i) + 1] = epochs[uint32_t(i)];
+ buffer[uint32_t(i) + 1] = buffer[uint32_t(i)];
+ }
+ // Init the new epoch.
+ epochs[write_position] = p_epoch;
+ buffer[write_position].clear();
+ buffer[write_position].resize(variables.size());
+ }
+ } else {
+ // No sort needed.
+ write_position = epochs.size();
+ epochs.push_back(p_epoch);
+ buffer.push_back(Array());
+ buffer[write_position].resize(variables.size());
+ }
+
+ // Set defaults.
+ Variant *ptr = &buffer[write_position][0];
+ for (uint32_t i = 0; i < variables.size(); i += 1) {
+ ptr[i] = variables[i].default_value;
+ }
+}
+
+void Interpolator::epoch_insert(int p_var_id, const Variant &p_value) {
+ ERR_FAIL_COND_MSG(write_position == UINT32_MAX, "Please call `begin_write` before.");
+ ERR_FAIL_INDEX_MSG(p_var_id, int(variables.size()), "The variable_id passed is unknown.");
+ const uint32_t var_id(p_var_id);
+ ERR_FAIL_COND_MSG(variables[var_id].default_value.get_type() != p_value.get_type(), "The variable: " + itos(p_var_id) + " expects the variable type: " + Variant::get_type_name(variables[var_id].default_value.get_type()) + ", and not: " + Variant::get_type_name(p_value.get_type()));
+ buffer[write_position][var_id] = p_value;
+}
+
+void Interpolator::end_write() {
+ ERR_FAIL_COND_MSG(write_position == UINT32_MAX, "You can't call this function before starting the epoch with `begin_write`.");
+ write_position = UINT32_MAX;
+}
+
+Array Interpolator::pop_epoch(uint32_t p_epoch, real_t p_fraction) {
+ ERR_FAIL_COND_V_MSG(init_phase, Array(), "You can't pop data if the interpolator is not fully initialized.");
+ ERR_FAIL_COND_V_MSG(write_position != UINT32_MAX, Array(), "You can't pop data while writing the epoch");
+
+ double epoch = double(p_epoch) + double(p_fraction);
+
+ // Search the epoch.
+ uint32_t position = UINT32_MAX;
+ for (uint32_t i = 0; i < epochs.size(); i += 1) {
+ if (static_cast(epochs[i]) >= epoch) {
+ position = i;
+ break;
+ }
+ }
+
+ ObjectID cache_object_id;
+ Object *cache_object = nullptr;
+
+ Array data;
+ if (unlikely(position == UINT32_MAX)) {
+ data.resize(variables.size());
+ Variant *ptr = &data[0];
+ if (buffer.size() == 0) {
+ // No data found, set all to default.
+ for (uint32_t i = 0; i < variables.size(); i += 1) {
+ ptr[i] = variables[i].default_value;
+ }
+ } else {
+ // No new data.
+ for (uint32_t i = 0; i < variables.size(); i += 1) {
+ switch (variables[i].fallback) {
+ case FALLBACK_DEFAULT:
+ ptr[i] = variables[i].default_value;
+ break;
+ case FALLBACK_INTERPOLATE: // No way to interpolate, so just send the nearest.
+ case FALLBACK_NEW_OR_NEAREST: // No new data, so send the nearest.
+ case FALLBACK_OLD_OR_NEAREST: // Just send the oldest, as desired.
+ ptr[i] = buffer[buffer.size() - 1][i];
+ break;
+ case FALLBACK_CUSTOM_INTERPOLATOR:
+ ptr[i] = variables[i].default_value;
+
+ if (cache_object_id != variables[i].custom_interpolator_object) {
+ ERR_CONTINUE_MSG(variables[i].custom_interpolator_object.is_null(), "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ Object *o = ObjectDB::get_instance(variables[i].custom_interpolator_object);
+ ERR_CONTINUE_MSG(o == nullptr, "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ cache_object_id = variables[i].custom_interpolator_object;
+ cache_object = o;
+ }
+
+ ptr[i] = cache_object->call(
+ variables[i].custom_interpolator_function,
+ epochs[buffer.size() - 1],
+ buffer[buffer.size() - 1][i],
+ -1,
+ variables[i].default_value,
+ 0.0);
+
+ if (ptr[i].get_type() != variables[i].default_value.get_type()) {
+ ERR_PRINT("The variable: " + itos(i) + " custom interpolator [" + variables[i].custom_interpolator_function + "], returned a different variant type. Expected: " + Variant::get_type_name(variables[i].default_value.get_type()) + ", returned: " + Variant::get_type_name(ptr[i].get_type()));
+ ptr[i] = variables[i].default_value;
+ }
+ break;
+ }
+ }
+ }
+ } else if (unlikely(ABS(epochs[position] - epoch) <= CMP_EPSILON)) {
+ // Precise data.
+ data = buffer[position];
+ } else if (unlikely(position == 0)) {
+ // No old data.
+ data.resize(variables.size());
+ Variant *ptr = &data[0];
+ for (uint32_t i = 0; i < variables.size(); i += 1) {
+ switch (variables[i].fallback) {
+ case FALLBACK_DEFAULT:
+ ptr[i] = variables[i].default_value;
+ break;
+ case FALLBACK_INTERPOLATE: // No way to interpolate, so just send the nearest.
+ case FALLBACK_NEW_OR_NEAREST: // Just send the newer data as desired.
+ case FALLBACK_OLD_OR_NEAREST: // No old data, so send nearest.
+ ptr[i] = buffer[0][i];
+ break;
+ case FALLBACK_CUSTOM_INTERPOLATOR:
+ ptr[i] = variables[i].default_value;
+ if (cache_object_id != variables[i].custom_interpolator_object) {
+ ERR_CONTINUE_MSG(variables[i].custom_interpolator_object.is_null(), "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ Object *o = ObjectDB::get_instance(variables[i].custom_interpolator_object);
+ ERR_CONTINUE_MSG(o == nullptr, "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ cache_object_id = variables[i].custom_interpolator_object;
+ cache_object = o;
+ }
+
+ ptr[i] = cache_object->call(
+ variables[i].custom_interpolator_function,
+ -1,
+ variables[i].default_value,
+ epochs[0],
+ buffer[0][i],
+ 1.0);
+
+ if (ptr[i].get_type() != variables[i].default_value.get_type()) {
+ ERR_PRINT("The variable: " + itos(i) + " custom interpolator [" + variables[i].custom_interpolator_function + "], returned a different variant type. Expected: " + Variant::get_type_name(variables[i].default_value.get_type()) + ", returned: " + Variant::get_type_name(ptr[i].get_type()));
+ ptr[i] = variables[i].default_value;
+ }
+ break;
+ }
+ }
+ } else {
+ // Enought data to do anything needed.
+ data.resize(variables.size());
+ Variant *ptr = &data[0];
+ for (uint32_t i = 0; i < variables.size(); i += 1) {
+ switch (variables[i].fallback) {
+ case FALLBACK_DEFAULT:
+ ptr[i] = variables[i].default_value;
+ break;
+ case FALLBACK_INTERPOLATE: {
+ const real_t delta = (epoch - double(epochs[position - 1])) / double(epochs[position] - epochs[position - 1]);
+ ptr[i] = interpolate(
+ buffer[position - 1][i],
+ buffer[position][i],
+ delta);
+ } break;
+ case FALLBACK_NEW_OR_NEAREST:
+ ptr[i] = buffer[position][i];
+ break;
+ case FALLBACK_OLD_OR_NEAREST:
+ ptr[i] = buffer[position - 1][i];
+ break;
+ case FALLBACK_CUSTOM_INTERPOLATOR: {
+ ptr[i] = variables[i].default_value;
+
+ if (cache_object_id != variables[i].custom_interpolator_object) {
+ ERR_CONTINUE_MSG(variables[i].custom_interpolator_object.is_null(), "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ Object *o = ObjectDB::get_instance(variables[i].custom_interpolator_object);
+ ERR_CONTINUE_MSG(o == nullptr, "The variable: " + itos(i) + " has a custom interpolator, but the function is invalid.");
+
+ cache_object_id = variables[i].custom_interpolator_object;
+ cache_object = o;
+ }
+
+ const real_t delta = (epoch - double(epochs[position - 1])) / double(epochs[position] - epochs[position - 1]);
+
+ ptr[i] = cache_object->call(
+ variables[i].custom_interpolator_function,
+ epochs[position - 1],
+ buffer[position - 1][i],
+ epochs[position],
+ buffer[position][i],
+ delta);
+
+ if (ptr[i].get_type() != variables[i].default_value.get_type()) {
+ ERR_PRINT("The variable: " + itos(i) + " custom interpolator [" + variables[i].custom_interpolator_function + "], returned a different variant type. Expected: " + Variant::get_type_name(variables[i].default_value.get_type()) + ", returned: " + Variant::get_type_name(ptr[i].get_type()));
+ ptr[i] = variables[i].default_value;
+ }
+ } break;
+ }
+ }
+ }
+
+ if (unlikely(position == UINT32_MAX)) {
+ if (buffer.size() > 1) {
+ // Remove all the elements but last. This happens when the p_epoch is
+ // bigger than the one already stored into the queue.
+ epochs[0] = epochs[buffer.size() - 1];
+ buffer[0] = buffer[buffer.size() - 1];
+ epochs.resize(1);
+ buffer.resize(1);
+ }
+ } else if (position >= 2) {
+ // TODO improve this by performing first the shifting then the resizing.
+ // Remove the old elements, but leave the one used to interpolate.
+ for (uint32_t i = 0; i < position - 1; i += 1) {
+ epochs.remove_at(0);
+ buffer.remove_at(0);
+ }
+ }
+
+ // TODO this is no more valid since I'm using the fractional part.
+ last_pop_epoch = MAX(p_epoch, last_pop_epoch);
+
+ return data;
+}
+
+uint32_t Interpolator::get_last_pop_epoch() const {
+ return last_pop_epoch;
+}
+
+uint32_t Interpolator::get_youngest_epoch() const {
+ if (epochs.size() <= 0) {
+ return UINT32_MAX;
+ }
+ return epochs[0];
+}
+
+uint32_t Interpolator::get_oldest_epoch() const {
+ if (epochs.size() <= 0) {
+ return UINT32_MAX;
+ }
+ return epochs[epochs.size() - 1];
+}
+
+uint32_t Interpolator::epochs_between_last_time_window() const {
+ if (epochs.size() <= 1) {
+ return 0;
+ }
+
+ return epochs[epochs.size() - 1] - epochs[epochs.size() - 2];
+}
+
+Variant Interpolator::interpolate(const Variant &p_v1, const Variant &p_v2, real_t p_delta) {
+ ERR_FAIL_COND_V(p_v1.get_type() != p_v2.get_type(), p_v1);
+
+ switch (p_v1.get_type()) {
+ case Variant::Type::INT:
+ return int(Math::round(Math::lerp(p_v1.operator real_t(), p_v2.operator real_t(), p_delta)));
+ case Variant::Type::FLOAT:
+ return Math::lerp(p_v1, p_v2, p_delta);
+ case Variant::Type::VECTOR2:
+ return p_v1.operator Vector2().lerp(p_v2.operator Vector2(), p_delta);
+ case Variant::Type::VECTOR2I:
+ return Vector2i(
+ int(Math::round(Math::lerp(p_v1.operator Vector2i()[0], p_v2.operator Vector2i()[0], p_delta))),
+ int(Math::round(Math::lerp(p_v1.operator Vector2i()[1], p_v2.operator Vector2i()[1], p_delta))));
+ case Variant::Type::TRANSFORM2D:
+ return p_v1.operator Transform2D().interpolate_with(p_v2.operator Transform2D(), p_delta);
+ case Variant::Type::VECTOR3:
+ return p_v1.operator Vector3().lerp(p_v2.operator Vector3(), p_delta);
+ case Variant::Type::VECTOR3I:
+ return Vector3i(
+ int(Math::round(Math::lerp(p_v1.operator Vector3i()[0], p_v2.operator Vector3i()[0], p_delta))),
+ int(Math::round(Math::lerp(p_v1.operator Vector3i()[1], p_v2.operator Vector3i()[1], p_delta))),
+ int(Math::round(Math::lerp(p_v1.operator Vector3i()[2], p_v2.operator Vector3i()[2], p_delta))));
+ case Variant::Type::QUATERNION:
+ return p_v1.operator Quaternion().slerp(p_v2.operator Quaternion(), p_delta);
+ case Variant::Type::BASIS:
+ return p_v1.operator Basis().slerp(p_v2.operator Basis(), p_delta);
+ case Variant::Type::TRANSFORM3D:
+ return p_v1.operator Transform3D().interpolate_with(p_v2.operator Transform3D(), p_delta);
+ default:
+ return p_delta > 0.5 ? p_v2 : p_v1;
+ }
+}
diff --git a/modules/network_synchronizer/interpolator.h b/modules/network_synchronizer/interpolator.h
new file mode 100644
index 000000000000..5b6f4de32050
--- /dev/null
+++ b/modules/network_synchronizer/interpolator.h
@@ -0,0 +1,104 @@
+/*************************************************************************/
+/* interpolator.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#ifndef INTERPOLATOR_H
+#define INTERPOLATOR_H
+
+#include "core/object/class_db.h"
+#include "core/templates/local_vector.h"
+
+class Interpolator : public Object {
+ GDCLASS(Interpolator, Object);
+
+public:
+ enum Fallback {
+ FALLBACK_INTERPOLATE,
+ FALLBACK_DEFAULT,
+ FALLBACK_OLD_OR_NEAREST,
+ FALLBACK_NEW_OR_NEAREST,
+ FALLBACK_CUSTOM_INTERPOLATOR
+ };
+
+private:
+ struct VariableInfo {
+ // TODO Do we need a name?
+ Variant default_value;
+ Fallback fallback;
+ ObjectID custom_interpolator_object;
+ StringName custom_interpolator_function;
+ };
+
+ LocalVector variables;
+
+ /// Epoch ids, sorted from youngest to oldest.
+ LocalVector epochs;
+ /// Epoch data.
+ LocalVector buffer;
+
+ bool init_phase = true;
+ uint32_t write_position = UINT32_MAX;
+ uint32_t last_pop_epoch = 0;
+
+ static void _bind_methods();
+
+public:
+ Interpolator() = default;
+
+ void clear();
+ void reset();
+
+ int register_variable(const Variant &p_default, Fallback p_fallback);
+ void set_variable_default(int p_var_id, const Variant &p_default);
+ void set_variable_custom_interpolator(int p_var_id, Object *p_object, const StringName &p_function_name);
+ void terminate_init();
+
+ /// Returns the epochs stored.
+ uint32_t known_epochs_count() const;
+ void begin_write(uint32_t p_epoch);
+ void epoch_insert(int p_var_id, const Variant &p_value);
+ void end_write();
+
+ Array pop_epoch(uint32_t p_epoch, real_t p_fraction);
+ uint32_t get_last_pop_epoch() const; // TODO do I need this? Remove if not.
+ uint32_t get_youngest_epoch() const;
+ uint32_t get_oldest_epoch() const;
+
+ /// Returns the epochs count between the two last received time window.
+ uint32_t epochs_between_last_time_window() const;
+
+ static Variant interpolate(const Variant &p_v1, const Variant &p_v2, real_t p_delta);
+};
+
+VARIANT_ENUM_CAST(Interpolator::Fallback);
+#endif
diff --git a/modules/network_synchronizer/net_utilities.cpp b/modules/network_synchronizer/net_utilities.cpp
new file mode 100644
index 000000000000..c041180f365f
--- /dev/null
+++ b/modules/network_synchronizer/net_utilities.cpp
@@ -0,0 +1,91 @@
+/*************************************************************************/
+/* net_utilities.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "net_utilities.h"
+
+#include "scene/main/node.h"
+
+bool NetUtility::ChangeListener::operator==(const ChangeListener &p_other) const {
+ return object_id == p_other.object_id && method == p_other.method;
+}
+
+NetUtility::VarData::VarData(const StringName &p_name) {
+ var.name = p_name;
+}
+
+NetUtility::VarData::VarData(NetVarId p_id, const StringName &p_name, const Variant &p_val, bool p_skip_rewinding, bool p_enabled) :
+ id(p_id),
+ skip_rewinding(p_skip_rewinding),
+ enabled(p_enabled) {
+ var.name = p_name;
+ var.value = p_val.duplicate(true);
+}
+
+bool NetUtility::VarData::operator==(const NetUtility::VarData &p_other) const {
+ return var.name == p_other.var.name;
+}
+
+bool NetUtility::VarData::operator<(const VarData &p_other) const {
+ return id < p_other.id;
+}
+
+void NetUtility::NodeData::process(const real_t p_delta) const {
+ const Variant var_delta = p_delta;
+ const Variant *fake_array_vars = &var_delta;
+
+ Callable::CallError e;
+ for (uint32_t i = 0; i < functions.size(); i += 1) {
+ node->callp(functions[i], &fake_array_vars, 1, e);
+ }
+}
+
+NetUtility::Snapshot::operator String() const {
+ String s;
+ s += "Snapshot input ID: " + itos(input_id);
+
+ for (int net_node_id = 0; net_node_id < node_vars.size(); net_node_id += 1) {
+ s += "\nNode Data: " + itos(net_node_id);
+ for (int i = 0; i < node_vars[net_node_id].size(); i += 1) {
+ s += "\n|- Variable: ";
+ s += node_vars[net_node_id][i].name;
+ s += " = ";
+ s += String(node_vars[net_node_id][i].value);
+ }
+ }
+ return s;
+}
+
+bool NetUtility::NodeChangeListener::operator==(const NodeChangeListener &p_other) const {
+ return node_data == p_other.node_data && var_id == p_other.var_id;
+}
diff --git a/modules/network_synchronizer/net_utilities.h b/modules/network_synchronizer/net_utilities.h
new file mode 100644
index 000000000000..cc80562fa0ea
--- /dev/null
+++ b/modules/network_synchronizer/net_utilities.h
@@ -0,0 +1,344 @@
+/*************************************************************************/
+/* net_utilities.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#ifndef NET_UTILITIES_H
+#define NET_UTILITIES_H
+
+#include "core/math/math_funcs.h"
+#include "core/templates/local_vector.h"
+#include "core/variant/variant.h"
+
+class Node;
+
+#ifdef DEBUG_ENABLED
+#define NET_DEBUG_PRINT(msg) \
+ print_line(String("[Net] ") + msg)
+#define NET_DEBUG_WARN(msg) \
+ WARN_PRINT(String("[Net] ") + msg)
+#define NET_DEBUG_ERR(msg) \
+ ERR_PRINT(String("[Net] ") + msg)
+#else
+#define NET_DEBUG_PRINT(msg)
+#define NET_DEBUG_WARN(msg)
+#define NET_DEBUG_ERR(msg)
+#endif
+
+typedef uint32_t NetNodeId;
+typedef uint32_t NetVarId;
+
+/// Flags used to control when an event is executed.
+enum NetEventFlag {
+
+ // ~~ Flags ~~ //
+ EMPTY = 0,
+
+ /// Called at the end of the frame, if the value is different.
+ /// It's also called when a variable is modified by the
+ /// `apply_scene_changes` function.
+ CHANGE = 1 << 0,
+
+ /// Called when the variable is modified by the `NetworkSynchronizer`
+ /// because not in sync with the server.
+ SYNC_RECOVER = 1 << 1,
+
+ /// Called when the variable is modified by the `NetworkSynchronizer`
+ /// because it's preparing the node for the rewinding.
+ SYNC_RESET = 1 << 2,
+
+ /// Called when the variable is modified during the rewinding phase.
+ SYNC_REWIND = 1 << 3,
+
+ /// Called at the end of the recovering phase, if the value was modified
+ /// during the rewinding.
+ END_SYNC = 1 << 4,
+
+ // ~~ Preconfigured ~~ //
+
+ DEFAULT = CHANGE | END_SYNC,
+ SYNC = SYNC_RECOVER | SYNC_RESET | SYNC_REWIND,
+ ALWAYS = CHANGE | SYNC_RECOVER | SYNC_RESET | SYNC_REWIND | END_SYNC
+};
+
+namespace NetUtility {
+
+template
+class StatisticalRingBuffer {
+ LocalVector data;
+ uint32_t index = 0;
+
+ T avg_sum = 0;
+
+public:
+ StatisticalRingBuffer(uint32_t p_size, T p_default);
+ void resize(uint32_t p_size, T p_default);
+ void reset(T p_default);
+
+ void push(T p_value);
+
+ /// Maximum value.
+ T max() const;
+
+ /// Minumum value.
+ T min(uint32_t p_consider_last) const;
+
+ /// Median value.
+ T average() const;
+
+ T get_deviation(T p_mean) const;
+
+private:
+ // Used to avoid accumulate precision loss.
+ void force_recompute_avg_sum();
+};
+
+template
+StatisticalRingBuffer::StatisticalRingBuffer(uint32_t p_size, T p_default) {
+ resize(p_size, p_default);
+}
+
+template
+void StatisticalRingBuffer::resize(uint32_t p_size, T p_default) {
+ data.resize(p_size);
+
+ reset(p_default);
+}
+
+template
+void StatisticalRingBuffer::reset(T p_default) {
+ for (uint32_t i = 0; i < data.size(); i += 1) {
+ data[i] = p_default;
+ }
+
+ index = 0;
+ force_recompute_avg_sum();
+}
+
+template
+void StatisticalRingBuffer::push(T p_value) {
+ avg_sum -= data[index];
+ avg_sum += p_value;
+ data[index] = p_value;
+
+ index = (index + 1) % data.size();
+ if (index == 0) {
+ // Each cycle recompute the sum.
+ force_recompute_avg_sum();
+ }
+}
+
+template
+T StatisticalRingBuffer::max() const {
+ CRASH_COND(data.size() == 0);
+
+ T a = data[0];
+ for (uint32_t i = 1; i < data.size(); i += 1) {
+ a = MAX(a, data[i]);
+ }
+ return a;
+}
+
+template
+T StatisticalRingBuffer::min(uint32_t p_consider_last) const {
+ CRASH_COND(data.size() == 0);
+ p_consider_last = MIN(p_consider_last, data.size());
+
+ const uint32_t youngest = (index == 0 ? data.size() : index) - 1;
+ const uint32_t oldest = (index + (data.size() - p_consider_last)) % data.size();
+
+ T a = data[oldest];
+
+ uint32_t i = oldest;
+ do {
+ i = (i + 1) % data.size();
+ a = MIN(a, data[i]);
+ } while (i != youngest);
+
+ return a;
+}
+
+template
+T StatisticalRingBuffer::average() const {
+ CRASH_COND(data.size() == 0);
+
+#ifdef DEBUG_ENABLED
+ T a = data[0];
+ for (uint32_t i = 1; i < data.size(); i += 1) {
+ a += data[i];
+ }
+ a = a / T(data.size());
+ T b = avg_sum / T(data.size());
+ const T difference = a > b ? a - b : b - a;
+ ERR_FAIL_COND_V_MSG(difference > (CMP_EPSILON * 4.0), b, "The `avg_sum` accumulated a sensible precision loss: " + rtos(difference));
+ return b;
+#else
+ // Divide it by the buffer size is wrong when the buffer is not yet fully
+ // initialized. However, this is wrong just for the first run.
+ // I'm leaving it as is because solve it mean do more operations. All this
+ // just to get the right value for the first few frames.
+ return avg_sum / T(data.size());
+#endif
+}
+
+template
+T StatisticalRingBuffer::get_deviation(T p_mean) const {
+ if (data.size() <= 0) {
+ return T();
+ }
+
+ double r = 0;
+ for (uint32_t i = 0; i < data.size(); i += 1) {
+ r += Math::pow(double(data[i]) - double(p_mean), 2.0);
+ }
+
+ return Math::sqrt(r / double(data.size()));
+}
+
+template
+void StatisticalRingBuffer::force_recompute_avg_sum() {
+#ifdef DEBUG_ENABLED
+ // This class is not supposed to be used with 0 size.
+ CRASH_COND(data.size() <= 0);
+#endif
+ avg_sum = data[0];
+ for (uint32_t i = 1; i < data.size(); i += 1) {
+ avg_sum += data[i];
+ }
+}
+
+/// Specific node listener. Alone this doesn't do much, but allows the
+/// `ChangeListener` to know and keep track of the node events.
+struct NodeChangeListener {
+ struct NodeData *node_data = nullptr;
+ NetVarId var_id = UINT32_MAX;
+
+ bool old_set = false;
+ Variant old_value;
+
+ bool operator==(const NodeChangeListener &p_other) const;
+};
+
+/// Change listener that rapresents a pair of Object and Method.
+/// This can track the changes of many nodes and variables. It's dispatched
+/// if one or more tracked variable change during the tracked phase, specified
+/// by the flag.
+struct ChangeListener {
+ // TODO use a callable instead??
+ ObjectID object_id = ObjectID();
+ StringName method;
+ uint32_t method_argument_count;
+ NetEventFlag flag;
+
+ LocalVector watching_vars;
+ LocalVector old_values;
+ bool emitted = true;
+
+ bool operator==(const ChangeListener &p_other) const;
+};
+
+struct Var {
+ StringName name;
+ Variant value;
+};
+
+struct VarData {
+ NetVarId id = UINT32_MAX;
+ Var var;
+ bool skip_rewinding = false;
+ bool enabled = false;
+ Vector change_listeners;
+
+ VarData() = default;
+ VarData(const StringName &p_name);
+ VarData(NetVarId p_id, const StringName &p_name, const Variant &p_val, bool p_skip_rewinding, bool p_enabled);
+
+ bool operator==(const VarData &p_other) const;
+ bool operator<(const VarData &p_other) const;
+};
+
+struct NodeData {
+ // ID used to reference this Node in the networked calls.
+ uint32_t id = 0;
+ ObjectID instance_id = ObjectID();
+ NodeData *controlled_by = nullptr;
+
+ /// When `false`, this node is not sync. It's usefult to locally pause sync
+ /// of specific nodes.
+ bool sync_enabled = true;
+
+ bool is_controller = false;
+ LocalVector controlled_nodes;
+ LocalVector dependency_nodes;
+ LocalVector dependency_nodes_end;
+
+ /// The sync variables of this node. The order of this vector matters
+ /// because the index is the `NetVarId`.
+ LocalVector vars;
+ LocalVector functions;
+
+ // This is valid to use only inside the process function.
+ Node *node = nullptr;
+
+ NodeData() = default;
+
+ void process(const real_t p_delta) const;
+};
+
+struct PeerData {
+ NetNodeId controller_id = UINT32_MAX;
+ // For new peers notify the state as soon as possible.
+ bool force_notify_snapshot = true;
+ // For new peers a full snapshot is needed.
+ bool need_full_snapshot = true;
+ // Used to know if the peer is enabled.
+ bool enabled = true;
+};
+
+struct Snapshot {
+ uint32_t input_id;
+ /// The Node variables in a particular frame. The order of this vector
+ /// matters because the index is the `NetNodeId`.
+ /// The variable array order also matter.
+ Vector> node_vars;
+
+ operator String() const;
+};
+
+struct PostponedRecover {
+ NodeData *node_data = nullptr;
+ Vector vars;
+};
+
+} // namespace NetUtility
+
+#endif
diff --git a/modules/network_synchronizer/networked_controller.cpp b/modules/network_synchronizer/networked_controller.cpp
new file mode 100644
index 000000000000..72ac8a31fed6
--- /dev/null
+++ b/modules/network_synchronizer/networked_controller.cpp
@@ -0,0 +1,1596 @@
+/*************************************************************************/
+/* networked_controller.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "networked_controller.h"
+
+#include "core/config/engine.h"
+#include "core/config/project_settings.h"
+#include "core/io/marshalls.h"
+#include "core/multiplayer/multiplayer_api.h"
+#include "scene_synchronizer.h"
+#include
+
+#define METADATA_SIZE 1
+
+#define MAX_ADDITIONAL_TICK_SPEED 2.0
+
+// 2%
+#define TICK_SPEED_CHANGE_NOTIF_THRESHOLD 4
+
+void NetworkedController::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_player_input_storage_size", "size"), &NetworkedController::set_player_input_storage_size);
+ ClassDB::bind_method(D_METHOD("get_player_input_storage_size"), &NetworkedController::get_player_input_storage_size);
+
+ ClassDB::bind_method(D_METHOD("set_max_redundant_inputs", "max_redundant_inputs"), &NetworkedController::set_max_redundant_inputs);
+ ClassDB::bind_method(D_METHOD("get_max_redundant_inputs"), &NetworkedController::get_max_redundant_inputs);
+
+ ClassDB::bind_method(D_METHOD("set_tick_speedup_notification_delay", "tick_speedup_notification_delay"), &NetworkedController::set_tick_speedup_notification_delay);
+ ClassDB::bind_method(D_METHOD("get_tick_speedup_notification_delay"), &NetworkedController::get_tick_speedup_notification_delay);
+
+ ClassDB::bind_method(D_METHOD("set_network_traced_frames", "size"), &NetworkedController::set_network_traced_frames);
+ ClassDB::bind_method(D_METHOD("get_network_traced_frames"), &NetworkedController::get_network_traced_frames);
+
+ ClassDB::bind_method(D_METHOD("set_min_frames_delay", "val"), &NetworkedController::set_min_frames_delay);
+ ClassDB::bind_method(D_METHOD("get_min_frames_delay"), &NetworkedController::get_min_frames_delay);
+
+ ClassDB::bind_method(D_METHOD("set_max_frames_delay", "val"), &NetworkedController::set_max_frames_delay);
+ ClassDB::bind_method(D_METHOD("get_max_frames_delay"), &NetworkedController::get_max_frames_delay);
+
+ ClassDB::bind_method(D_METHOD("set_net_sensitivity", "val"), &NetworkedController::set_net_sensitivity);
+ ClassDB::bind_method(D_METHOD("get_net_sensitivity"), &NetworkedController::get_net_sensitivity);
+
+ ClassDB::bind_method(D_METHOD("set_tick_acceleration", "acceleration"), &NetworkedController::set_tick_acceleration);
+ ClassDB::bind_method(D_METHOD("get_tick_acceleration"), &NetworkedController::get_tick_acceleration);
+
+ ClassDB::bind_method(D_METHOD("set_doll_epoch_collect_rate", "rate"), &NetworkedController::set_doll_epoch_collect_rate);
+ ClassDB::bind_method(D_METHOD("get_doll_epoch_collect_rate"), &NetworkedController::get_doll_epoch_collect_rate);
+
+ ClassDB::bind_method(D_METHOD("set_doll_epoch_batch_sync_rate", "rate"), &NetworkedController::set_doll_epoch_batch_sync_rate);
+ ClassDB::bind_method(D_METHOD("get_doll_epoch_batch_sync_rate"), &NetworkedController::get_doll_epoch_batch_sync_rate);
+
+ ClassDB::bind_method(D_METHOD("set_doll_min_frames_delay", "traced"), &NetworkedController::set_doll_min_frames_delay);
+ ClassDB::bind_method(D_METHOD("get_doll_min_frames_delay"), &NetworkedController::get_doll_min_frames_delay);
+
+ ClassDB::bind_method(D_METHOD("set_doll_max_frames_delay", "sensitivity"), &NetworkedController::set_doll_max_frames_delay);
+ ClassDB::bind_method(D_METHOD("get_doll_max_frames_delay"), &NetworkedController::get_doll_max_frames_delay);
+
+ ClassDB::bind_method(D_METHOD("set_doll_interpolation_max_speedup", "speedup"), &NetworkedController::set_doll_interpolation_max_speedup);
+ ClassDB::bind_method(D_METHOD("get_doll_interpolation_max_speedup"), &NetworkedController::get_doll_interpolation_max_speedup);
+
+ ClassDB::bind_method(D_METHOD("set_doll_connection_stats_frame_span", "speedup"), &NetworkedController::set_doll_connection_stats_frame_span);
+ ClassDB::bind_method(D_METHOD("get_doll_connection_stats_frame_span"), &NetworkedController::get_doll_connection_stats_frame_span);
+
+ ClassDB::bind_method(D_METHOD("set_doll_net_sensitivity", "sensitivity"), &NetworkedController::set_doll_net_sensitivity);
+ ClassDB::bind_method(D_METHOD("get_doll_net_sensitivity"), &NetworkedController::get_doll_net_sensitivity);
+
+ ClassDB::bind_method(D_METHOD("set_doll_max_delay", "max_delay"), &NetworkedController::set_doll_max_delay);
+ ClassDB::bind_method(D_METHOD("get_doll_max_delay"), &NetworkedController::get_doll_max_delay);
+
+ ClassDB::bind_method(D_METHOD("get_current_input_id"), &NetworkedController::get_current_input_id);
+
+ ClassDB::bind_method(D_METHOD("player_get_pretended_delta", "iterations_per_seconds"), &NetworkedController::player_get_pretended_delta);
+
+ ClassDB::bind_method(D_METHOD("mark_epoch_as_important"), &NetworkedController::mark_epoch_as_important);
+
+ ClassDB::bind_method(D_METHOD("set_doll_collect_rate_factor", "peer", "factor"), &NetworkedController::set_doll_collect_rate_factor);
+
+ ClassDB::bind_method(D_METHOD("set_doll_peer_active", "peer_id", "active"), &NetworkedController::set_doll_peer_active);
+
+ ClassDB::bind_method(D_METHOD("_rpc_server_send_inputs"), &NetworkedController::_rpc_server_send_inputs);
+ ClassDB::bind_method(D_METHOD("_rpc_send_tick_additional_speed"), &NetworkedController::_rpc_send_tick_additional_speed);
+ ClassDB::bind_method(D_METHOD("_rpc_doll_notify_sync_pause"), &NetworkedController::_rpc_doll_notify_sync_pause);
+ ClassDB::bind_method(D_METHOD("_rpc_doll_send_epoch_batch"), &NetworkedController::_rpc_doll_send_epoch_batch);
+
+ ClassDB::bind_method(D_METHOD("is_server_controller"), &NetworkedController::is_server_controller);
+ ClassDB::bind_method(D_METHOD("is_player_controller"), &NetworkedController::is_player_controller);
+ ClassDB::bind_method(D_METHOD("is_doll_controller"), &NetworkedController::is_doll_controller);
+ ClassDB::bind_method(D_METHOD("is_nonet_controller"), &NetworkedController::is_nonet_controller);
+
+ ClassDB::bind_method(D_METHOD("__on_sync_paused"), &NetworkedController::__on_sync_paused);
+
+ GDVIRTUAL_BIND(_collect_inputs, "delta", "buffer");
+ GDVIRTUAL_BIND(_controller_process, "delta", "buffer");
+ GDVIRTUAL_BIND(_are_inputs_different, "inputs_A", "inputs_B");
+ GDVIRTUAL_BIND(_count_input_size, "inputs");
+ GDVIRTUAL_BIND(_collect_epoch_data, "buffer");
+ GDVIRTUAL_BIND(_setup_interpolator, "interpolator");
+ GDVIRTUAL_BIND(_parse_epoch_data, "interpolator", "buffer");
+ GDVIRTUAL_BIND(_apply_epoch, "delta", "interpolated_data");
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "input_storage_size", PROPERTY_HINT_RANGE, "5,2000,1"), "set_player_input_storage_size", "get_player_input_storage_size");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "max_redundant_inputs", PROPERTY_HINT_RANGE, "0,1000,1"), "set_max_redundant_inputs", "get_max_redundant_inputs");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tick_speedup_notification_delay", PROPERTY_HINT_RANGE, "0.001,2.0,0.001"), "set_tick_speedup_notification_delay", "get_tick_speedup_notification_delay");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "network_traced_frames", PROPERTY_HINT_RANGE, "1,1000,1"), "set_network_traced_frames", "get_network_traced_frames");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "min_frames_delay", PROPERTY_HINT_RANGE, "0,100,1"), "set_min_frames_delay", "get_min_frames_delay");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "max_frames_delay", PROPERTY_HINT_RANGE, "0,100,1"), "set_max_frames_delay", "get_max_frames_delay");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "net_sensitivity", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_net_sensitivity", "get_net_sensitivity");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tick_acceleration", PROPERTY_HINT_RANGE, "0.1,20.0,0.01"), "set_tick_acceleration", "get_tick_acceleration");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "doll_epoch_collect_rate", PROPERTY_HINT_RANGE, "1,100,1"), "set_doll_epoch_collect_rate", "get_doll_epoch_collect_rate");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "doll_epoch_batch_sync_rate", PROPERTY_HINT_RANGE, "0.01,5.0,0.01"), "set_doll_epoch_batch_sync_rate", "get_doll_epoch_batch_sync_rate");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "doll_min_frames_delay", PROPERTY_HINT_RANGE, "0,10,1"), "set_doll_min_frames_delay", "get_doll_min_frames_delay");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "doll_max_frames_delay", PROPERTY_HINT_RANGE, "0,10,1"), "set_doll_max_frames_delay", "get_doll_max_frames_delay");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "doll_interpolation_max_speedup", PROPERTY_HINT_RANGE, "0.01,5.0,0.01"), "set_doll_interpolation_max_speedup", "get_doll_interpolation_max_speedup");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "doll_connection_stats_frame_span", PROPERTY_HINT_RANGE, "0,1000,1"), "set_doll_connection_stats_frame_span", "get_doll_connection_stats_frame_span");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "doll_net_sensitivity", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_doll_net_sensitivity", "get_doll_net_sensitivity");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "doll_max_delay", PROPERTY_HINT_RANGE, "1,1000,1"), "set_doll_max_delay", "get_doll_max_delay");
+
+ ADD_SIGNAL(MethodInfo("doll_sync_started"));
+ ADD_SIGNAL(MethodInfo("doll_sync_paused"));
+}
+
+NetworkedController::NetworkedController() {
+ constexpr bool call_local = false;
+ rpc_config(SNAME("_rpc_server_send_inputs"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_UNRELIABLE);
+ rpc_config(SNAME("_rpc_send_tick_additional_speed"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_UNRELIABLE);
+ rpc_config(SNAME("_rpc_doll_notify_sync_pause"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_RELIABLE);
+ rpc_config(SNAME("_rpc_doll_send_epoch_batch"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_UNRELIABLE);
+}
+
+void NetworkedController::set_player_input_storage_size(int p_size) {
+ player_input_storage_size = p_size;
+}
+
+int NetworkedController::get_player_input_storage_size() const {
+ return player_input_storage_size;
+}
+
+void NetworkedController::set_max_redundant_inputs(int p_max) {
+ max_redundant_inputs = p_max;
+}
+
+int NetworkedController::get_max_redundant_inputs() const {
+ return max_redundant_inputs;
+}
+
+void NetworkedController::set_tick_speedup_notification_delay(real_t p_delay) {
+ tick_speedup_notification_delay = p_delay;
+}
+
+real_t NetworkedController::get_tick_speedup_notification_delay() const {
+ return tick_speedup_notification_delay;
+}
+
+void NetworkedController::set_network_traced_frames(int p_size) {
+ network_traced_frames = p_size;
+}
+
+int NetworkedController::get_network_traced_frames() const {
+ return network_traced_frames;
+}
+
+void NetworkedController::set_min_frames_delay(int p_val) {
+ min_frames_delay = p_val;
+}
+
+int NetworkedController::get_min_frames_delay() const {
+ return min_frames_delay;
+}
+
+void NetworkedController::set_max_frames_delay(int p_val) {
+ max_frames_delay = p_val;
+}
+
+int NetworkedController::get_max_frames_delay() const {
+ return max_frames_delay;
+}
+
+void NetworkedController::set_net_sensitivity(real_t p_val) {
+ net_sensitivity = p_val;
+}
+
+real_t NetworkedController::get_net_sensitivity() const {
+ return net_sensitivity;
+}
+
+void NetworkedController::set_tick_acceleration(real_t p_acceleration) {
+ tick_acceleration = p_acceleration;
+}
+
+real_t NetworkedController::get_tick_acceleration() const {
+ return tick_acceleration;
+}
+
+void NetworkedController::set_doll_epoch_collect_rate(int p_rate) {
+ doll_epoch_collect_rate = MAX(p_rate, 1);
+}
+
+int NetworkedController::get_doll_epoch_collect_rate() const {
+ return doll_epoch_collect_rate;
+}
+
+void NetworkedController::set_doll_epoch_batch_sync_rate(real_t p_rate) {
+ doll_epoch_batch_sync_rate = MAX(p_rate, 0.001);
+}
+
+real_t NetworkedController::get_doll_epoch_batch_sync_rate() const {
+ return doll_epoch_batch_sync_rate;
+}
+
+void NetworkedController::set_doll_min_frames_delay(int p_min) {
+ doll_min_frames_delay = p_min;
+}
+
+int NetworkedController::get_doll_min_frames_delay() const {
+ return doll_min_frames_delay;
+}
+
+void NetworkedController::set_doll_max_frames_delay(int p_max) {
+ doll_max_frames_delay = p_max;
+}
+
+int NetworkedController::get_doll_max_frames_delay() const {
+ return doll_max_frames_delay;
+}
+
+void NetworkedController::set_doll_interpolation_max_speedup(real_t p_speedup) {
+ doll_interpolation_max_speedup = p_speedup;
+}
+
+real_t NetworkedController::get_doll_interpolation_max_speedup() const {
+ return doll_interpolation_max_speedup;
+}
+
+void NetworkedController::set_doll_connection_stats_frame_span(int p_span) {
+ doll_connection_stats_frame_span = p_span;
+}
+
+int NetworkedController::get_doll_connection_stats_frame_span() const {
+ return doll_connection_stats_frame_span;
+}
+
+void NetworkedController::set_doll_net_sensitivity(real_t p_sensitivity) {
+ doll_net_sensitivity = p_sensitivity;
+}
+
+real_t NetworkedController::get_doll_net_sensitivity() const {
+ return doll_net_sensitivity;
+}
+
+void NetworkedController::set_doll_max_delay(uint32_t p_max_delay) {
+ doll_max_delay = p_max_delay;
+}
+
+uint32_t NetworkedController::get_doll_max_delay() const {
+ return doll_max_delay;
+}
+
+uint32_t NetworkedController::get_current_input_id() const {
+ ERR_FAIL_NULL_V(controller, 0);
+ return controller->get_current_input_id();
+}
+
+real_t NetworkedController::player_get_pretended_delta(uint32_t p_iterations_per_seconds) const {
+ ERR_FAIL_COND_V_MSG(is_player_controller() == false, 1.0 / real_t(p_iterations_per_seconds), "This function can be called only on client.");
+ return get_player_controller()->get_pretended_delta(p_iterations_per_seconds);
+}
+
+void NetworkedController::mark_epoch_as_important() {
+ ERR_FAIL_COND_MSG(is_server_controller() == false, "This function must be called only within the function `collect_epoch_data`.");
+ get_server_controller()->is_epoch_important = true;
+}
+
+void NetworkedController::set_doll_collect_rate_factor(int p_peer, real_t p_factor) {
+ ERR_FAIL_COND_MSG(is_server_controller() == false, "This function can be called only on server.");
+ ServerController *server_controller = static_cast(controller);
+ const uint32_t pos = server_controller->find_peer(p_peer);
+ if (pos == UINT32_MAX) {
+ // This peers seems disabled, nothing to do here.
+ return;
+ }
+ server_controller->peers[pos].update_rate_factor = CLAMP(p_factor, 0.001, 1.0);
+}
+
+void NetworkedController::set_doll_peer_active(int p_peer_id, bool p_active) {
+ ERR_FAIL_COND_MSG(is_server_controller() == false, "You can set doll activation only on server");
+ ERR_FAIL_COND_MSG(p_peer_id == get_multiplayer_authority(), "This `peer_id` is equal to the Master `peer_id`, which is not allowed.");
+
+ ServerController *server_controller = static_cast(controller);
+ const uint32_t pos = server_controller->find_peer(p_peer_id);
+ if (pos == UINT32_MAX) {
+ // This peers seems disabled, nothing to do here.
+ return;
+ }
+ if (server_controller->peers[pos].active == p_active) {
+ // Nothing to do.
+ return;
+ }
+
+ server_controller->peers[pos].active = p_active;
+ server_controller->peers[pos].collect_timer = 0.0;
+
+ if (p_active == false) {
+ // Notify the doll only for deactivations. The activations are automatically
+ // handled when the first epoch is received.
+ rpc_id(p_peer_id, SNAME("_rpc_doll_notify_sync_pause"), server_controller->epoch);
+ }
+}
+
+void NetworkedController::pause_notify_dolls() {
+ ERR_FAIL_COND_MSG(is_server_controller() == false, "You can pause the dolls only on server. [BUG]");
+
+ // Notify the dolls this actor is disabled.
+ ServerController *server_controller = static_cast(controller);
+ for (uint32_t i = 0; i < server_controller->peers.size(); i += 1) {
+ if (server_controller->peers[i].active) {
+ // Notify this actor is no more active.
+ rpc_id(server_controller->peers[i].peer, SNAME("_rpc_doll_notify_sync_pause"), server_controller->epoch);
+ }
+ }
+}
+
+bool NetworkedController::process_instant(int p_i, real_t p_delta) {
+ ERR_FAIL_COND_V_MSG(is_player_controller() == false, false, "Can be executed only on player controllers.");
+ return static_cast(controller)->process_instant(p_i, p_delta);
+}
+
+ServerController *NetworkedController::get_server_controller() {
+ ERR_FAIL_COND_V_MSG(is_server_controller() == false, nullptr, "This controller is not a server controller.");
+ return static_cast(controller);
+}
+
+const ServerController *NetworkedController::get_server_controller() const {
+ ERR_FAIL_COND_V_MSG(is_server_controller() == false, nullptr, "This controller is not a server controller.");
+ return static_cast(controller);
+}
+
+PlayerController *NetworkedController::get_player_controller() {
+ ERR_FAIL_COND_V_MSG(is_player_controller() == false, nullptr, "This controller is not a player controller.");
+ return static_cast(controller);
+}
+
+const PlayerController *NetworkedController::get_player_controller() const {
+ ERR_FAIL_COND_V_MSG(is_player_controller() == false, nullptr, "This controller is not a player controller.");
+ return static_cast(controller);
+}
+
+DollController *NetworkedController::get_doll_controller() {
+ ERR_FAIL_COND_V_MSG(is_doll_controller() == false, nullptr, "This controller is not a doll controller.");
+ return static_cast(controller);
+}
+
+const DollController *NetworkedController::get_doll_controller() const {
+ ERR_FAIL_COND_V_MSG(is_doll_controller() == false, nullptr, "This controller is not a doll controller.");
+ return static_cast(controller);
+}
+
+NoNetController *NetworkedController::get_nonet_controller() {
+ ERR_FAIL_COND_V_MSG(is_nonet_controller() == false, nullptr, "This controller is not a no net controller.");
+ return static_cast(controller);
+}
+
+const NoNetController *NetworkedController::get_nonet_controller() const {
+ ERR_FAIL_COND_V_MSG(is_nonet_controller() == false, nullptr, "This controller is not a no net controller.");
+ return static_cast(controller);
+}
+
+bool NetworkedController::is_server_controller() const {
+ return controller_type == CONTROLLER_TYPE_SERVER;
+}
+
+bool NetworkedController::is_player_controller() const {
+ return controller_type == CONTROLLER_TYPE_PLAYER;
+}
+
+bool NetworkedController::is_doll_controller() const {
+ return controller_type == CONTROLLER_TYPE_DOLL;
+}
+
+bool NetworkedController::is_nonet_controller() const {
+ return controller_type == CONTROLLER_TYPE_NONETWORK;
+}
+
+void NetworkedController::set_inputs_buffer(const BitArray &p_new_buffer, uint32_t p_metadata_size_in_bit, uint32_t p_size_in_bit) {
+ inputs_buffer.get_buffer_mut().get_bytes_mut() = p_new_buffer.get_bytes();
+ inputs_buffer.shrink_to(p_metadata_size_in_bit, p_size_in_bit);
+}
+
+void NetworkedController::set_scene_synchronizer(SceneSynchronizer *p_synchronizer) {
+ if (scene_synchronizer) {
+ scene_synchronizer->disconnect(SNAME("sync_paused"), Callable(this, SNAME("__on_sync_paused")));
+ }
+
+ scene_synchronizer = p_synchronizer;
+
+ if (scene_synchronizer) {
+ scene_synchronizer->connect(SNAME("sync_paused"), Callable(this, SNAME("__on_sync_paused")));
+ }
+}
+
+SceneSynchronizer *NetworkedController::get_scene_synchronizer() const {
+ return scene_synchronizer;
+}
+
+bool NetworkedController::has_scene_synchronizer() const {
+ return scene_synchronizer;
+}
+
+void NetworkedController::_rpc_server_send_inputs(const Vector &p_data) {
+ ERR_FAIL_COND(is_server_controller() == false);
+ static_cast(controller)->receive_inputs(p_data);
+}
+
+void NetworkedController::_rpc_send_tick_additional_speed(const Vector &p_data) {
+ ERR_FAIL_COND(is_player_controller() == false);
+ ERR_FAIL_COND(p_data.size() != 1);
+
+ const uint8_t speed = p_data[0];
+ const real_t additional_speed = MAX_ADDITIONAL_TICK_SPEED * (((static_cast(speed) / static_cast(UINT8_MAX)) - 0.5) / 0.5);
+
+ PlayerController *player_controller = static_cast(controller);
+ player_controller->tick_additional_speed = CLAMP(additional_speed, -MAX_ADDITIONAL_TICK_SPEED, MAX_ADDITIONAL_TICK_SPEED);
+}
+
+void NetworkedController::_rpc_doll_notify_sync_pause(uint32_t p_epoch) {
+ ERR_FAIL_COND_MSG(is_doll_controller() == false, "Only dolls are supposed to receive this function call");
+
+ static_cast(controller)->pause(p_epoch);
+}
+
+void NetworkedController::_rpc_doll_send_epoch_batch(const Vector &p_data) {
+ ERR_FAIL_COND_MSG(is_doll_controller() == false, "Only dolls are supposed to receive this function call.");
+ ERR_FAIL_COND_MSG(p_data.size() <= 0, "It's not supposed to receive a 0 size data.");
+
+ static_cast(controller)->receive_batch(p_data);
+}
+
+void NetworkedController::player_set_has_new_input(bool p_has) {
+ has_player_new_input = p_has;
+}
+
+bool NetworkedController::player_has_new_input() const {
+ return has_player_new_input;
+}
+
+void NetworkedController::__on_sync_paused() {
+ if (controller_type == CONTROLLER_TYPE_DOLL) {
+ DollController *doll = static_cast(controller);
+ doll->pause(doll->current_epoch);
+ }
+}
+
+void NetworkedController::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+
+#ifdef DEBUG_ENABLED
+ // This can't happen, since only the doll are processed here.
+ CRASH_COND(is_doll_controller() == false);
+#endif
+ static_cast(controller)->process(get_physics_process_delta_time());
+
+ } break;
+#ifdef DEBUG_ENABLED
+ case NOTIFICATION_READY: {
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_collect_inputs) == false, "In your script you must inherit the virtual method `_collect_inputs` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_controller_process) == false, "In your script you must inherit the virtual method `_controller_process` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_are_inputs_different) == false, "In your script you must inherit the virtual method `_are_inputs_different` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_count_input_size) == false, "In your script you must inherit the virtual method `_count_input_size` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_collect_epoch_data) == false, "In your script you must inherit the virtual method `_collect_epoch_data` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_setup_interpolator) == false, "In your script you must inherit the virtual method `_setup_interpolator` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_parse_epoch_data) == false, "In your script you must inherit the virtual method `_parse_epoch_data` to correctly use the `NetworkedController`.");
+ ERR_FAIL_COND_MSG(GDVIRTUAL_IS_OVERRIDDEN(_apply_epoch) == false, "In your script you must inherit the virtual method `_apply_epoch` to correctly use the `NetworkedController`.");
+
+ } break;
+#endif
+ }
+}
+
+ServerController::ServerController(
+ NetworkedController *p_node,
+ int p_traced_frames) :
+ Controller(p_node),
+ network_watcher(p_traced_frames, 0) {
+}
+
+void ServerController::process(real_t p_delta) {
+ if (unlikely(enabled == false)) {
+ // Disabled by the SceneSynchronizer.
+ return;
+ }
+
+ fetch_next_input();
+
+ if (unlikely(current_input_buffer_id == UINT32_MAX)) {
+ // Skip this until the first input arrive.
+ return;
+ }
+
+ node->get_inputs_buffer_mut().begin_read();
+ node->get_inputs_buffer_mut().seek(METADATA_SIZE);
+ GDVIRTUAL_CALL_PTR(
+ node,
+ _controller_process,
+ p_delta,
+ &node->get_inputs_buffer_mut());
+
+ doll_sync(p_delta);
+
+ if (streaming_paused == false) {
+ calculates_player_tick_rate(p_delta);
+ adjust_player_tick_rate(p_delta);
+ }
+}
+
+bool is_remote_frame_A_older(const FrameSnapshot &p_snap_a, const FrameSnapshot &p_snap_b) {
+ return p_snap_a.id < p_snap_b.id;
+}
+
+uint32_t ServerController::last_known_input() const {
+ if (snapshots.size() > 0) {
+ return snapshots.back().id;
+ } else {
+ return UINT32_MAX;
+ }
+}
+
+uint32_t ServerController::get_current_input_id() const {
+ return current_input_buffer_id;
+}
+
+void ServerController::set_enabled(bool p_enable) {
+ if (enabled == p_enable) {
+ return;
+ }
+ enabled = p_enable;
+ node->pause_notify_dolls();
+
+ // ~~ On state change, reset everything to avoid accumulate old data. ~~
+
+ // Client inputs reset.
+ ghost_input_count = 0;
+ last_sent_state_input_id = 0;
+ client_tick_additional_speed = 0.0;
+ additional_speed_notif_timer = 0.0;
+ snapshots.clear();
+ input_arrival_time = UINT32_MAX;
+ network_watcher.reset(0.0);
+
+ // Doll reset.
+ is_epoch_important = false;
+ batch_sync_timer = 0.0;
+}
+
+void ServerController::clear_peers() {
+ peers.clear();
+}
+
+void ServerController::activate_peer(int p_peer) {
+ // Collects all the dolls.
+
+#ifdef DEBUG_ENABLED
+ // Unreachable because this is the server controller.
+ CRASH_COND(node->get_tree()->get_multiplayer()->is_server() == false);
+#endif
+ if (p_peer == node->get_multiplayer_authority()) {
+ // This is self, so not a doll.
+ return;
+ }
+
+ const uint32_t index = find_peer(p_peer);
+ if (index == UINT32_MAX) {
+ peers.push_back(p_peer);
+ }
+}
+
+void ServerController::deactivate_peer(int p_peer) {
+ const uint32_t index = find_peer(p_peer);
+ if (index != UINT32_MAX) {
+ peers.remove_at_unordered(index);
+ }
+}
+
+void ServerController::receive_inputs(const Vector &p_data) {
+ // The packet is composed as follow:
+ // |- The following four bytes for the first input ID.
+ // \- Array of inputs:
+ // |-- First byte the amount of times this input is duplicated in the packet.
+ // |-- inputs buffer.
+ //
+ // Let's decode it!
+
+ const uint32_t now = OS::get_singleton()->get_ticks_msec();
+ // If now is bigger, then the timer has been disabled, so we assume 0.
+ network_watcher.push(now > input_arrival_time ? now - input_arrival_time : 0);
+ input_arrival_time = now;
+
+ const int data_len = p_data.size();
+
+ int ofs = 0;
+
+ ERR_FAIL_COND(data_len < 4);
+ const uint32_t first_input_id = decode_uint32(p_data.ptr() + ofs);
+ ofs += 4;
+
+ uint32_t inserted_input_count = 0;
+
+ // Contains the entire packet and in turn it will be seek to specific location
+ // so I will not need to copy chunk of the packet data.
+ DataBuffer pir;
+ pir.begin_read();
+ pir.get_buffer_mut().get_bytes_mut() = p_data;
+ // TODO this is for 3.2
+ //pir.get_buffer_mut().resize_in_bytes(data_len);
+ //memcpy(pir.get_buffer_mut().get_bytes_mut().ptrw(), p_data.ptr(), data_len);
+
+ while (ofs < data_len) {
+ ERR_FAIL_COND_MSG(ofs + 1 > data_len, "The arrived packet size doesn't meet the expected size.");
+ // First byte is used for the duplication count.
+ const uint8_t duplication = p_data[ofs];
+ ofs += 1;
+
+ // Validate input
+ const int input_buffer_offset_bit = ofs * 8;
+ pir.shrink_to(input_buffer_offset_bit, (data_len - ofs) * 8);
+ pir.seek(input_buffer_offset_bit);
+ // Read metadata
+ const bool has_data = pir.read_bool();
+
+ int input_size = 0;
+ if (has_data) {
+ if (!GDVIRTUAL_CALL_PTR(node, _count_input_size, const_cast(&pir), input_size)) {
+ ERR_PRINT("The function `_count_input_size` was not executed.");
+ }
+ }
+
+ const int input_size_in_bits = input_size + METADATA_SIZE;
+
+ // Pad to 8 bits.
+ const int input_size_padded =
+ Math::ceil((static_cast(input_size_in_bits)) / 8.0);
+ ERR_FAIL_COND_MSG(ofs + input_size_padded > data_len, "The arrived packet size doesn't meet the expected size.");
+
+ // The input is valid, populate the buffer.
+ for (int sub = 0; sub <= duplication; sub += 1) {
+ const uint32_t input_id = first_input_id + inserted_input_count;
+ inserted_input_count += 1;
+
+ if (unlikely(current_input_buffer_id != UINT32_MAX && current_input_buffer_id >= input_id)) {
+ // We already have this input, so we don't need it anymore.
+ continue;
+ }
+
+ FrameSnapshot rfs;
+ rfs.id = input_id;
+
+ const bool found = std::binary_search(
+ snapshots.begin(),
+ snapshots.end(),
+ rfs,
+ is_remote_frame_A_older);
+
+ if (found == false) {
+ rfs.buffer_size_bit = input_size_in_bits;
+ rfs.inputs_buffer.get_bytes_mut().resize(input_size_padded);
+ memcpy(
+ rfs.inputs_buffer.get_bytes_mut().ptrw(),
+ p_data.ptr() + ofs,
+ input_size_padded);
+
+ snapshots.push_back(rfs);
+
+ // Sort the new inserted snapshot.
+ std::sort(
+ snapshots.begin(),
+ snapshots.end(),
+ is_remote_frame_A_older);
+ }
+ }
+
+ // We can now advance the offset.
+ ofs += input_size_padded;
+ }
+
+#ifdef DEBUG_ENABLED
+ if (snapshots.empty() == false && current_input_buffer_id != UINT32_MAX) {
+ // At this point is guaranteed that the current_input_buffer_id is never
+ // greater than the first item contained by `snapshots`.
+ CRASH_COND(current_input_buffer_id >= snapshots.front().id);
+ }
+#endif
+
+ ERR_FAIL_COND_MSG(ofs != data_len, "At the end was detected that the arrived packet has an unexpected size.");
+}
+
+int ServerController::get_inputs_count() const {
+ return snapshots.size();
+}
+
+bool ServerController::fetch_next_input() {
+ bool is_new_input = true;
+
+ if (unlikely(current_input_buffer_id == UINT32_MAX)) {
+ // As initial packet, anything is good.
+ if (snapshots.empty() == false) {
+ // First input arrived.
+ node->set_inputs_buffer(snapshots.front().inputs_buffer, METADATA_SIZE, snapshots.front().buffer_size_bit - METADATA_SIZE);
+ current_input_buffer_id = snapshots.front().id;
+ snapshots.pop_front();
+ // Start tracing the packets from this moment on.
+ network_watcher.reset(0);
+ input_arrival_time = UINT32_MAX;
+ } else {
+ is_new_input = false;
+ }
+ } else {
+ const uint32_t next_input_id = current_input_buffer_id + 1;
+
+ if (unlikely(streaming_paused)) {
+ // Stream is paused.
+ if (snapshots.empty() == false &&
+ snapshots.front().id >= next_input_id) {
+ // A new input is arrived while the streaming is paused.
+ const bool is_buffer_void = (snapshots.front().buffer_size_bit - METADATA_SIZE) == 0;
+ streaming_paused = is_buffer_void;
+ node->set_inputs_buffer(snapshots.front().inputs_buffer, METADATA_SIZE, snapshots.front().buffer_size_bit - METADATA_SIZE);
+ current_input_buffer_id = snapshots.front().id;
+ is_new_input = true;
+ snapshots.pop_front();
+ network_watcher.reset(0);
+ input_arrival_time = UINT32_MAX;
+ } else {
+ // No inputs, or we are not yet arrived to the client input,
+ // so just pretend the next input is void.
+ node->set_inputs_buffer(BitArray(METADATA_SIZE), METADATA_SIZE, 0);
+ is_new_input = false;
+ }
+ } else if (unlikely(snapshots.empty() == true)) {
+ // The input buffer is empty; a packet is missing.
+ is_new_input = false;
+ ghost_input_count += 1;
+ NET_DEBUG_PRINT("Input buffer is void, i'm using the previous one!");
+
+ } else {
+ // The input buffer is not empty, search the new input.
+ if (next_input_id == snapshots.front().id) {
+ // Wow, the next input is perfect!
+ node->set_inputs_buffer(snapshots.front().inputs_buffer, METADATA_SIZE, snapshots.front().buffer_size_bit - METADATA_SIZE);
+ current_input_buffer_id = snapshots.front().id;
+ snapshots.pop_front();
+
+ ghost_input_count = 0;
+ } else {
+ // The next packet is not here. This can happen when:
+ // - The packet is lost or not yet arrived.
+ // - The client for any reason desync with the server.
+ //
+ // In this cases, the server has the hard task to re-sync.
+ //
+ // # What it does, then?
+ // Initially it see that only 1 packet is missing so it just use
+ // the previous one and increase `ghost_inputs_count` to 1.
+ //
+ // The next iteration, if the packet is not yet arrived the
+ // server trys to take the next packet with the `id` less or
+ // equal to `next_packet_id + ghost_packet_id`.
+ //
+ // As you can see the server doesn't lose immediately the hope
+ // to find the missing packets, but at the same time deals with
+ // it so increases its search pool per each iteration.
+ //
+ // # Wise input search.
+ // Let's consider the case when a set of inputs arrive at the
+ // same time, while the server is struggling for the missing packets.
+ //
+ // In the meanwhile that the packets were chilling on the net,
+ // the server were simulating by guessing on their data; this
+ // mean that they don't have any longer room to be simulated
+ // when they arrive, and the right thing would be just forget
+ // about these.
+ //
+ // The thing is that these can still contain meaningful data, so
+ // instead to jump directly to the newest we restart the inputs
+ // from the next important packet.
+ //
+ // For this reason we keep track the amount of missing packets
+ // using `ghost_input_count`.
+
+ ghost_input_count += 1;
+
+ const int size = MIN(ghost_input_count, snapshots.size());
+ const uint32_t ghost_packet_id = next_input_id + ghost_input_count;
+
+ bool recovered = false;
+ FrameSnapshot pi;
+
+ DataBuffer pir_A = node->get_inputs_buffer();
+
+ for (int i = 0; i < size; i += 1) {
+ if (ghost_packet_id < snapshots.front().id) {
+ break;
+ } else {
+ pi = snapshots.front();
+ snapshots.pop_front();
+ recovered = true;
+
+ // If this input has some important changes compared to the last
+ // good input, let's recover to this point otherwise skip it
+ // until the last one.
+ // Useful to avoid that the server stay too much behind the
+ // client.
+
+ DataBuffer pir_B(pi.inputs_buffer);
+ pir_B.shrink_to(METADATA_SIZE, pi.buffer_size_bit - METADATA_SIZE);
+
+ pir_A.begin_read();
+ pir_A.seek(METADATA_SIZE);
+ pir_B.begin_read();
+ pir_B.seek(METADATA_SIZE);
+
+ bool is_meaningful = true;
+ const bool executed = GDVIRTUAL_CALL_PTR(node, _are_inputs_different, const_cast(&pir_A), const_cast(&pir_B), is_meaningful);
+ if (executed == false) {
+ ERR_PRINT("The function _are_inputs_different was not executed!");
+ }
+
+ if (is_meaningful) {
+ break;
+ }
+ }
+ }
+
+ if (recovered) {
+ node->set_inputs_buffer(pi.inputs_buffer, METADATA_SIZE, snapshots.front().buffer_size_bit - METADATA_SIZE);
+ current_input_buffer_id = pi.id;
+ ghost_input_count = 0;
+ NET_DEBUG_PRINT("Packet recovered");
+ } else {
+ is_new_input = false;
+ NET_DEBUG_PRINT("Packet still missing");
+ }
+ }
+ }
+ }
+
+#ifdef DEBUG_ENABLED
+ if (snapshots.empty() == false && current_input_buffer_id != UINT32_MAX) {
+ // At this point is guaranteed that the current_input_buffer_id is never
+ // greater than the first item contained by `snapshots`.
+ CRASH_COND(current_input_buffer_id >= snapshots.front().id);
+ }
+#endif
+ return is_new_input;
+}
+
+void ServerController::notify_send_state() {
+ last_sent_state_input_id = get_current_input_id();
+ // If the notified input is a void buffer, the client is allowed to pause
+ // the input streaming. So missing packets are just handled as void inputs.
+ if (node->get_inputs_buffer().size() == 0) {
+ streaming_paused = true;
+ }
+}
+
+void ServerController::doll_sync(real_t p_delta) {
+ // Advance the epoch.
+ epoch += 1;
+ batch_sync_timer += p_delta;
+ const bool send_batch = batch_sync_timer >= node->get_doll_epoch_batch_sync_rate();
+
+ bool epoch_state_collected = false;
+
+ // Process each peer and send the data if needed.
+ for (uint32_t i = 0; i < peers.size(); i += 1) {
+ if (peers[i].active == false) {
+ // Nothing to do on this peer.
+ continue;
+ }
+
+ peers[i].collect_timer += 1;
+ if (
+ is_epoch_important ||
+ peers[i].collect_timer >= peers[i].collect_threshold) {
+ // Resets the timer.
+ peers[i].collect_timer -= peers[i].collect_threshold;
+ // Since is possible to force send the state update, we need to make
+ // sure the timer doesn't go below 0.
+ peers[i].collect_timer = MAX(0, peers[i].collect_timer);
+
+ // Prepare the epoch_data cache.
+ if (epoch_state_collected == false) {
+ epoch_state_data_cache.begin_write(0);
+ epoch_state_data_cache.add_int(epoch, DataBuffer::COMPRESSION_LEVEL_1);
+ GDVIRTUAL_CALL_PTR(node, _collect_epoch_data, &epoch_state_data_cache);
+ epoch_state_data_cache.dry();
+ epoch_state_collected = true;
+ }
+
+ // Store this into epoch batch.
+ if (unlikely(epoch_state_data_cache.get_buffer().get_bytes().size() > UINT8_MAX)) {
+ // If the packet is more than 255 it can't be sent.
+ NET_DEBUG_ERR("The status update is too big, try to staty under 255 bytes per update. This status is dropped.");
+ } else {
+ peers[i].batch_size += 1 + epoch_state_data_cache.get_buffer().get_bytes().size();
+ peers[i].epoch_batch.push_back(epoch_state_data_cache.get_buffer().get_bytes());
+ }
+ }
+
+ // Send batch data.
+ if (send_batch) {
+ const uint8_t next_collect_rate =
+ MIN(node->get_doll_epoch_collect_rate() /
+ peers[i].update_rate_factor,
+ UINT8_MAX);
+
+ // Next rate is
+ peers[i].collect_threshold = next_collect_rate;
+
+ if (peers[i].epoch_batch.size() > 0) {
+ // Add space to allocate the next_collect_rate.
+ peers[i].batch_size += 1;
+
+#ifdef DEBUG_ENABLED
+ if (peers[i].batch_size >= 1350) {
+ NET_DEBUG_WARN("The amount of data collected for this batch is more than 1350 bytes. Please make sure the `doll_sync_timer_rate` is not so big, so to avoid packet fragmentation. Batch size: " + itos(peers[i].batch_size) + " - Epochs into the batch: " + itos(peers[i].epoch_batch.size()));
+ }
+#endif
+
+ // Prepare the batch data.
+ Vector data;
+ data.resize(peers[i].batch_size);
+ uint8_t *data_ptr = data.ptrw();
+ uint32_t offset = 0;
+ data_ptr[offset] = next_collect_rate;
+ offset += 1;
+ for (uint32_t x = 0; x < peers[i].epoch_batch.size(); x += 1) {
+ ERR_CONTINUE_MSG(peers[i].epoch_batch[x].size() > 256, "It's not allowed to send more than 256 bytes per status. This status is dropped.");
+ data_ptr[offset] = peers[i].epoch_batch[x].size();
+ offset += 1;
+ for (int l = 0; l < peers[i].epoch_batch[x].size(); l += 1) {
+ data_ptr[offset] = peers[i].epoch_batch[x][l];
+ offset += 1;
+ }
+ }
+#ifdef DEBUG_ENABLED
+ // This is not supposed to happen because the batch_size is
+ // correctly computed.
+ CRASH_COND(offset != peers[i].batch_size);
+#endif
+ peers[i].epoch_batch.clear();
+ peers[i].batch_size = 0;
+
+ // Send the data
+ node->rpc_id(
+ peers[i].peer,
+ SNAME("_rpc_doll_send_epoch_batch"),
+ data);
+ }
+ }
+ }
+
+ if (send_batch) {
+ batch_sync_timer = 0.0;
+ }
+
+ is_epoch_important = false;
+}
+
+void ServerController::calculates_player_tick_rate(real_t p_delta) {
+ const real_t min_frames_delay = node->get_min_frames_delay();
+ const real_t max_frames_delay = node->get_max_frames_delay();
+ const real_t net_sensitivity = node->get_net_sensitivity();
+
+ const uint32_t avg_receive_time = network_watcher.average();
+ const real_t deviation_receive_time = real_t(network_watcher.get_deviation(avg_receive_time)) / 1000.0;
+
+ // The network quality can be established just by checking the standard
+ // deviation. Stable connections have standard deviation that tend to 0.
+ const real_t net_poorness = MIN(
+ deviation_receive_time / net_sensitivity,
+ 1.0);
+
+ const int optimal_frame_delay = Math::lerp(
+ min_frames_delay,
+ max_frames_delay,
+ net_poorness);
+
+ int consecutive_inputs = 0;
+ for (uint32_t i = 0; i < snapshots.size(); i += 1) {
+ if (snapshots[i].id == (current_input_buffer_id + consecutive_inputs + 1)) {
+ consecutive_inputs += 1;
+ }
+ }
+
+ const real_t distance_to_optimal_count = real_t(optimal_frame_delay - consecutive_inputs);
+
+ const real_t acc = distance_to_optimal_count * node->get_tick_acceleration() * p_delta;
+ // Used to avoid oscillations.
+ const real_t damp = -(client_tick_additional_speed * 0.95);
+ client_tick_additional_speed += acc + damp * ((SIGN(acc) * SIGN(damp) + 1) / 2.0);
+ client_tick_additional_speed = CLAMP(client_tick_additional_speed, -MAX_ADDITIONAL_TICK_SPEED, MAX_ADDITIONAL_TICK_SPEED);
+
+#ifdef DEBUG_ENABLED
+ const bool debug = ProjectSettings::get_singleton()->get_setting("NetworkSynchronizer/debug_server_speedup");
+ if (debug) {
+ print_line("Network poorness: " + rtos(net_poorness) + ", optimal frame delay: " + itos(optimal_frame_delay) + ", deviation: " + rtos(deviation_receive_time) + ", current frame delay: " + itos(consecutive_inputs));
+ }
+#endif
+}
+
+void ServerController::adjust_player_tick_rate(real_t p_delta) {
+ additional_speed_notif_timer += p_delta;
+ if (additional_speed_notif_timer >= node->get_tick_speedup_notification_delay()) {
+ additional_speed_notif_timer = 0.0;
+
+ const uint8_t new_speed = UINT8_MAX * (((client_tick_additional_speed / MAX_ADDITIONAL_TICK_SPEED) + 1.0) / 2.0);
+
+ Vector packet_data;
+ packet_data.push_back(new_speed);
+
+ node->rpc_id(
+ node->get_multiplayer_authority(),
+ SNAME("_rpc_send_tick_additional_speed"),
+ packet_data);
+ }
+}
+
+uint32_t ServerController::find_peer(int p_peer) const {
+ for (uint32_t i = 0; i < peers.size(); i += 1) {
+ if (peers[i].peer == p_peer) {
+ return i;
+ }
+ }
+ return UINT32_MAX;
+}
+
+PlayerController::PlayerController(NetworkedController *p_node) :
+ Controller(p_node),
+ current_input_id(UINT32_MAX),
+ input_buffers_counter(0),
+ time_bank(0.0),
+ tick_additional_speed(0.0) {
+}
+
+void PlayerController::process(real_t p_delta) {
+ // We need to know if we can accept a new input because in case of bad
+ // internet connection we can't keep accumulating inputs forever
+ // otherwise the server will differ too much from the client and we
+ // introduce virtual lag.
+ const bool accept_new_inputs = can_accept_new_inputs();
+
+ if (accept_new_inputs) {
+ current_input_id = input_buffers_counter;
+
+ node->get_inputs_buffer_mut().begin_write(METADATA_SIZE);
+
+ node->get_inputs_buffer_mut().seek(1);
+ GDVIRTUAL_CALL_PTR(node, _collect_inputs, p_delta, &node->get_inputs_buffer_mut());
+
+ // Set metadata data.
+ node->get_inputs_buffer_mut().seek(0);
+ if (node->get_inputs_buffer().size() > 0) {
+ node->get_inputs_buffer_mut().add_bool(true);
+ streaming_paused = false;
+ } else {
+ node->get_inputs_buffer_mut().add_bool(false);
+ }
+ } else {
+ NET_DEBUG_WARN("It's not possible to accept new inputs. Is this lagging?");
+ }
+
+ node->get_inputs_buffer_mut().dry();
+ node->get_inputs_buffer_mut().begin_read();
+ node->get_inputs_buffer_mut().seek(METADATA_SIZE); // Skip meta.
+
+ // The physics process is always emitted, because we still need to simulate
+ // the character motion even if we don't store the player inputs.
+ GDVIRTUAL_CALL_PTR(node, _controller_process, p_delta, &node->get_inputs_buffer());
+
+ node->player_set_has_new_input(false);
+ if (accept_new_inputs) {
+ if (streaming_paused == false) {
+ input_buffers_counter += 1;
+ store_input_buffer(current_input_id);
+ send_frame_input_buffer_to_server();
+ node->player_set_has_new_input(true);
+ }
+ }
+}
+
+int PlayerController::calculates_sub_ticks(real_t p_delta, real_t p_iteration_per_seconds) {
+ const real_t pretended_delta = get_pretended_delta(p_iteration_per_seconds);
+
+ time_bank += p_delta;
+ const int sub_ticks = static_cast(time_bank / pretended_delta);
+ time_bank -= static_cast(sub_ticks) * pretended_delta;
+ return sub_ticks;
+}
+
+int PlayerController::notify_input_checked(uint32_t p_input_id) {
+ if (frames_snapshot.empty() || p_input_id < frames_snapshot.front().id || p_input_id > frames_snapshot.back().id) {
+ // The received p_input_id is not known, so nothing to do.
+ NET_DEBUG_ERR("The received snapshot, with input id: " + itos(p_input_id) + " is not known. This is a bug or someone is trying to hack.");
+ return frames_snapshot.size();
+ }
+
+ // Remove inputs prior to the known one. We may still need the known one
+ // when the stream is paused.
+ while (frames_snapshot.empty() == false && frames_snapshot.front().id <= p_input_id) {
+ if (frames_snapshot.front().id == p_input_id) {
+ streaming_paused = (frames_snapshot.front().buffer_size_bit - METADATA_SIZE) <= 0;
+ }
+ frames_snapshot.pop_front();
+ }
+
+#ifdef DEBUG_ENABLED
+ // Unreachable, because the next input have always the next `p_input_id` or empty.
+ CRASH_COND(frames_snapshot.empty() == false && (p_input_id + 1) != frames_snapshot.front().id);
+#endif
+
+ // Make sure the remaining inputs are 0 sized, if not streaming can't be paused.
+ if (streaming_paused) {
+ for (auto it = frames_snapshot.begin(); it != frames_snapshot.end(); it += 1) {
+ if ((it->buffer_size_bit - METADATA_SIZE) > 0) {
+ // Streaming can't be paused.
+ streaming_paused = false;
+ break;
+ }
+ }
+ }
+
+ return frames_snapshot.size();
+}
+
+uint32_t PlayerController::last_known_input() const {
+ return get_stored_input_id(-1);
+}
+
+uint32_t PlayerController::get_stored_input_id(int p_i) const {
+ if (p_i < 0) {
+ if (frames_snapshot.empty() == false) {
+ return frames_snapshot.back().id;
+ } else {
+ return UINT32_MAX;
+ }
+ } else {
+ const size_t i = p_i;
+ if (i < frames_snapshot.size()) {
+ return frames_snapshot[i].id;
+ } else {
+ return UINT32_MAX;
+ }
+ }
+}
+
+bool PlayerController::process_instant(int p_i, real_t p_delta) {
+ const size_t i = p_i;
+ if (i < frames_snapshot.size()) {
+ DataBuffer ib(frames_snapshot[i].inputs_buffer);
+ ib.shrink_to(METADATA_SIZE, frames_snapshot[i].buffer_size_bit - METADATA_SIZE);
+ ib.begin_read();
+ ib.seek(METADATA_SIZE);
+ GDVIRTUAL_CALL_PTR(node, _controller_process, p_delta, &ib);
+
+ return (i + 1) < frames_snapshot.size();
+ } else {
+ return false;
+ }
+}
+
+uint32_t PlayerController::get_current_input_id() const {
+ return current_input_id;
+}
+
+real_t PlayerController::get_pretended_delta(real_t p_iteration_per_seconds) const {
+ return 1.0 / (p_iteration_per_seconds + tick_additional_speed);
+}
+
+void PlayerController::store_input_buffer(uint32_t p_id) {
+ FrameSnapshot inputs;
+ inputs.id = p_id;
+ inputs.inputs_buffer = node->get_inputs_buffer().get_buffer();
+ inputs.buffer_size_bit = node->get_inputs_buffer().size() + METADATA_SIZE;
+ inputs.similarity = UINT32_MAX;
+ frames_snapshot.push_back(inputs);
+}
+
+void PlayerController::send_frame_input_buffer_to_server() {
+ // The packet is composed as follow:
+ // - The following four bytes for the first input ID.
+ // - Array of inputs:
+ // |-- First byte the amount of times this input is duplicated in the packet.
+ // |-- Input buffer.
+
+ const size_t inputs_count = MIN(frames_snapshot.size(), static_cast(node->get_max_redundant_inputs() + 1));
+ CRASH_COND(inputs_count < 1); // Unreachable
+
+#define MAKE_ROOM(p_size) \
+ if (cached_packet_data.size() < static_cast(ofs + p_size)) \
+ cached_packet_data.resize(ofs + p_size);
+
+ int ofs = 0;
+
+ // Let's store the ID of the first snapshot.
+ MAKE_ROOM(4);
+ const uint32_t first_input_id = frames_snapshot[frames_snapshot.size() - inputs_count].id;
+ ofs += encode_uint32(first_input_id, cached_packet_data.ptr() + ofs);
+
+ uint32_t previous_input_id = UINT32_MAX;
+ uint32_t previous_input_similarity = UINT32_MAX;
+ int previous_buffer_size = 0;
+ uint8_t duplication_count = 0;
+
+ DataBuffer pir_A(node->get_inputs_buffer().get_buffer());
+
+ // Compose the packets
+ for (size_t i = frames_snapshot.size() - inputs_count; i < frames_snapshot.size(); i += 1) {
+ bool is_similar = false;
+
+ if (previous_input_id == UINT32_MAX) {
+ // This happens for the first input of the packet.
+ // Just write it.
+ is_similar = false;
+ } else if (duplication_count == UINT8_MAX) {
+ // Prevent to overflow the `uint8_t`.
+ is_similar = false;
+ } else {
+ if (frames_snapshot[i].similarity != previous_input_id) {
+ if (frames_snapshot[i].similarity == UINT32_MAX) {
+ // This input was never compared, let's do it now.
+ DataBuffer pir_B(frames_snapshot[i].inputs_buffer);
+ pir_B.shrink_to(METADATA_SIZE, frames_snapshot[i].buffer_size_bit - METADATA_SIZE);
+
+ pir_A.begin_read();
+ pir_A.seek(METADATA_SIZE);
+ pir_B.begin_read();
+ pir_B.seek(METADATA_SIZE);
+
+ bool are_different = true;
+ const bool executed = GDVIRTUAL_CALL_PTR(node, _are_inputs_different, const_cast(&pir_A), const_cast(&pir_B), are_different);
+ if (executed == false) {
+ ERR_PRINT("The function _are_inputs_different was not executed!");
+ }
+
+ is_similar = are_different == false;
+
+ } else if (frames_snapshot[i].similarity == previous_input_similarity) {
+ // This input is similar to the previous one, the thing is
+ // that the similarity check was done on an older input.
+ // Fortunatelly we are able to compare the similarity id
+ // and detect its similarity correctly.
+ is_similar = true;
+ } else {
+ // This input is simply different from the previous one.
+ is_similar = false;
+ }
+ } else {
+ // These are the same, let's save some space.
+ is_similar = true;
+ }
+ }
+
+ if (is_similar) {
+ // This input is similar to the previous one, so just duplicate it.
+ duplication_count += 1;
+ // In this way, we don't need to compare these frames again.
+ frames_snapshot[i].similarity = previous_input_id;
+
+ } else {
+ // This input is different from the previous one, so let's
+ // finalize the previous and start another one.
+
+ if (previous_input_id != UINT32_MAX) {
+ // We can finally finalize the previous input
+ cached_packet_data[ofs - previous_buffer_size - 1] = duplication_count;
+ }
+
+ // Resets the duplication count.
+ duplication_count = 0;
+
+ // Writes the duplication_count for this new input
+ MAKE_ROOM(1);
+ cached_packet_data[ofs] = 0;
+ ofs += 1;
+
+ // Write the inputs
+ const int buffer_size = frames_snapshot[i].inputs_buffer.get_bytes().size();
+ MAKE_ROOM(buffer_size);
+ memcpy(
+ cached_packet_data.ptr() + ofs,
+ frames_snapshot[i].inputs_buffer.get_bytes().ptr(),
+ buffer_size);
+ ofs += buffer_size;
+
+ // Let's see if we can duplicate this input.
+ previous_input_id = frames_snapshot[i].id;
+ previous_input_similarity = frames_snapshot[i].similarity;
+ previous_buffer_size = buffer_size;
+
+ pir_A.get_buffer_mut() = frames_snapshot[i].inputs_buffer;
+ pir_A.shrink_to(METADATA_SIZE, frames_snapshot[i].buffer_size_bit - METADATA_SIZE);
+ }
+ }
+
+ // Finalize the last added input_buffer.
+ cached_packet_data[ofs - previous_buffer_size - 1] = duplication_count;
+
+ // Make the packet data.
+ Vector packet_data;
+ packet_data.resize(ofs);
+
+ memcpy(
+ packet_data.ptrw(),
+ cached_packet_data.ptr(),
+ ofs);
+
+ const int server_peer_id = 1;
+ node->rpc_id(
+ server_peer_id,
+ SNAME("_rpc_server_send_inputs"),
+ packet_data);
+}
+
+bool PlayerController::can_accept_new_inputs() const {
+ return frames_snapshot.size() < static_cast(node->get_player_input_storage_size());
+}
+
+DollController::DollController(NetworkedController *p_node) :
+ Controller(p_node),
+ network_watcher(node->get_doll_connection_stats_frame_span(), 0) {
+}
+
+void DollController::ready() {
+ interpolator.reset();
+ GDVIRTUAL_CALL_PTR(
+ node,
+ _setup_interpolator,
+ &interpolator);
+ interpolator.terminate_init();
+}
+
+void DollController::process(real_t p_delta) {
+ const uint32_t frame_epoch = next_epoch();
+
+ if (unlikely(frame_epoch == UINT32_MAX)) {
+ // Nothing to do.
+ return;
+ }
+
+ const real_t fractional_part = advancing_epoch;
+ GDVIRTUAL_CALL_PTR(
+ node,
+ _apply_epoch,
+ p_delta,
+ interpolator.pop_epoch(frame_epoch, fractional_part));
+}
+
+uint32_t DollController::get_current_input_id() const {
+ return current_epoch;
+}
+
+void DollController::receive_batch(const Vector &p_data) {
+ if (unlikely(node->get_scene_synchronizer()->is_enabled() == false)) {
+ // The sync is disabled, nothing to do.
+ return;
+ }
+
+ // Take the epochs befoe the batch is applied.
+ const uint32_t youngest_epoch = interpolator.get_youngest_epoch();
+ const uint32_t oldest_epoch = interpolator.get_oldest_epoch();
+
+ int initially_stored_epochs = 0;
+ if (youngest_epoch != UINT32_MAX && oldest_epoch != UINT32_MAX) {
+ initially_stored_epochs = oldest_epoch - youngest_epoch;
+ }
+
+ initially_stored_epochs -= missing_epochs;
+ missing_epochs = 0;
+
+ uint32_t batch_young_epoch = UINT32_MAX;
+
+ int buffer_start_position = 0;
+
+ const uint8_t next_collect_rate = p_data[buffer_start_position];
+ buffer_start_position += 1;
+
+ while (buffer_start_position < p_data.size()) {
+ const int buffer_size = p_data[buffer_start_position];
+ const Vector buffer = p_data.slice(
+ buffer_start_position + 1,
+ buffer_start_position + 1 + buffer_size);
+
+ ERR_FAIL_COND(buffer.size() <= 0);
+
+ const uint32_t epoch = receive_epoch(buffer);
+ buffer_start_position += 1 + buffer_size;
+
+ batch_young_epoch = MIN(epoch, batch_young_epoch);
+ }
+
+ // ~~ Establish the interpolation speed ~~
+ if (batch_young_epoch == UINT32_MAX) {
+ // This may just be a late arrived batch, so nothing more to do.
+ return;
+ }
+
+ const real_t net_sentitivity = node->get_doll_net_sensitivity();
+
+ // Establish the connection quality by checking if the batch takes
+ // always the same time to arrive.
+ const uint32_t now = OS::get_singleton()->get_ticks_msec();
+ // If now is bigger, then the timer has been disabled, so we assume 0.
+ network_watcher.push(now > batch_receiver_timer ? now - batch_receiver_timer : 0);
+ batch_receiver_timer = now;
+
+ const uint32_t avg_receive_time = network_watcher.average();
+ const real_t deviation_receive_time = real_t(network_watcher.get_deviation(avg_receive_time)) / 1000.0;
+
+ // The network quality can be established just by checking the standard
+ // deviation. Stable connections have standard deviation that tend to 0.
+ const real_t net_poorness = MIN(
+ deviation_receive_time / net_sentitivity,
+ 1.0);
+
+ const int optimal_frame_delay = Math::lerp(
+ node->get_doll_min_frames_delay(),
+ node->get_doll_max_frames_delay(),
+ net_poorness);
+
+ // TODO cache this?
+ const double frames_per_batch = node->get_doll_epoch_batch_sync_rate() * real_t(Engine::get_singleton()->get_physics_ticks_per_second());
+ const double next_batch_arrives_in = Math::ceil(double(next_collect_rate) / frames_per_batch) * frames_per_batch;
+
+ const real_t doll_interpolation_max_speedup = node->get_doll_interpolation_max_speedup();
+ additional_speed = doll_interpolation_max_speedup * (real_t(initially_stored_epochs - optimal_frame_delay) / next_batch_arrives_in);
+ additional_speed = CLAMP(additional_speed, -doll_interpolation_max_speedup, doll_interpolation_max_speedup);
+
+#ifdef DEBUG_ENABLED
+ const bool debug = ProjectSettings::get_singleton()->get_setting("NetworkSynchronizer/debug_doll_speedup");
+ if (debug) {
+ print_line("Network poorness: " + rtos(net_poorness) + ", optimal stored epochs: " + rtos(optimal_frame_delay) + ", deviation: " + rtos(deviation_receive_time));
+ }
+#endif
+}
+
+uint32_t DollController::receive_epoch(const Vector &p_data) {
+ DataBuffer buffer(p_data);
+ buffer.begin_read();
+ const uint32_t epoch = buffer.read_int(DataBuffer::COMPRESSION_LEVEL_1);
+
+ if (epoch <= paused_epoch) {
+ // The sync is in pause from this epoch, so just discard this received
+ // epoch that may just be a late received epoch.
+ return UINT32_MAX;
+ }
+
+ interpolator.begin_write(epoch);
+ GDVIRTUAL_CALL_PTR(node, _parse_epoch_data, &interpolator, &buffer);
+ interpolator.end_write();
+
+ return epoch;
+}
+
+uint32_t DollController::next_epoch() {
+ // TODO re-describe.
+ // This function regulates the epoch ID to process.
+ // The epoch is not simply increased by one because we need to make sure
+ // to make the client apply the nearest server state while giving some room
+ // for the subsequent information to arrive.
+
+ // Step 1, Wait that we have at least two epochs.
+ if (unlikely(current_epoch == UINT32_MAX)) {
+ // Interpolator is not yet started.
+ if (interpolator.known_epochs_count() < 2) {
+ // Not ready yet.
+ return UINT32_MAX;
+ }
+
+#ifdef DEBUG_ENABLED
+ // At this point we have 2 epoch, something is always returned at this
+ // point.
+ CRASH_COND(interpolator.get_youngest_epoch() == UINT32_MAX);
+#endif
+
+ // Start epoch interpolation.
+ current_epoch = interpolator.get_youngest_epoch();
+ node->emit_signal("doll_sync_started");
+ }
+
+ // At this point the interpolation is started and the function must
+ // return the best epoch id which we have to apply the state.
+
+ // Step 2. Make sure we have something to interpolate with.
+ const uint32_t oldest_epoch = interpolator.get_oldest_epoch();
+ if (unlikely(oldest_epoch == UINT32_MAX || oldest_epoch <= current_epoch)) {
+ missing_epochs += 1;
+ // Nothing to interpolate with.
+ return current_epoch;
+ }
+
+#ifdef DEBUG_ENABLED
+ // This can't happen because the current_epoch is advances only if it's
+ // possible to do so.
+ CRASH_COND(oldest_epoch < current_epoch);
+#endif
+
+ const uint64_t max_delay = node->get_doll_max_delay();
+
+ if (unlikely((oldest_epoch - current_epoch) > max_delay)) {
+ // This client seems too much behind at this point. Teleport forward.
+ const uint32_t youngest_epoch = interpolator.get_youngest_epoch();
+ current_epoch = MAX(oldest_epoch - max_delay, youngest_epoch);
+ } else {
+ advancing_epoch += 1.0 + additional_speed;
+ }
+
+ if (advancing_epoch > 0.0) {
+ // Advance the epoch by the the integral amount.
+ current_epoch += uint32_t(advancing_epoch);
+ // Clamp to the oldest epoch.
+ current_epoch = MIN(current_epoch, oldest_epoch);
+
+ // Keep the floating point part.
+ advancing_epoch -= uint32_t(advancing_epoch);
+ }
+
+ return current_epoch;
+}
+
+void DollController::pause(uint32_t p_epoch) {
+ paused_epoch = p_epoch;
+
+ interpolator.clear();
+ additional_speed = 0.0;
+ current_epoch = UINT32_MAX;
+ advancing_epoch = 0.0;
+ missing_epochs = 0;
+ network_watcher.resize(node->get_doll_connection_stats_frame_span(), 0);
+ batch_receiver_timer = UINT32_MAX;
+
+ node->emit_signal("doll_sync_paused");
+}
+
+NoNetController::NoNetController(NetworkedController *p_node) :
+ Controller(p_node),
+ frame_id(0) {
+}
+
+void NoNetController::process(real_t p_delta) {
+ node->get_inputs_buffer_mut().begin_write(0); // No need of meta in this case.
+ GDVIRTUAL_CALL_PTR(node, _collect_inputs, p_delta, &node->get_inputs_buffer_mut());
+ node->get_inputs_buffer_mut().dry();
+ node->get_inputs_buffer_mut().begin_read();
+ GDVIRTUAL_CALL_PTR(node, _controller_process, p_delta, &node->get_inputs_buffer_mut());
+ frame_id += 1;
+}
+
+uint32_t NoNetController::get_current_input_id() const {
+ return frame_id;
+}
diff --git a/modules/network_synchronizer/networked_controller.h b/modules/network_synchronizer/networked_controller.h
new file mode 100644
index 000000000000..1ae8a5771ef1
--- /dev/null
+++ b/modules/network_synchronizer/networked_controller.h
@@ -0,0 +1,535 @@
+/*************************************************************************/
+/* networked_controller.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "scene/main/node.h"
+
+#include "data_buffer.h"
+#include "interpolator.h"
+#include "net_utilities.h"
+#include
+
+#ifndef NETWORKED_CONTROLLER_H
+#define NETWORKED_CONTROLLER_H
+
+class SceneSynchronizer;
+struct Controller;
+struct ServerController;
+struct PlayerController;
+struct DollController;
+struct NoNetController;
+
+/// The `NetworkedController` is responsible to sync the `Player` inputs between
+/// the peers. This allows to control a character, or an object with high precision
+/// and replicates that movement on all connected peers.
+///
+/// The `NetworkedController` will sync inputs, based on those will perform
+/// operations.
+/// The result of these operations, are guaranteed to be the same accross the
+/// peers, if we stay under the assumption that the initial state is the same.
+///
+/// Is possible to use the `SceneSynchronizer` to keep the state in sync with the
+/// peers.
+///
+// # Implementation details
+//
+// The `NetworkedController` perform different operations depending where it's
+// instantiated.
+// The most important part is inside the `PlayerController`, `ServerController`,
+// `DollController`, `NoNetController`.
+class NetworkedController : public Node {
+ GDCLASS(NetworkedController, Node);
+
+ friend class SceneSynchronizer;
+
+public:
+ enum ControllerType {
+ CONTROLLER_TYPE_NULL,
+ CONTROLLER_TYPE_NONETWORK,
+ CONTROLLER_TYPE_PLAYER,
+ CONTROLLER_TYPE_SERVER,
+ CONTROLLER_TYPE_DOLL
+ };
+
+ GDVIRTUAL2(_collect_inputs, real_t, DataBuffer*);
+ GDVIRTUAL2(_controller_process, real_t, const DataBuffer*);
+ GDVIRTUAL2R(bool, _are_inputs_different, const DataBuffer*, const DataBuffer*);
+ GDVIRTUAL1RC(int, _count_input_size, const DataBuffer*);
+ GDVIRTUAL1(_collect_epoch_data, DataBuffer*);
+ GDVIRTUAL1(_setup_interpolator, Interpolator*);
+ GDVIRTUAL2(_parse_epoch_data, Interpolator*, const DataBuffer*);
+ GDVIRTUAL2(_apply_epoch, real_t, Array);
+
+private:
+ /// The input storage size is used to cap the amount of inputs collected by
+ /// the `PlayerController`.
+ ///
+ /// The server sends a message, to all the connected peers, notifing its
+ /// status at a fixed interval.
+ /// The peers, after receiving this update, removes all the old inputs until
+ /// that moment.
+ ///
+ /// `input_storage_size`: is too small, the player may stop collect
+ /// - Too small value makes the `PlayerController` stop collecting inputs
+ /// too early, in case of lag.
+ /// - Too big values may introduce too much latency, because the player keep
+ /// pushing new inputs without receiving the server snapshot.
+ ///
+ /// With 60 iteration per seconds a good value is `180` (60 * 3) so the
+ /// `PlayerController` can be at max 3 seconds ahead the `ServerController`.
+ int player_input_storage_size = 180;
+
+ /// Amount of time an inputs is re-sent to each peer.
+ /// Resenging inputs is necessary because the packets may be lost since as
+ /// they are sent in an unreliable way.
+ int max_redundant_inputs = 5;
+
+ /// Time in seconds between each `tick_speedup` that the server sends to the
+ /// client.
+ real_t tick_speedup_notification_delay = 0.33;
+
+ /// The connection quality is established by watching the time passed
+ /// between each input is received.
+ /// The more this time is the same the more the connection health is good.
+ ///
+ /// The `network_traced_frames` defines how many frames have
+ /// to be used to establish the connection quality.
+ /// - Big values make the mechanism too slow.
+ /// - Small values make the mechanism too sensible.
+ int network_traced_frames = 120;
+
+ /// Sensitivity to network oscillations. The value is in seconds and can be
+ /// used to establish the connection quality.
+ ///
+ /// For each input, the time needed for its arrival is traced; the standard
+ /// deviation of these is divided by `net_sensitivity`: the result is
+ /// the connection quality.
+ ///
+ /// The more the time needed for each batch to arrive is different the
+ /// bigger this value is: when this value approaches to
+ /// `net_sensitivity` (or even surpasses it) the bad the connection is.
+ ///
+ /// The result is the `connection_poorness` that goes from 0 to 1 and is
+ /// used to decide the `optimal_frame_delay` that is interpolated between
+ /// `min_frames_delay` and `max_frames_delay`.
+ real_t net_sensitivity = 0.1;
+
+ /// The `ServerController` will try to keep a margin of error, so that
+ /// network oscillations doesn't leave the `ServerController` without
+ /// inputs.
+ ///
+ /// This margin of error is called `optimal_frame_delay` and it changes
+ /// depending on the connection health:
+ /// it can go from `min_frames_delay` to `max_frames_delay`.
+ int min_frames_delay = 2;
+ int max_frames_delay = 6;
+
+ /// Rate at which the tick speed changes, so the `optimal_frame_delay` is
+ /// matched.
+ real_t tick_acceleration = 2.0;
+
+ /// Collect rate (in frames) used by the server to estabish when to collect
+ /// the state for a particular peer.
+ /// It's possible to scale down this rate, for a particular peer,
+ /// using the function: set_doll_collect_rate_factor(peer, factor);
+ /// Current default is 10Hz.
+ ///
+ /// The collected state is not immediatelly sent to the clients, rather it's
+ /// delayed so to be sent in batch. The states marked as important are
+ /// always collected.
+ int doll_epoch_collect_rate = 1;
+
+ /// The time rate at which a new batch is sent.
+ real_t doll_epoch_batch_sync_rate = 0.25;
+
+ /// The doll interpolator will try to keep a margin of error, so that network
+ /// oscillations doesn't make the dolls freeze.
+ ///
+ /// This margin of error is called `optimal_frame_delay` and it changes
+ /// depending on the connection health:
+ /// it can go from `doll_min_frames_delay` to `doll_max_frames_delay`.
+ int doll_min_frames_delay = 2;
+ int doll_max_frames_delay = 5;
+
+ /// Max speedup / slowdown the doll can apply to recover its epoch buffer size.
+ real_t doll_interpolation_max_speedup = 0.2;
+
+ /// The connection quality is established by watching the time passed
+ /// between each batch arrival.
+ /// The more this time is the same the more the connection health is good.
+ ///
+ /// The `doll_connection_stats_frame_span` defines how many frames have
+ /// to be used to establish the connection quality.
+ /// - Big values make the mechanism too slow.
+ /// - Small values make the mechanism too sensible.
+ /// The correct value should be give considering the
+ /// `doll_epoch_batch_sync_rate`.
+ int doll_connection_stats_frame_span = 30;
+
+ /// Sensitivity to network oscillations. The value is in seconds and can be
+ /// used to establish the connection quality.
+ ///
+ /// For each batch, the time needed for its arrival is traced; the standard
+ /// deviation of these is divided by `doll_net_sensitivity`: the result is
+ /// the connection quality.
+ ///
+ /// The more the time needed for each batch to arrive is different the
+ /// bigger this value is: when this value approaches to
+ /// `doll_net_sensitivity` (or even surpasses it) the bad the connection is.
+ ///
+ /// The result is the `connection_poorness` that goes from 0 to 1 and is
+ /// used to decide the `optimal_frames_delay` that is interpolated between
+ /// `doll_min_frames_delay` and `doll_max_frames_delay`.
+ real_t doll_net_sensitivity = 0.2;
+
+ /// Just after a bad connection phase, may happen that all the sent epochs
+ /// are received at once. To make sure the actor is always the more uptodate
+ /// possible, the number of epochs the actor has to fetch is clamped.
+ /// When `doll_max_delay` is surpassed the doll is teleported forward the
+ /// timeline so to be the more update possible.
+ uint64_t doll_max_delay = 60;
+
+ ControllerType controller_type = CONTROLLER_TYPE_NULL;
+ Controller *controller = nullptr;
+ DataBuffer inputs_buffer;
+
+ SceneSynchronizer *scene_synchronizer = nullptr;
+
+ bool packet_missing = false;
+ bool has_player_new_input = false;
+
+public:
+ static void _bind_methods();
+
+public:
+ NetworkedController();
+
+ void set_player_input_storage_size(int p_size);
+ int get_player_input_storage_size() const;
+
+ void set_max_redundant_inputs(int p_max);
+ int get_max_redundant_inputs() const;
+
+ void set_tick_speedup_notification_delay(real_t p_delay);
+ real_t get_tick_speedup_notification_delay() const;
+
+ void set_network_traced_frames(int p_size);
+ int get_network_traced_frames() const;
+
+ void set_min_frames_delay(int p_val);
+ int get_min_frames_delay() const;
+
+ void set_max_frames_delay(int p_val);
+ int get_max_frames_delay() const;
+
+ void set_net_sensitivity(real_t p_val);
+ real_t get_net_sensitivity() const;
+
+ void set_tick_acceleration(real_t p_acceleration);
+ real_t get_tick_acceleration() const;
+
+ void set_doll_epoch_collect_rate(int p_rate);
+ int get_doll_epoch_collect_rate() const;
+
+ void set_doll_epoch_batch_sync_rate(real_t p_rate);
+ real_t get_doll_epoch_batch_sync_rate() const;
+
+ void set_doll_min_frames_delay(int p_min);
+ int get_doll_min_frames_delay() const;
+
+ void set_doll_max_frames_delay(int p_max);
+ int get_doll_max_frames_delay() const;
+
+ void set_doll_interpolation_max_speedup(real_t p_speedup);
+ real_t get_doll_interpolation_max_speedup() const;
+
+ void set_doll_connection_stats_frame_span(int p_span);
+ int get_doll_connection_stats_frame_span() const;
+
+ void set_doll_net_sensitivity(real_t p_sensitivity);
+ real_t get_doll_net_sensitivity() const;
+
+ void set_doll_max_delay(uint32_t p_max_delay);
+ uint32_t get_doll_max_delay() const;
+
+ uint32_t get_current_input_id() const;
+
+ const DataBuffer &get_inputs_buffer() const {
+ return inputs_buffer;
+ }
+
+ DataBuffer &get_inputs_buffer_mut() {
+ return inputs_buffer;
+ }
+
+ /// Returns the pretended delta used by the player.
+ real_t player_get_pretended_delta(uint32_t p_iterations_per_seconds) const;
+
+ void mark_epoch_as_important();
+
+ void set_doll_collect_rate_factor(int p_peer, real_t p_factor);
+ void set_doll_peer_active(int p_peer_id, bool p_active);
+ void pause_notify_dolls();
+
+ bool process_instant(int p_i, real_t p_delta);
+
+ /// Returns the server controller or nullptr if this is not a server.
+ ServerController *get_server_controller();
+ const ServerController *get_server_controller() const;
+ /// Returns the player controller or nullptr if this is not a player.
+ PlayerController *get_player_controller();
+ const PlayerController *get_player_controller() const;
+ /// Returns the doll controller or nullptr if this is not a doll.
+ DollController *get_doll_controller();
+ const DollController *get_doll_controller() const;
+ /// Returns the no net controller or nullptr if this is not a no net.
+ NoNetController *get_nonet_controller();
+ const NoNetController *get_nonet_controller() const;
+
+ bool is_server_controller() const;
+ bool is_player_controller() const;
+ bool is_doll_controller() const;
+ bool is_nonet_controller() const;
+
+public:
+ void set_inputs_buffer(const BitArray &p_new_buffer, uint32_t p_metadata_size_in_bit, uint32_t p_size_in_bit);
+
+ void set_scene_synchronizer(SceneSynchronizer *p_synchronizer);
+ SceneSynchronizer *get_scene_synchronizer() const;
+ bool has_scene_synchronizer() const;
+
+ /* On server rpc functions. */
+ void _rpc_server_send_inputs(const Vector &p_data);
+
+ /* On client rpc functions. */
+ void _rpc_send_tick_additional_speed(const Vector &p_data);
+
+ /* On puppet rpc functions. */
+ void _rpc_doll_notify_sync_pause(uint32_t p_epoch);
+ void _rpc_doll_send_epoch_batch(const Vector &p_data);
+
+ void process(real_t p_delta);
+
+ void player_set_has_new_input(bool p_has);
+ bool player_has_new_input() const;
+
+ void __on_sync_paused();
+
+private:
+ virtual void _notification(int p_what);
+};
+
+struct FrameSnapshot {
+ uint32_t id;
+ BitArray inputs_buffer;
+ uint32_t buffer_size_bit;
+ uint32_t similarity;
+
+ bool operator==(const FrameSnapshot &p_other) const {
+ return p_other.id == id;
+ }
+};
+
+struct Controller {
+ NetworkedController *node;
+
+ Controller(NetworkedController *p_node) :
+ node(p_node) {}
+
+ virtual ~Controller() = default;
+
+ virtual void ready() {}
+ virtual uint32_t get_current_input_id() const = 0;
+
+ virtual void clear_peers() {}
+ virtual void activate_peer(int p_peer) {}
+ virtual void deactivate_peer(int p_peer) {}
+};
+
+struct ServerController : public Controller {
+ struct Peer {
+ Peer() = default;
+ Peer(int p_peer) :
+ peer(p_peer) {}
+
+ int peer = 0;
+ bool active = true;
+ real_t update_rate_factor = 1.0;
+ int collect_timer = 0; // In frames
+ int collect_threshold = 0; // In frames
+ LocalVector> epoch_batch;
+ uint32_t batch_size = 0;
+ };
+
+ uint32_t current_input_buffer_id = UINT32_MAX;
+ uint32_t ghost_input_count = 0;
+ uint32_t last_sent_state_input_id = 0;
+ real_t client_tick_additional_speed = 0.0;
+ real_t additional_speed_notif_timer = 0.0;
+ std::deque snapshots;
+ bool streaming_paused = false;
+ bool enabled = true;
+
+ uint32_t input_arrival_time = UINT32_MAX;
+ NetUtility::StatisticalRingBuffer network_watcher;
+
+ /// Used to sync the dolls.
+ LocalVector peers;
+ DataBuffer epoch_state_data_cache;
+ uint32_t epoch = 0;
+ bool is_epoch_important = false;
+ real_t batch_sync_timer = 0.0;
+
+ ServerController(
+ NetworkedController *p_node,
+ int p_traced_frames);
+
+ void process(real_t p_delta);
+ uint32_t last_known_input() const;
+ virtual uint32_t get_current_input_id() const override;
+
+ void set_enabled(bool p_enable);
+
+ virtual void clear_peers() override;
+ virtual void activate_peer(int p_peer) override;
+ virtual void deactivate_peer(int p_peer) override;
+
+ void receive_inputs(const Vector &p_data);
+ int get_inputs_count() const;
+
+ /// Fetch the next inputs, returns true if the input is new.
+ bool fetch_next_input();
+
+ void notify_send_state();
+
+ void doll_sync(real_t p_delta);
+
+ /// This function updates the `tick_additional_speed` so that the `frames_inputs`
+ /// size is enough to reduce the missing packets to 0.
+ ///
+ /// When the internet connection is bad, the packets need more time to arrive.
+ /// To heal this problem, the server tells the client to speed up a little bit
+ /// so it send the inputs a bit earlier than the usual.
+ ///
+ /// If the `frames_inputs` size is too big the input lag between the client and
+ /// the server is artificial and no more dependent on the internet. For this
+ /// reason the server tells the client to slowdown so to keep the `frames_inputs`
+ /// size moderate to the needs.
+ void calculates_player_tick_rate(real_t p_delta);
+ void adjust_player_tick_rate(real_t p_delta);
+
+ uint32_t find_peer(int p_peer) const;
+};
+
+struct PlayerController : public Controller {
+ uint32_t current_input_id;
+ uint32_t input_buffers_counter;
+ real_t time_bank;
+ real_t tick_additional_speed;
+ bool streaming_paused = false;
+
+ std::deque frames_snapshot;
+ LocalVector cached_packet_data;
+
+ PlayerController(NetworkedController *p_node);
+
+ void process(real_t p_delta);
+ int calculates_sub_ticks(real_t p_delta, real_t p_iteration_per_seconds);
+ int notify_input_checked(uint32_t p_input_id);
+ uint32_t last_known_input() const;
+ uint32_t get_stored_input_id(int p_i) const;
+ virtual uint32_t get_current_input_id() const override;
+
+ bool process_instant(int p_i, real_t p_delta);
+ real_t get_pretended_delta(real_t p_iteration_per_second) const;
+
+ void store_input_buffer(uint32_t p_id);
+
+ /// Sends an unreliable packet to the server, containing a packed array of
+ /// frame snapshots.
+ void send_frame_input_buffer_to_server();
+
+ bool can_accept_new_inputs() const;
+};
+
+/// The doll controller is kind of special controller, it's using a
+/// `ServerController` + `MastertController`.
+/// The `DollController` receives inputs from the client as the server does,
+/// and fetch them exactly like the server.
+/// After the execution of the inputs, the puppet start to act like the player,
+/// because it wait the player status from the server to correct its motion.
+///
+/// There are some extra features available that allow the doll to stay in sync
+/// with the server execution (see `soft_reset_to_server_state`) and the possibility
+/// for the server to stop the data streaming.
+struct DollController : public Controller {
+ Interpolator interpolator;
+ real_t additional_speed = 0.0;
+ uint32_t current_epoch = UINT32_MAX;
+ real_t advancing_epoch = 0.0;
+ uint32_t missing_epochs = 0;
+ // Any received epoch prior to this one is discarded.
+ uint32_t paused_epoch = 0;
+
+ // Used to track the time taken for the next batch to arrive.
+ uint32_t batch_receiver_timer = UINT32_MAX;
+ /// Used to track how network is performing.
+ NetUtility::StatisticalRingBuffer network_watcher;
+
+ DollController(NetworkedController *p_node);
+
+ virtual void ready() override;
+ void process(real_t p_delta);
+ // TODO consider make this non virtual
+ virtual uint32_t get_current_input_id() const override;
+
+ void receive_batch(const Vector &p_data);
+ uint32_t receive_epoch(const Vector &p_data);
+
+ uint32_t next_epoch();
+ void pause(uint32_t p_epoch);
+};
+
+/// This controller is used when the game instance is not a peer of any kind.
+/// This controller keeps the workflow as usual so it's possible to use the
+/// `NetworkedController` even without network.
+struct NoNetController : public Controller {
+ uint32_t frame_id;
+
+ NoNetController(NetworkedController *p_node);
+
+ void process(real_t p_delta);
+ virtual uint32_t get_current_input_id() const override;
+};
+
+#endif
diff --git a/modules/network_synchronizer/register_types.cpp b/modules/network_synchronizer/register_types.cpp
new file mode 100644
index 000000000000..e61c6bd95155
--- /dev/null
+++ b/modules/network_synchronizer/register_types.cpp
@@ -0,0 +1,60 @@
+/*************************************************************************/
+/* register_types.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "register_types.h"
+
+#include "core/config/project_settings.h"
+#include "data_buffer.h"
+#include "interpolator.h"
+#include "networked_controller.h"
+#include "scene_diff.h"
+#include "scene_synchronizer.h"
+
+void initialize_network_synchronizer_module(ModuleInitializationLevel p_level) {
+ if (p_level != MODULE_INITIALIZATION_LEVEL_SERVERS) {
+ return;
+ }
+
+ GDREGISTER_CLASS(DataBuffer);
+ GDREGISTER_CLASS(SceneDiff);
+ GDREGISTER_CLASS(Interpolator);
+ GDREGISTER_CLASS(NetworkedController);
+ GDREGISTER_CLASS(SceneSynchronizer);
+
+ GLOBAL_DEF("NetworkSynchronizer/debug_server_speedup", false);
+ GLOBAL_DEF("NetworkSynchronizer/debug_doll_speedup", false);
+}
+
+void uninitialize_network_synchronizer_module(ModuleInitializationLevel p_level) {
+}
diff --git a/modules/network_synchronizer/register_types.h b/modules/network_synchronizer/register_types.h
new file mode 100644
index 000000000000..d1c390a62f41
--- /dev/null
+++ b/modules/network_synchronizer/register_types.h
@@ -0,0 +1,37 @@
+/*************************************************************************/
+/* register_types.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+#include "modules/register_module_types.h"
+
+void initialize_network_synchronizer_module(ModuleInitializationLevel p_level);
+void uninitialize_network_synchronizer_module(ModuleInitializationLevel p_level);
diff --git a/modules/network_synchronizer/scene_diff.cpp b/modules/network_synchronizer/scene_diff.cpp
new file mode 100644
index 000000000000..a900215f3307
--- /dev/null
+++ b/modules/network_synchronizer/scene_diff.cpp
@@ -0,0 +1,165 @@
+/*************************************************************************/
+/* scene_diff.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "scene_diff.h"
+
+#include "scene/main/node.h"
+#include "scene_synchronizer.h"
+
+void SceneDiff::_bind_methods() {
+}
+
+void SceneDiff::start_tracking_scene_changes(
+ const LocalVector &p_nodes) {
+ start_tracking_count += 1;
+ if (start_tracking_count > 1) {
+ // Nothing to do, the tracking is already started.
+ return;
+ }
+
+ tracking.resize(p_nodes.size());
+
+ for (uint32_t i = 0; i < p_nodes.size(); i += 1) {
+ if (
+ p_nodes[i] == nullptr ||
+ // Check if this is a controller.
+ p_nodes[i]->is_controller ||
+ p_nodes[i]->controlled_by != nullptr) {
+ tracking[i].clear();
+ continue;
+ }
+
+#ifdef DEBUG_ENABLED
+ // This is never triggered because we always pass the `organized_node_data`
+ // array.
+ CRASH_COND(p_nodes[i]->id != i);
+ // This is never triggered because when the node is invalid the node data
+ // is destroyed.
+ CRASH_COND(p_nodes[i]->node == nullptr);
+#endif
+
+ tracking[i].resize(p_nodes[i]->vars.size());
+
+ for (uint32_t v = 0; v < p_nodes[i]->vars.size(); v += 1) {
+ // Take the current variable value and store it.
+ if (p_nodes[i]->vars[v].enabled && p_nodes[i]->vars[v].id != UINT32_MAX) {
+ // Note: Taking the value using `get` so to take the most updated
+ // value.
+ tracking[i][v] = p_nodes[i]->node->get(p_nodes[i]->vars[v].var.name).duplicate(true);
+ } else {
+ tracking[i][v] = Variant();
+ }
+ }
+ }
+}
+
+void SceneDiff::stop_tracking_scene_changes(const SceneSynchronizer *p_synchronizer) {
+ ERR_FAIL_COND_MSG(
+ start_tracking_count == 0,
+ "The tracking is not yet started on this SceneDiff, so can't be end.");
+
+ start_tracking_count -= 1;
+ if (start_tracking_count > 0) {
+ // Nothing to do, the tracking is still ongoing.
+ return;
+ }
+
+ if (p_synchronizer->get_biggest_node_id() == UINT32_MAX) {
+ // No nodes to track.
+ tracking.clear();
+ return;
+ }
+
+ if (tracking.size() > (p_synchronizer->get_biggest_node_id() + 1)) {
+ NET_DEBUG_ERR("[BUG] The tracked nodes are exceeding the sync nodes. Probably the sync is different or it has reset?");
+ tracking.clear();
+ return;
+ }
+
+ if (diff.size() < tracking.size()) {
+ // Make sure the diff has room to store the needed info.
+ diff.resize(tracking.size());
+ }
+
+ for (NetNodeId i = 0; i < tracking.size(); i += 1) {
+ const NetUtility::NodeData *nd = p_synchronizer->get_node_data(i);
+ if (nd == nullptr) {
+ continue;
+ }
+
+#ifdef DEBUG_ENABLED
+ // This is never triggered because we always pass the `organized_node_data`
+ // array.
+ CRASH_COND(nd->id != i);
+ // This is never triggered because when the node is invalid the node data
+ // is destroyed.
+ CRASH_COND(nd->node == nullptr);
+#endif
+
+ if (nd->vars.size() != tracking[i].size()) {
+ // These two arrays are different because the node was null
+ // during the start. So we can assume we are not tracking it.
+ continue;
+ }
+
+ if (diff[i].size() < tracking[i].size()) {
+ // Make sure the diff has room to store the variable info.
+ diff[i].resize(tracking[i].size());
+ }
+
+ for (uint32_t v = 0; v < tracking[i].size(); v += 1) {
+ if (nd->vars[v].id == UINT32_MAX || nd->vars[v].enabled == false) {
+ continue;
+ }
+
+ // Take the current variable value.
+ const Variant current_value =
+ nd->node->get(nd->vars[v].var.name);
+
+ // Compare the current value with the one taken during the start.
+ if (p_synchronizer->compare(
+ tracking[i][v],
+ current_value) == false) {
+ diff[i][v].is_different = true;
+ diff[i][v].value = current_value;
+ }
+ }
+ }
+
+ tracking.clear();
+}
+
+bool SceneDiff::is_tracking_in_progress() const {
+ return start_tracking_count > 0;
+}
diff --git a/modules/network_synchronizer/scene_diff.h b/modules/network_synchronizer/scene_diff.h
new file mode 100644
index 000000000000..84814bb88698
--- /dev/null
+++ b/modules/network_synchronizer/scene_diff.h
@@ -0,0 +1,71 @@
+/*************************************************************************/
+/* scene_diff.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#ifndef SCENE_DIFF_H
+#define SCENE_DIFF_H
+
+#include "core/object/class_db.h"
+#include "net_utilities.h"
+
+class SceneSynchronizer;
+
+struct VarDiff {
+ bool is_different = false;
+ Variant value;
+};
+
+/// This class is used to track the scene changes during a particular period of
+/// the frame. You can use it to generate partial FrameSnapshot that contains
+/// only portion of a change.
+class SceneDiff : public Object {
+ GDCLASS(SceneDiff, Object);
+
+ friend class SceneSynchronizer;
+
+ static void _bind_methods();
+
+ uint32_t start_tracking_count = 0;
+ LocalVector> tracking;
+ LocalVector> diff;
+
+public:
+ SceneDiff() = default;
+
+ void start_tracking_scene_changes(const LocalVector &p_nodes);
+ void stop_tracking_scene_changes(const SceneSynchronizer *p_synchronizer);
+
+ bool is_tracking_in_progress() const;
+};
+
+#endif // SCENE_DIFF_H
diff --git a/modules/network_synchronizer/scene_synchronizer.cpp b/modules/network_synchronizer/scene_synchronizer.cpp
new file mode 100644
index 000000000000..08e201656787
--- /dev/null
+++ b/modules/network_synchronizer/scene_synchronizer.cpp
@@ -0,0 +1,3053 @@
+/*************************************************************************/
+/* scene_synchronizer.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "scene_synchronizer.h"
+
+#include "core/multiplayer/multiplayer_api.h"
+#include "networked_controller.h"
+#include "scene/main/window.h"
+#include "scene_diff.h"
+
+void SceneSynchronizer::_bind_methods() {
+ BIND_ENUM_CONSTANT(CHANGE)
+ BIND_ENUM_CONSTANT(SYNC_RECOVER)
+ BIND_ENUM_CONSTANT(SYNC_RESET)
+ BIND_ENUM_CONSTANT(SYNC_REWIND)
+ BIND_ENUM_CONSTANT(END_SYNC)
+ BIND_ENUM_CONSTANT(DEFAULT)
+ BIND_ENUM_CONSTANT(SYNC)
+ BIND_ENUM_CONSTANT(ALWAYS)
+
+ ClassDB::bind_method(D_METHOD("reset_synchronizer_mode"), &SceneSynchronizer::reset_synchronizer_mode);
+ ClassDB::bind_method(D_METHOD("clear"), &SceneSynchronizer::clear);
+
+ ClassDB::bind_method(D_METHOD("set_server_notify_state_interval", "interval"), &SceneSynchronizer::set_server_notify_state_interval);
+ ClassDB::bind_method(D_METHOD("get_server_notify_state_interval"), &SceneSynchronizer::get_server_notify_state_interval);
+
+ ClassDB::bind_method(D_METHOD("set_comparison_float_tolerance", "tolerance"), &SceneSynchronizer::set_comparison_float_tolerance);
+ ClassDB::bind_method(D_METHOD("get_comparison_float_tolerance"), &SceneSynchronizer::get_comparison_float_tolerance);
+
+ ClassDB::bind_method(D_METHOD("register_node", "node"), &SceneSynchronizer::register_node_gdscript);
+ ClassDB::bind_method(D_METHOD("unregister_node", "node"), &SceneSynchronizer::unregister_node);
+ ClassDB::bind_method(D_METHOD("get_node_id", "node"), &SceneSynchronizer::get_node_id);
+ ClassDB::bind_method(D_METHOD("get_node_from_id", "id"), &SceneSynchronizer::get_node_from_id);
+
+ ClassDB::bind_method(D_METHOD("register_variable", "node", "variable", "on_change_notify", "flags"), &SceneSynchronizer::register_variable, DEFVAL(StringName()), DEFVAL(NetEventFlag::DEFAULT));
+ ClassDB::bind_method(D_METHOD("unregister_variable", "node", "variable"), &SceneSynchronizer::unregister_variable);
+ ClassDB::bind_method(D_METHOD("get_variable_id", "node", "variable"), &SceneSynchronizer::get_variable_id);
+
+ ClassDB::bind_method(D_METHOD("start_node_sync", "node"), &SceneSynchronizer::start_node_sync);
+ ClassDB::bind_method(D_METHOD("stop_node_sync", "node"), &SceneSynchronizer::stop_node_sync);
+ ClassDB::bind_method(D_METHOD("is_node_sync", "node"), &SceneSynchronizer::is_node_sync);
+
+ ClassDB::bind_method(D_METHOD("set_skip_rewinding", "node", "variable", "skip_rewinding"), &SceneSynchronizer::set_skip_rewinding);
+
+ ClassDB::bind_method(D_METHOD("track_variable_changes", "node", "variable", "object", "method", "flags"), &SceneSynchronizer::track_variable_changes, DEFVAL(NetEventFlag::DEFAULT));
+ ClassDB::bind_method(D_METHOD("untrack_variable_changes", "node", "variable", "object", "method"), &SceneSynchronizer::untrack_variable_changes);
+
+ ClassDB::bind_method(D_METHOD("set_node_as_controlled_by", "node", "controller"), &SceneSynchronizer::set_node_as_controlled_by);
+
+ ClassDB::bind_method(D_METHOD("controller_add_dependency", "controller", "node"), &SceneSynchronizer::controller_add_dependency);
+ ClassDB::bind_method(D_METHOD("controller_remove_dependency", "controller", "node"), &SceneSynchronizer::controller_remove_dependency);
+ ClassDB::bind_method(D_METHOD("controller_get_dependency_count", "controller"), &SceneSynchronizer::controller_get_dependency_count);
+ ClassDB::bind_method(D_METHOD("controller_get_dependency", "controller", "index"), &SceneSynchronizer::controller_get_dependency);
+
+ ClassDB::bind_method(D_METHOD("register_process", "node", "function"), &SceneSynchronizer::register_process);
+ ClassDB::bind_method(D_METHOD("unregister_process", "node", "function"), &SceneSynchronizer::unregister_process);
+
+ ClassDB::bind_method(D_METHOD("start_tracking_scene_changes", "diff_handle"), &SceneSynchronizer::start_tracking_scene_changes);
+ ClassDB::bind_method(D_METHOD("stop_tracking_scene_changes", "diff_handle"), &SceneSynchronizer::stop_tracking_scene_changes);
+ ClassDB::bind_method(D_METHOD("pop_scene_changes", "diff_handle"), &SceneSynchronizer::pop_scene_changes);
+ ClassDB::bind_method(D_METHOD("apply_scene_changes", "sync_data"), &SceneSynchronizer::apply_scene_changes);
+
+ ClassDB::bind_method(D_METHOD("is_recovered"), &SceneSynchronizer::is_recovered);
+ ClassDB::bind_method(D_METHOD("is_resetted"), &SceneSynchronizer::is_resetted);
+ ClassDB::bind_method(D_METHOD("is_rewinding"), &SceneSynchronizer::is_rewinding);
+ ClassDB::bind_method(D_METHOD("is_end_sync"), &SceneSynchronizer::is_end_sync);
+
+ ClassDB::bind_method(D_METHOD("force_state_notify"), &SceneSynchronizer::force_state_notify);
+
+ ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &SceneSynchronizer::set_enabled);
+ ClassDB::bind_method(D_METHOD("set_peer_networking_enable", "peer", "enabled"), &SceneSynchronizer::set_peer_networking_enable);
+ ClassDB::bind_method(D_METHOD("get_peer_networking_enable", "peer"), &SceneSynchronizer::is_peer_networking_enable);
+
+ ClassDB::bind_method(D_METHOD("is_server"), &SceneSynchronizer::is_server);
+ ClassDB::bind_method(D_METHOD("is_client"), &SceneSynchronizer::is_client);
+ ClassDB::bind_method(D_METHOD("is_networked"), &SceneSynchronizer::is_networked);
+
+ ClassDB::bind_method(D_METHOD("_on_peer_connected"), &SceneSynchronizer::_on_peer_connected);
+ ClassDB::bind_method(D_METHOD("_on_peer_disconnected"), &SceneSynchronizer::_on_peer_disconnected);
+
+ ClassDB::bind_method(D_METHOD("_on_node_removed"), &SceneSynchronizer::_on_node_removed);
+
+ ClassDB::bind_method(D_METHOD("_rpc_send_state"), &SceneSynchronizer::_rpc_send_state);
+ ClassDB::bind_method(D_METHOD("_rpc_notify_need_full_snapshot"), &SceneSynchronizer::_rpc_notify_need_full_snapshot);
+ ClassDB::bind_method(D_METHOD("_rpc_set_network_enabled", "enabled"), &SceneSynchronizer::_rpc_set_network_enabled);
+ ClassDB::bind_method(D_METHOD("_rpc_notify_peer_status", "enabled"), &SceneSynchronizer::_rpc_notify_peer_status);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "server_notify_state_interval", PROPERTY_HINT_RANGE, "0.001,10.0,0.0001"), "set_server_notify_state_interval", "get_server_notify_state_interval");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "comparison_float_tolerance", PROPERTY_HINT_RANGE, "0.000001,0.01,0.000001"), "set_comparison_float_tolerance", "get_comparison_float_tolerance");
+
+ ADD_SIGNAL(MethodInfo("sync_started"));
+ ADD_SIGNAL(MethodInfo("sync_paused"));
+}
+
+void SceneSynchronizer::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+ if (Engine::get_singleton()->is_editor_hint())
+ return;
+
+ // TODO add a signal that allows to not check this each frame.
+ if (unlikely(peer_ptr != get_multiplayer()->get_multiplayer_peer().ptr())) {
+ reset_synchronizer_mode();
+ }
+
+ const int lowest_priority_number = INT32_MAX;
+ ERR_FAIL_COND_MSG(get_process_priority() != lowest_priority_number, "The process priority MUST not be changed, it's likely there is a better way of doing what you are trying to do, if you really need it please open an issue.");
+
+ process();
+ } break;
+ case NOTIFICATION_ENTER_TREE: {
+ if (Engine::get_singleton()->is_editor_hint())
+ return;
+
+ clear();
+ reset_synchronizer_mode();
+
+ get_multiplayer()->connect(SNAME("peer_connected"), Callable(this, SNAME("_on_peer_connected")));
+ get_multiplayer()->connect(SNAME("peer_disconnected"), Callable(this, SNAME("_on_peer_disconnected")));
+
+ get_tree()->connect(SNAME("node_removed"), Callable(this, SNAME("_on_node_removed")));
+
+ // Make sure to reset all the assigned controllers.
+ reset_controllers();
+
+ // Init the peers already connected.
+ if (get_tree()->get_multiplayer()->get_multiplayer_peer().is_valid()) {
+ const Set peer_ids = get_tree()->get_multiplayer()->get_connected_peers();
+ for (Set::ConstIterator it = peer_ids.begin(); it != peer_ids.end(); ++it) {
+ _on_peer_connected(*it);
+ }
+ }
+
+ } break;
+ case NOTIFICATION_EXIT_TREE: {
+ if (Engine::get_singleton()->is_editor_hint())
+ return;
+
+ clear_peers();
+
+ get_multiplayer()->disconnect(SNAME("peer_connected"), Callable(this, SNAME("_on_peer_connected")));
+ get_multiplayer()->disconnect(SNAME("peer_disconnected"), Callable(this, SNAME("_on_peer_disconnected")));
+
+ get_tree()->disconnect(SNAME("node_removed"), Callable(this, SNAME("_on_node_removed")));
+
+ clear();
+
+ if (synchronizer) {
+ memdelete(synchronizer);
+ synchronizer = nullptr;
+ synchronizer_type = SYNCHRONIZER_TYPE_NULL;
+ }
+
+ set_physics_process_internal(false);
+
+ // Make sure to reset all the assigned controllers.
+ reset_controllers();
+ }
+ }
+}
+
+SceneSynchronizer::SceneSynchronizer() {
+ constexpr bool call_local = false;
+ rpc_config(SNAME("_rpc_send_state"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_RELIABLE);
+ rpc_config(SNAME("_rpc_notify_need_full_snapshot"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_RELIABLE);
+ rpc_config(SNAME("_rpc_set_network_enabled"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_RELIABLE);
+ rpc_config(SNAME("_rpc_notify_peer_status"), Multiplayer::RPC_MODE_ANY_PEER, call_local, Multiplayer::TRANSFER_MODE_RELIABLE);
+
+ // Avoid too much useless re-allocations.
+ event_listener.reserve(100);
+}
+
+SceneSynchronizer::~SceneSynchronizer() {
+ clear();
+ if (synchronizer) {
+ memdelete(synchronizer);
+ synchronizer = nullptr;
+ synchronizer_type = SYNCHRONIZER_TYPE_NULL;
+ }
+}
+
+void SceneSynchronizer::set_server_notify_state_interval(real_t p_interval) {
+ server_notify_state_interval = p_interval;
+}
+
+real_t SceneSynchronizer::get_server_notify_state_interval() const {
+ return server_notify_state_interval;
+}
+
+void SceneSynchronizer::set_comparison_float_tolerance(real_t p_tolerance) {
+ comparison_float_tolerance = p_tolerance;
+}
+
+real_t SceneSynchronizer::get_comparison_float_tolerance() const {
+ return comparison_float_tolerance;
+}
+
+NetUtility::NodeData *SceneSynchronizer::register_node(Node *p_node) {
+ ERR_FAIL_COND_V(p_node == nullptr, nullptr);
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ if (unlikely(nd == nullptr)) {
+ nd = memnew(NetUtility::NodeData);
+ nd->id = UINT32_MAX;
+ nd->instance_id = p_node->get_instance_id();
+ nd->node = p_node;
+
+ NetworkedController *controller = Object::cast_to(p_node);
+ if (controller) {
+ if (unlikely(controller->has_scene_synchronizer())) {
+ ERR_FAIL_V_MSG(nullptr, "This controller already has a synchronizer. This is a bug!");
+ }
+
+ nd->is_controller = true;
+ controller->set_scene_synchronizer(this);
+ dirty_peers();
+ }
+
+ add_node_data(nd);
+
+ NET_DEBUG_PRINT("New node registered" + (generate_id ? String(" #ID: ") + itos(nd->id) : "") + " : " + p_node->get_path());
+ }
+
+ return nd;
+}
+
+uint32_t SceneSynchronizer::register_node_gdscript(Node *p_node) {
+ NetUtility::NodeData *nd = register_node(p_node);
+ if (unlikely(nd == nullptr)) {
+ return UINT32_MAX;
+ }
+ return nd->id;
+}
+
+void SceneSynchronizer::unregister_node(Node *p_node) {
+ ERR_FAIL_COND(p_node == nullptr);
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ if (unlikely(nd == nullptr)) {
+ // Nothing to do.
+ return;
+ }
+
+ drop_node_data(nd);
+}
+
+uint32_t SceneSynchronizer::get_node_id(Node *p_node) {
+ ERR_FAIL_COND_V(p_node == nullptr, UINT32_MAX);
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND_V_MSG(nd == nullptr, UINT32_MAX, "This node " + p_node->get_path() + " is not yet registered, so there is not an available ID.");
+ return nd->id;
+}
+
+Node *SceneSynchronizer::get_node_from_id(uint32_t p_id) {
+ NetUtility::NodeData *nd = get_node_data(p_id);
+ ERR_FAIL_COND_V_MSG(nd == nullptr, nullptr, "The ID " + itos(p_id) + " is not assigned to any node.");
+ return nd->node;
+}
+
+void SceneSynchronizer::register_variable(Node *p_node, const StringName &p_variable, const StringName &p_on_change_notify, NetEventFlag p_flags) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_variable == StringName());
+
+ NetUtility::NodeData *node_data = register_node(p_node);
+ ERR_FAIL_COND(node_data == nullptr);
+
+ const int index = node_data->vars.find(p_variable);
+ if (index == -1) {
+ // The variable is not yet registered.
+ const Variant old_val = p_node->get(p_variable);
+ const int var_id = generate_id ? node_data->vars.size() : UINT32_MAX;
+ node_data->vars.push_back(
+ NetUtility::VarData(
+ var_id,
+ p_variable,
+ old_val,
+ false,
+ true));
+ } else {
+ // Make sure the var is active.
+ node_data->vars[index].enabled = true;
+ }
+
+#ifdef DEBUG_ENABLED
+ for (uint32_t v = 0; v < node_data->vars.size(); v += 1) {
+ // This can't happen, because the ID is always consecutive, or UINT32_MAX.
+ CRASH_COND(node_data->vars[v].id != v && node_data->vars[v].id != UINT32_MAX);
+ }
+#endif
+
+ if (p_on_change_notify != StringName()) {
+ track_variable_changes(p_node, p_variable, p_node, p_on_change_notify, p_flags);
+ }
+
+ if (synchronizer) {
+ synchronizer->on_variable_added(node_data, p_variable);
+ }
+}
+
+void SceneSynchronizer::unregister_variable(Node *p_node, const StringName &p_variable) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_variable == StringName());
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND(nd == nullptr);
+
+ const int64_t index = nd->vars.find(p_variable);
+ ERR_FAIL_COND(index == -1);
+
+ const NetVarId var_id = index;
+
+ // Never remove the variable values, because the order of the vars matters.
+ nd->vars[index].enabled = false;
+
+ for (int i = 0; i < nd->vars[var_id].change_listeners.size(); i += 1) {
+ const uint32_t event_index = nd->vars[var_id].change_listeners[i];
+ // Just erase the tracked variables without removing the listener to
+ // keep the order.
+ NetUtility::NodeChangeListener ncl;
+ ncl.node_data = nd;
+ ncl.var_id = var_id;
+ event_listener[event_index].watching_vars.erase(ncl);
+ }
+
+ nd->vars[index].change_listeners.clear();
+}
+
+void SceneSynchronizer::start_node_sync(const Node *p_node) {
+ ERR_FAIL_COND(p_node == nullptr);
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND(nd == nullptr);
+
+ nd->sync_enabled = true;
+}
+
+void SceneSynchronizer::stop_node_sync(const Node *p_node) {
+ ERR_FAIL_COND(p_node == nullptr);
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND(nd == nullptr);
+
+ nd->sync_enabled = false;
+}
+
+bool SceneSynchronizer::is_node_sync(const Node *p_node) const {
+ ERR_FAIL_COND_V(p_node == nullptr, false);
+
+ const NetUtility::NodeData *nd = find_node_data(p_node);
+ if (nd == nullptr) {
+ return false;
+ }
+
+ return nd->sync_enabled;
+}
+
+uint32_t SceneSynchronizer::get_variable_id(Node *p_node, const StringName &p_variable) {
+ ERR_FAIL_COND_V(p_node == nullptr, UINT32_MAX);
+ ERR_FAIL_COND_V(p_variable == StringName(), UINT32_MAX);
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND_V_MSG(nd == nullptr, UINT32_MAX, "This node " + p_node->get_path() + "is not registered.");
+
+ const int64_t index = nd->vars.find(p_variable);
+ ERR_FAIL_COND_V_MSG(index == -1, UINT32_MAX, "This variable " + p_node->get_path() + ":" + p_variable + " is not registered.");
+
+ return uint32_t(index);
+}
+
+void SceneSynchronizer::set_skip_rewinding(Node *p_node, const StringName &p_variable, bool p_skip_rewinding) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_variable == StringName());
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND(nd == nullptr);
+
+ const int64_t index = nd->vars.find(p_variable);
+ ERR_FAIL_COND(index == -1);
+
+ nd->vars[index].skip_rewinding = p_skip_rewinding;
+}
+
+void SceneSynchronizer::track_variable_changes(Node *p_node, const StringName &p_variable, Object *p_object, const StringName &p_method, NetEventFlag p_flags) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_variable == StringName());
+ ERR_FAIL_COND(p_method == StringName());
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND_MSG(nd == nullptr, "You need to register the variable to track its changes.");
+
+ const int64_t v = nd->vars.find(p_variable);
+ ERR_FAIL_COND_MSG(v == -1, "You need to register the variable to track its changes.");
+
+ const NetVarId var_id = v;
+
+ int64_t index;
+
+ {
+ NetUtility::ChangeListener listener;
+ listener.object_id = p_object->get_instance_id();
+ listener.method = p_method;
+
+ index = event_listener.find(listener);
+
+ if (-1 == index) {
+ // Add it.
+ listener.flag = p_flags;
+ listener.method_argument_count = UINT32_MAX;
+
+ // Search the method and get the argument count.
+ List methods;
+ p_object->get_method_list(&methods);
+ for (List::Element *e = methods.front(); e != nullptr; e = e->next()) {
+ if (e->get().name != p_method) {
+ continue;
+ }
+
+ listener.method_argument_count = e->get().arguments.size();
+
+ break;
+ }
+ ERR_FAIL_COND_MSG(listener.method_argument_count == UINT32_MAX, "The method " + p_method + " doesn't exist in this node: " + p_node->get_path());
+
+ index = event_listener.size();
+ event_listener.push_back(listener);
+ } else {
+ ERR_FAIL_COND_MSG(event_listener[index].flag != p_flags, "The event listener is already registered with the flag: " + itos(event_listener[index].flag) + ". You can't specify a different one.");
+ }
+ }
+
+ NetUtility::NodeChangeListener ncl;
+ ncl.node_data = nd;
+ ncl.var_id = var_id;
+
+ if (event_listener[index].watching_vars.find(ncl) != -1) {
+ return;
+ }
+
+ event_listener[index].watching_vars.push_back(ncl);
+ nd->vars[var_id].change_listeners.push_back(index);
+}
+
+void SceneSynchronizer::untrack_variable_changes(Node *p_node, const StringName &p_variable, Object *p_object, const StringName &p_method) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_variable == StringName());
+ ERR_FAIL_COND(p_method == StringName());
+
+ NetUtility::NodeData *nd = find_node_data(p_node);
+ ERR_FAIL_COND_MSG(nd == nullptr, "This not is not registered.");
+
+ const int64_t v = nd->vars.find(p_variable);
+ ERR_FAIL_COND_MSG(v == -1, "This variable is not registered.");
+
+ const NetVarId var_id = v;
+
+ NetUtility::ChangeListener listener;
+ listener.object_id = p_object->get_instance_id();
+ listener.method = p_method;
+
+ const int64_t index = event_listener.find(listener);
+
+ ERR_FAIL_COND_MSG(index == -1, "The variable is not know.");
+
+ NetUtility::NodeChangeListener ncl;
+ ncl.node_data = nd;
+ ncl.var_id = var_id;
+
+ event_listener[index].watching_vars.erase(ncl);
+ nd->vars[var_id].change_listeners.erase(index);
+
+ // Don't remove the listener to preserve the order.
+}
+
+void SceneSynchronizer::set_node_as_controlled_by(Node *p_node, Node *p_controller) {
+ NetUtility::NodeData *nd = register_node(p_node);
+ ERR_FAIL_COND(nd == nullptr);
+ ERR_FAIL_COND_MSG(nd->is_controller, "A controller can't be controlled by another controller.");
+
+ if (nd->controlled_by) {
+ // Put the node back into global.
+ nd->controlled_by->controlled_nodes.erase(nd);
+ nd->controlled_by = nullptr;
+ }
+
+ if (p_controller) {
+ NetworkedController *c = Object::cast_to(p_controller);
+ ERR_FAIL_COND_MSG(c == nullptr, "The controller must be a node of type: NetworkedController.");
+
+ NetUtility::NodeData *controller_node_data = register_node(p_controller);
+ ERR_FAIL_COND(controller_node_data == nullptr);
+ ERR_FAIL_COND_MSG(controller_node_data->is_controller == false, "The node can be only controlled by a controller.");
+
+#ifdef DEBUG_ENABLED
+ CRASH_COND_MSG(controller_node_data->controlled_nodes.find(nd) != -1, "There is a bug the same node is added twice into the controlled_nodes.");
+#endif
+ controller_node_data->controlled_nodes.push_back(nd);
+ nd->controlled_by = controller_node_data;
+ }
+
+#ifdef DEBUG_ENABLED
+ // Make sure that all controlled nodes are into the proper controller.
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ for (uint32_t y = 0; y < node_data_controllers[i]->controlled_nodes.size(); y += 1) {
+ CRASH_COND(node_data_controllers[i]->controlled_nodes[y]->controlled_by != node_data_controllers[i]);
+ }
+ }
+#endif
+}
+
+void SceneSynchronizer::controller_add_dependency(Node *p_controller, Node *p_node) {
+ if (is_client() == false) {
+ // Nothing to do.
+ return;
+ }
+
+ NetUtility::NodeData *controller_nd = find_node_data(p_controller);
+ ERR_FAIL_COND_MSG(controller_nd == nullptr, "The passed controller (" + p_controller->get_path() + ") is not registered.");
+ ERR_FAIL_COND_MSG(controller_nd->is_controller == false, "The node passed as controller (" + p_controller->get_path() + ") is not a controller.");
+
+ NetUtility::NodeData *node_nd = find_node_data(p_node);
+ ERR_FAIL_COND_MSG(node_nd == nullptr, "The passed node (" + p_node->get_path() + ") is not registered.");
+ ERR_FAIL_COND_MSG(node_nd->is_controller, "The node (" + p_node->get_path() + ") set as dependency is supposed to be just a node.");
+ ERR_FAIL_COND_MSG(node_nd->controlled_by != nullptr, "The node (" + p_node->get_path() + ") set as dependency is supposed to be just a node.");
+
+ const int64_t index = controller_nd->dependency_nodes.find(node_nd);
+ if (index == -1) {
+ controller_nd->dependency_nodes.push_back(node_nd);
+ controller_nd->dependency_nodes_end.push_back(UINT32_MAX);
+ } else {
+ // We already have this dependency, just make sure we don't delete it.
+ controller_nd->dependency_nodes_end[index] = UINT32_MAX;
+ }
+}
+
+void SceneSynchronizer::controller_remove_dependency(Node *p_controller, Node *p_node) {
+ if (is_client() == false) {
+ // Nothing to do.
+ return;
+ }
+
+ NetUtility::NodeData *controller_nd = find_node_data(p_controller);
+ ERR_FAIL_COND_MSG(controller_nd == nullptr, "The passed controller (" + p_controller->get_path() + ") is not registered.");
+ ERR_FAIL_COND_MSG(controller_nd->is_controller == false, "The node passed as controller (" + p_controller->get_path() + ") is not a controller.");
+
+ NetUtility::NodeData *node_nd = find_node_data(p_node);
+ ERR_FAIL_COND_MSG(node_nd == nullptr, "The passed node (" + p_node->get_path() + ") is not registered.");
+ ERR_FAIL_COND_MSG(node_nd->is_controller, "The node (" + p_node->get_path() + ") set as dependency is supposed to be just a node.");
+ ERR_FAIL_COND_MSG(node_nd->controlled_by != nullptr, "The node (" + p_node->get_path() + ") set as dependency is supposed to be just a node.");
+
+ const int64_t index = controller_nd->dependency_nodes.find(node_nd);
+ if (index == -1) {
+ // Nothing to do, this node is not a dependency.
+ return;
+ }
+
+ // Instead to remove the dependency immeditaly we have to postpone it till
+ // the server confirms the valitity via state.
+ // This operation is required otherwise the dependency is remvoved too early,
+ // and an eventual rewind may miss it.
+ // The actual removal is performed at the end of the sync.
+ controller_nd->dependency_nodes_end[index] =
+ static_cast(controller_nd->node)->get_current_input_id();
+}
+
+int SceneSynchronizer::controller_get_dependency_count(Node *p_controller) const {
+ if (is_client() == false) {
+ // Nothing to do.
+ return 0;
+ }
+
+ const NetUtility::NodeData *controller_nd = find_node_data(p_controller);
+ ERR_FAIL_COND_V_MSG(controller_nd == nullptr, 0, "The passed controller (" + p_controller->get_path() + ") is not registered.");
+ ERR_FAIL_COND_V_MSG(controller_nd->is_controller == false, 0, "The node passed as controller (" + p_controller->get_path() + ") is not a controller.");
+ return controller_nd->dependency_nodes.size();
+}
+
+Node *SceneSynchronizer::controller_get_dependency(Node *p_controller, int p_index) {
+ if (is_client() == false) {
+ // Nothing to do.
+ return nullptr;
+ }
+
+ NetUtility::NodeData *controller_nd = find_node_data(p_controller);
+ ERR_FAIL_COND_V_MSG(controller_nd == nullptr, nullptr, "The passed controller (" + p_controller->get_path() + ") is not registered.");
+ ERR_FAIL_COND_V_MSG(controller_nd->is_controller == false, nullptr, "The node passed as controller (" + p_controller->get_path() + ") is not a controller.");
+ ERR_FAIL_INDEX_V(p_index, int(controller_nd->dependency_nodes.size()), nullptr);
+
+ return controller_nd->dependency_nodes[p_index]->node;
+}
+
+void SceneSynchronizer::register_process(Node *p_node, const StringName &p_function) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_function == StringName());
+ NetUtility::NodeData *node_data = register_node(p_node);
+ ERR_FAIL_COND(node_data == nullptr);
+
+ if (node_data->functions.find(p_function) == -1) {
+ node_data->functions.push_back(p_function);
+ }
+}
+
+void SceneSynchronizer::unregister_process(Node *p_node, const StringName &p_function) {
+ ERR_FAIL_COND(p_node == nullptr);
+ ERR_FAIL_COND(p_function == StringName());
+ NetUtility::NodeData *node_data = register_node(p_node);
+ ERR_FAIL_COND(node_data == nullptr);
+ node_data->functions.erase(p_function);
+}
+
+void SceneSynchronizer::start_tracking_scene_changes(Object *p_diff_handle) const {
+ ERR_FAIL_COND_MSG(get_tree()->get_multiplayer()->is_server() == false, "This function is supposed to be called only on server.");
+ SceneDiff *diff = Object::cast_to(p_diff_handle);
+ ERR_FAIL_COND_MSG(diff == nullptr, "The object is not a SceneDiff class.");
+
+ diff->start_tracking_scene_changes(organized_node_data);
+}
+
+void SceneSynchronizer::stop_tracking_scene_changes(Object *p_diff_handle) const {
+ ERR_FAIL_COND_MSG(get_tree()->get_multiplayer()->is_server() == false, "This function is supposed to be called only on server.");
+ SceneDiff *diff = Object::cast_to(p_diff_handle);
+ ERR_FAIL_COND_MSG(diff == nullptr, "The object is not a SceneDiff class.");
+
+ diff->stop_tracking_scene_changes(this);
+}
+
+Variant SceneSynchronizer::pop_scene_changes(Object *p_diff_handle) const {
+ ERR_FAIL_COND_V_MSG(
+ synchronizer_type != SYNCHRONIZER_TYPE_SERVER,
+ Variant(),
+ "This function is supposed to be called only on server.");
+
+ SceneDiff *diff = Object::cast_to(p_diff_handle);
+ ERR_FAIL_COND_V_MSG(
+ diff == nullptr,
+ Variant(),
+ "The object is not a SceneDiff class.");
+
+ ERR_FAIL_COND_V_MSG(
+ diff->is_tracking_in_progress(),
+ Variant(),
+ "You can't pop the changes while the tracking is still in progress.");
+
+ // Generates a sync_data and returns it.
+ Vector ret;
+ for (NetNodeId node_id = 0; node_id < diff->diff.size(); node_id += 1) {
+ if (diff->diff[node_id].size() == 0) {
+ // Nothing to do.
+ continue;
+ }
+
+ bool node_id_in_ret = false;
+ for (NetVarId var_id = 0; var_id < diff->diff[node_id].size(); var_id += 1) {
+ if (diff->diff[node_id][var_id].is_different == false) {
+ continue;
+ }
+ if (node_id_in_ret == false) {
+ node_id_in_ret = true;
+ // Set the node id.
+ ret.push_back(node_id);
+ }
+ ret.push_back(var_id);
+ ret.push_back(diff->diff[node_id][var_id].value);
+ }
+ if (node_id_in_ret) {
+ // Close the Node data.
+ ret.push_back(Variant());
+ }
+ }
+
+ // Clear the diff data.
+ diff->diff.clear();
+
+ return ret.size() > 0 ? Variant(ret) : Variant();
+}
+
+void SceneSynchronizer::apply_scene_changes(const Variant &p_sync_data) {
+ ERR_FAIL_COND_MSG(is_client() == false, "This function is not supposed to be called on server.");
+
+ ClientSynchronizer *client_sync = static_cast(synchronizer);
+
+ change_events_begin(NetEventFlag::CHANGE);
+
+ const bool success = client_sync->parse_sync_data(
+ p_sync_data,
+ this,
+
+ // Parse the Node:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data) {},
+
+ // Parse controller:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_input_id) {},
+
+ // Parse variable:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_var_id, const Variant &p_value) {
+ SceneSynchronizer *scene_sync = static_cast(p_user_pointer);
+
+ const Variant current_val = p_node_data->vars[p_var_id].var.value;
+
+ if (scene_sync->compare(current_val, p_value) == false) {
+ // There is a difference.
+ // Set the new value.
+ p_node_data->vars[p_var_id].var.value = p_value;
+ p_node_data->node->set(
+ p_node_data->vars[p_var_id].var.name,
+ p_value);
+
+ // Add an event.
+ scene_sync->change_event_add(
+ p_node_data,
+ p_var_id,
+ current_val);
+ }
+ });
+
+ if (success == false) {
+ NET_DEBUG_ERR("Scene changes:");
+ NET_DEBUG_ERR(p_sync_data.stringify());
+ }
+
+ change_events_flush();
+}
+
+bool SceneSynchronizer::is_recovered() const {
+ return recover_in_progress;
+}
+
+bool SceneSynchronizer::is_resetted() const {
+ return reset_in_progress;
+}
+
+bool SceneSynchronizer::is_rewinding() const {
+ return rewinding_in_progress;
+}
+
+bool SceneSynchronizer::is_end_sync() const {
+ return end_sync;
+}
+
+void SceneSynchronizer::force_state_notify() {
+ ERR_FAIL_COND(is_server() == false);
+ ServerSynchronizer *r = static_cast(synchronizer);
+ // + 1.0 is just a ridiculous high number to be sure to avoid float
+ // precision error.
+ r->state_notifier_timer = get_server_notify_state_interval() + 1.0;
+}
+
+void SceneSynchronizer::dirty_peers() {
+ peer_dirty = true;
+}
+
+void SceneSynchronizer::set_enabled(bool p_enable) {
+ ERR_FAIL_COND_MSG(synchronizer_type == SYNCHRONIZER_TYPE_SERVER, "The server is always enabled.");
+ if (synchronizer_type == SYNCHRONIZER_TYPE_CLIENT) {
+ rpc_id(1, SNAME("_rpc_set_network_enabled"), p_enable);
+ if (p_enable == false) {
+ // If the peer want to disable, we can disable it locally
+ // immediately. When it wants to enable the networking, the server
+ // must be notified so it decides when to start the networking
+ // again.
+ static_cast(synchronizer)->set_enabled(p_enable);
+ }
+ } else if (synchronizer_type == SYNCHRONIZER_TYPE_NONETWORK) {
+ set_peer_networking_enable(0, p_enable);
+ }
+}
+
+bool SceneSynchronizer::is_enabled() const {
+ ERR_FAIL_COND_V_MSG(synchronizer_type == SYNCHRONIZER_TYPE_SERVER, false, "The server is always enabled.");
+ if (likely(synchronizer_type == SYNCHRONIZER_TYPE_CLIENT)) {
+ return static_cast(synchronizer)->enabled;
+ } else if (synchronizer_type == SYNCHRONIZER_TYPE_NONETWORK) {
+ return static_cast(synchronizer)->enabled;
+ } else {
+ return true;
+ }
+}
+
+void SceneSynchronizer::set_peer_networking_enable(int p_peer, bool p_enable) {
+ if (synchronizer_type == SYNCHRONIZER_TYPE_SERVER) {
+ ERR_FAIL_COND_MSG(p_peer == 1, "Disable the server is not possible.");
+
+ NetUtility::PeerData *pd = peer_data.lookup_ptr(p_peer);
+ ERR_FAIL_COND_MSG(pd == nullptr, "The peer: " + itos(p_peer) + " is not know. [bug]");
+
+ if (pd->enabled == p_enable) {
+ // Nothing to do.
+ return;
+ }
+
+ pd->enabled = p_enable;
+ // Set to true, so next time this peer connects a full snapshot is sent.
+ pd->force_notify_snapshot = true;
+ pd->need_full_snapshot = true;
+
+ dirty_peers();
+
+ // Just notify the peer status.
+ rpc_id(p_peer, SNAME("_rpc_notify_peer_status"), p_enable);
+ } else {
+ ERR_FAIL_COND_MSG(synchronizer_type != SYNCHRONIZER_TYPE_NONETWORK, "At this point no network is expected.");
+ static_cast(synchronizer)->set_enabled(p_enable);
+ }
+}
+
+bool SceneSynchronizer::is_peer_networking_enable(int p_peer) const {
+ if (synchronizer_type == SYNCHRONIZER_TYPE_SERVER) {
+ if (p_peer == 1) {
+ // Server is always enabled.
+ return true;
+ }
+
+ const NetUtility::PeerData *pd = peer_data.lookup_ptr(p_peer);
+ ERR_FAIL_COND_V_MSG(pd == nullptr, false, "The peer: " + itos(p_peer) + " is not know. [bug]");
+ return pd->enabled;
+ } else {
+ ERR_FAIL_COND_V_MSG(synchronizer_type != SYNCHRONIZER_TYPE_NONETWORK, false, "At this point no network is expected.");
+ return static_cast(synchronizer)->is_enabled();
+ }
+}
+
+void SceneSynchronizer::_on_peer_connected(int p_peer) {
+ peer_data.insert(p_peer, NetUtility::PeerData());
+ dirty_peers();
+}
+
+void SceneSynchronizer::_on_peer_disconnected(int p_peer) {
+ peer_data.remove(p_peer);
+
+ // Notify all controllers that this peer is gone.
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ NetworkedController *c = static_cast(node_data_controllers[i]->node);
+ c->controller->deactivate_peer(p_peer);
+ }
+}
+
+void SceneSynchronizer::_on_node_removed(Node *p_node) {
+ unregister_node(p_node);
+}
+
+void SceneSynchronizer::reset_synchronizer_mode() {
+ set_physics_process_internal(false);
+ const bool was_generating_ids = generate_id;
+ generate_id = false;
+
+ if (synchronizer) {
+ memdelete(synchronizer);
+ synchronizer = nullptr;
+ synchronizer_type = SYNCHRONIZER_TYPE_NULL;
+ }
+
+ peer_ptr = get_multiplayer() == nullptr ? nullptr : get_multiplayer()->get_multiplayer_peer().ptr();
+
+ if (get_tree() == nullptr || get_tree()->get_multiplayer()->get_multiplayer_peer().is_null()) {
+ synchronizer_type = SYNCHRONIZER_TYPE_NONETWORK;
+ synchronizer = memnew(NoNetSynchronizer(this));
+ generate_id = true;
+
+ } else if (get_tree()->get_multiplayer()->is_server()) {
+ synchronizer_type = SYNCHRONIZER_TYPE_SERVER;
+ synchronizer = memnew(ServerSynchronizer(this));
+ generate_id = true;
+ } else {
+ synchronizer_type = SYNCHRONIZER_TYPE_CLIENT;
+ synchronizer = memnew(ClientSynchronizer(this));
+ }
+
+ // Always runs the SceneSynchronizer last.
+ const int lowest_priority_number = INT32_MAX;
+ set_process_priority(lowest_priority_number);
+ set_physics_process_internal(true);
+
+ if (was_generating_ids != generate_id) {
+ organized_node_data.resize(node_data.size());
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (node_data[i] == nullptr) {
+ continue;
+ }
+
+ // Handle the node ID.
+ if (generate_id) {
+ node_data[i]->id = i;
+ organized_node_data[i] = node_data[i];
+ } else {
+ node_data[i]->id = UINT32_MAX;
+ organized_node_data[i] = nullptr;
+ }
+
+ // Handle the variables ID.
+ for (uint32_t v = 0; v < node_data[i]->vars.size(); v += 1) {
+ if (generate_id) {
+ node_data[i]->vars[v].id = v;
+ } else {
+ node_data[i]->vars[v].id = UINT32_MAX;
+ }
+ }
+ }
+ }
+
+ // Notify the presence all available nodes and its variables to the synchronizer.
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ synchronizer->on_node_added(node_data[i]);
+ for (uint32_t y = 0; y < node_data[i]->vars.size(); y += 1) {
+ synchronizer->on_variable_added(node_data[i], node_data[i]->vars[y].var.name);
+ }
+ }
+
+ // Reset the controllers.
+ reset_controllers();
+}
+
+void SceneSynchronizer::clear() {
+ // Drop the node_data.
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (node_data[i] != nullptr) {
+ drop_node_data(node_data[i]);
+ }
+ }
+
+ node_data.reset();
+ organized_node_data.reset();
+ node_data_controllers.reset();
+ event_listener.reset();
+
+ // Avoid too much useless re-allocations.
+ event_listener.reserve(100);
+
+ if (synchronizer) {
+ synchronizer->clear();
+ }
+}
+
+void SceneSynchronizer::_rpc_send_state(const Variant &p_snapshot) {
+ ERR_FAIL_COND_MSG(is_client() == false, "Only clients are suposed to receive the server snapshot.");
+ static_cast(synchronizer)->receive_snapshot(p_snapshot);
+}
+
+void SceneSynchronizer::_rpc_notify_need_full_snapshot() {
+ ERR_FAIL_COND_MSG(is_server() == false, "Only the server can receive the request to send a full snapshot.");
+
+ const int sender_peer = get_tree()->get_multiplayer()->get_remote_sender_id();
+ NetUtility::PeerData *pd = peer_data.lookup_ptr(sender_peer);
+ ERR_FAIL_COND(pd == nullptr);
+ pd->need_full_snapshot = true;
+}
+
+void SceneSynchronizer::_rpc_set_network_enabled(bool p_enabled) {
+ ERR_FAIL_COND_MSG(is_server() == false, "The peer status is supposed to be received by the server.");
+ set_peer_networking_enable(
+ get_multiplayer()->get_remote_sender_id(),
+ p_enabled);
+}
+
+void SceneSynchronizer::_rpc_notify_peer_status(bool p_enabled) {
+ ERR_FAIL_COND_MSG(is_client() == false, "The peer status is supposed to be received by the client.");
+ static_cast(synchronizer)->set_enabled(p_enabled);
+}
+
+void SceneSynchronizer::update_peers() {
+#ifdef DEBUG_ENABLED
+ // This function is only called on server.
+ CRASH_COND(synchronizer_type != SYNCHRONIZER_TYPE_SERVER);
+#endif
+
+ if (likely(peer_dirty == false)) {
+ return;
+ }
+
+ peer_dirty = false;
+
+ for (OAHashMap::Iterator it = peer_data.iter();
+ it.valid;
+ it = peer_data.next_iter(it)) {
+ // Validate the peer.
+ if (it.value->controller_id != UINT32_MAX) {
+ NetUtility::NodeData *nd = get_node_data(it.value->controller_id);
+ if (nd == nullptr ||
+ nd->is_controller == false ||
+ nd->node->get_multiplayer_authority() != (*it.key)) {
+ // Invalidate the controller id
+ it.value->controller_id = UINT32_MAX;
+ }
+ }
+
+ if (it.value->controller_id == UINT32_MAX) {
+ // The controller_id is not assigned, search it.
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ if (node_data_controllers[i]->node->get_multiplayer_authority() == (*it.key)) {
+ // Controller found.
+ it.value->controller_id = node_data_controllers[i]->id;
+ break;
+ }
+ }
+ }
+
+ // Propagate the peer change to controllers.
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ NetworkedController *c = static_cast(node_data_controllers[i]->node);
+
+ if (it.value->controller_id == node_data_controllers[i]->id) {
+ // This is the controller owned by this peer.
+ c->get_server_controller()->set_enabled(it.value->enabled);
+ } else {
+ // This is a controller owned by another peer.
+ if (it.value->enabled) {
+ c->controller->activate_peer(*it.key);
+ } else {
+ c->controller->deactivate_peer(*it.key);
+ }
+ }
+ }
+ }
+}
+
+void SceneSynchronizer::clear_peers() {
+ peer_data.clear();
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ NetworkedController *c = static_cast(node_data_controllers[i]->node);
+ c->controller->clear_peers();
+ }
+}
+
+void SceneSynchronizer::change_events_begin(int p_flag) {
+#ifdef DEBUG_ENABLED
+ // This can't happen because at the end these are reset.
+ CRASH_COND(recover_in_progress);
+ CRASH_COND(reset_in_progress);
+ CRASH_COND(rewinding_in_progress);
+ CRASH_COND(end_sync);
+#endif
+ event_flag = p_flag;
+ recover_in_progress = NetEventFlag::SYNC & p_flag;
+ reset_in_progress = NetEventFlag::SYNC_RESET & p_flag;
+ rewinding_in_progress = NetEventFlag::SYNC_REWIND & p_flag;
+ end_sync = NetEventFlag::END_SYNC & p_flag;
+}
+
+void SceneSynchronizer::change_event_add(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old) {
+ for (int i = 0; i < p_node_data->vars[p_var_id].change_listeners.size(); i += 1) {
+ const uint32_t listener_index = p_node_data->vars[p_var_id].change_listeners[i];
+ NetUtility::ChangeListener &listener = event_listener[listener_index];
+ if ((listener.flag & event_flag) == 0) {
+ // Not listening to this event.
+ continue;
+ }
+
+ listener.emitted = false;
+
+ NetUtility::NodeChangeListener ncl;
+ ncl.node_data = p_node_data;
+ ncl.var_id = p_var_id;
+
+ const int64_t index = listener.watching_vars.find(ncl);
+#ifdef DEBUG_ENABLED
+ // This can't never happen because the `NodeData::change_listeners`
+ // tracks the correct listener.
+ CRASH_COND(index == -1);
+#endif
+ listener.watching_vars[index].old_value = p_old;
+ listener.watching_vars[index].old_set = true;
+ }
+
+ // Notify the synchronizer.
+ if (synchronizer) {
+ synchronizer->on_variable_changed(
+ p_node_data,
+ p_var_id,
+ p_old,
+ event_flag);
+ }
+}
+
+void SceneSynchronizer::change_events_flush() {
+ LocalVector vars;
+ LocalVector vars_ptr;
+
+ // TODO this can be optimized by storing the changed listener in a separate
+ // vector. This change must be inserted into the `change_event_add`.
+ for (uint32_t listener_i = 0; listener_i < event_listener.size(); listener_i += 1) {
+ NetUtility::ChangeListener &listener = event_listener[listener_i];
+ if (listener.emitted) {
+ continue;
+ }
+ listener.emitted = true;
+
+ Object *obj = ObjectDB::get_instance(listener.object_id);
+ if (obj == nullptr) {
+ // Setting the flag to 0 so no events trigger this anymore.
+ listener.flag = NetEventFlag::EMPTY;
+ listener.object_id = ObjectID();
+ listener.method = StringName();
+
+ // Make sure this listener is not tracking any variable.
+ for (uint32_t wv = 0; wv < listener.watching_vars.size(); wv += 1) {
+ NetUtility::NodeData *nd = listener.watching_vars[wv].node_data;
+ uint32_t var_id = listener.watching_vars[wv].var_id;
+ nd->vars[var_id].change_listeners.erase(listener_i);
+ }
+ listener.watching_vars.clear();
+ continue;
+ }
+
+ // Initialize the arguments
+ ERR_CONTINUE_MSG(listener.method_argument_count > listener.watching_vars.size(), "This method " + listener.method + " has more arguments than the watched variables. This listener is broken.");
+
+ vars.resize(MIN(listener.watching_vars.size(), listener.method_argument_count));
+ vars_ptr.resize(vars.size());
+ for (uint32_t v = 0; v < MIN(listener.watching_vars.size(), listener.method_argument_count); v += 1) {
+ if (listener.watching_vars[v].old_set) {
+ vars[v] = listener.watching_vars[v].old_value;
+ listener.watching_vars[v].old_set = false;
+ } else {
+ // This value is not changed, so just retrive the current one.
+ vars[v] = listener.watching_vars[v].node_data->vars[listener.watching_vars[v].var_id].var.value;
+ }
+ vars_ptr[v] = vars.ptr() + v;
+ }
+
+ Callable::CallError e;
+ obj->callp(listener.method, vars_ptr.ptr(), vars_ptr.size(), e);
+ }
+
+ recover_in_progress = false;
+ reset_in_progress = false;
+ rewinding_in_progress = false;
+ end_sync = false;
+}
+
+void SceneSynchronizer::add_node_data(NetUtility::NodeData *p_node_data) {
+ if (generate_id) {
+#ifdef DEBUG_ENABLED
+ // When generate_id is true, the id must always be undefined.
+ CRASH_COND(p_node_data->id != UINT32_MAX);
+#endif
+ p_node_data->id = organized_node_data.size();
+ }
+
+#ifdef DEBUG_ENABLED
+ // Make sure the registered nodes have an unique ID.
+ // Due to an engine bug, it's possible to have two different nodes with the
+ // exact same path:
+ // - Create a scene.
+ // - Add a child with the name `BadChild`.
+ // - Instance the scene into another scene.
+ // - Add a child, under the instanced scene, with the name `BadChild`.
+ // Now you have the scene with two different nodes but same path.
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (node_data[i]->node->get_path() == p_node_data->node->get_path()) {
+ NET_DEBUG_ERR("You have two different nodes with the same path: " + p_node_data->node->get_path() + ". This will cause troubles. Fix it.");
+ break;
+ }
+ }
+#endif
+
+ node_data.push_back(p_node_data);
+
+ if (generate_id) {
+ organized_node_data.push_back(p_node_data);
+ } else {
+ if (p_node_data->id != UINT32_MAX) {
+ // This node has an ID, make sure to organize it properly.
+
+ if (organized_node_data.size() <= p_node_data->id) {
+ expand_organized_node_data_vector((p_node_data->id + 1) - organized_node_data.size());
+ }
+
+ organized_node_data[p_node_data->id] = p_node_data;
+ }
+ }
+
+ if (p_node_data->is_controller) {
+ node_data_controllers.push_back(p_node_data);
+ reset_controller(p_node_data);
+ }
+
+ if (synchronizer) {
+ synchronizer->on_node_added(p_node_data);
+ }
+}
+
+void SceneSynchronizer::drop_node_data(NetUtility::NodeData *p_node_data) {
+ if (synchronizer) {
+ synchronizer->on_node_removed(p_node_data);
+ }
+
+ if (p_node_data->controlled_by) {
+ // This node is controlled by another one, remove from that node.
+ p_node_data->controlled_by->controlled_nodes.erase(p_node_data);
+ p_node_data->controlled_by = nullptr;
+ }
+
+ if (p_node_data->is_controller) {
+ // This is a controller, make sure to reset the peers.
+ static_cast(p_node_data->node)->set_scene_synchronizer(nullptr);
+ dirty_peers();
+ node_data_controllers.erase(p_node_data);
+ }
+
+ node_data.erase(p_node_data);
+
+ if (p_node_data->id < organized_node_data.size()) {
+ // Never resize this vector to keep it sort.
+ organized_node_data[p_node_data->id] = nullptr;
+ }
+
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ const int64_t index = node_data_controllers[i]->dependency_nodes.find(p_node_data);
+ if (index != -1) {
+ node_data_controllers[i]->dependency_nodes.remove_at_unordered(index);
+ node_data_controllers[i]->dependency_nodes_end.remove_at_unordered(index);
+ }
+ }
+
+ // Remove this `NodeData` from any event listener.
+ for (uint32_t i = 0; i < event_listener.size(); i += 1) {
+ while (true) {
+ uint32_t index_to_remove = UINT32_MAX;
+
+ // Search.
+ for (uint32_t v = 0; v < event_listener[i].watching_vars.size(); v += 1) {
+ if (event_listener[i].watching_vars[v].node_data == p_node_data) {
+ index_to_remove = v;
+ break;
+ }
+ }
+
+ if (index_to_remove == UINT32_MAX) {
+ // Nothing more to do.
+ break;
+ } else {
+ event_listener[i].watching_vars.remove_at_unordered(index_to_remove);
+ }
+ }
+ }
+
+ memdelete(p_node_data);
+}
+
+void SceneSynchronizer::set_node_data_id(NetUtility::NodeData *p_node_data, NetNodeId p_id) {
+#ifdef DEBUG_ENABLED
+ CRASH_COND_MSG(generate_id, "This function is not supposed to be called, because this instance is generating the IDs");
+#endif
+ if (organized_node_data.size() <= p_id) {
+ expand_organized_node_data_vector((p_id + 1) - organized_node_data.size());
+ }
+ p_node_data->id = p_id;
+ organized_node_data[p_id] = p_node_data;
+ NET_DEBUG_PRINT("NetNodeId: " + itos(p_id) + " just assigned to: " + p_node_data->node->get_path());
+}
+
+bool SceneSynchronizer::compare(const Vector2 &p_first, const Vector2 &p_second) const {
+ return compare(p_first, p_second, comparison_float_tolerance);
+}
+
+bool SceneSynchronizer::compare(const Vector3 &p_first, const Vector3 &p_second) const {
+ return compare(p_first, p_second, comparison_float_tolerance);
+}
+
+bool SceneSynchronizer::compare(const Variant &p_first, const Variant &p_second) const {
+ return compare(p_first, p_second, comparison_float_tolerance);
+}
+
+bool SceneSynchronizer::compare(const Vector2 &p_first, const Vector2 &p_second, real_t p_tolerance) {
+ return Math::is_equal_approx(p_first.x, p_second.x, p_tolerance) &&
+ Math::is_equal_approx(p_first.y, p_second.y, p_tolerance);
+}
+
+bool SceneSynchronizer::compare(const Vector3 &p_first, const Vector3 &p_second, real_t p_tolerance) {
+ return Math::is_equal_approx(p_first.x, p_second.x, p_tolerance) &&
+ Math::is_equal_approx(p_first.y, p_second.y, p_tolerance) &&
+ Math::is_equal_approx(p_first.z, p_second.z, p_tolerance);
+}
+
+bool SceneSynchronizer::compare(const Variant &p_first, const Variant &p_second, real_t p_tolerance) {
+ if (p_first.get_type() != p_second.get_type()) {
+ return false;
+ }
+
+ // Custom evaluation methods
+ switch (p_first.get_type()) {
+ case Variant::FLOAT: {
+ return Math::is_equal_approx(p_first, p_second, p_tolerance);
+ }
+ case Variant::VECTOR2: {
+ return compare(Vector2(p_first), Vector2(p_second), p_tolerance);
+ }
+ case Variant::RECT2: {
+ const Rect2 a(p_first);
+ const Rect2 b(p_second);
+ if (compare(a.position, b.position, p_tolerance)) {
+ if (compare(a.size, b.size, p_tolerance)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ case Variant::TRANSFORM2D: {
+ const Transform2D a(p_first);
+ const Transform2D b(p_second);
+ if (compare(a.columns[0], b.columns[0], p_tolerance)) {
+ if (compare(a.columns[1], b.columns[1], p_tolerance)) {
+ if (compare(a.columns[2], b.columns[2], p_tolerance)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case Variant::VECTOR3: {
+ return compare(Vector3(p_first), Vector3(p_second), p_tolerance);
+ }
+ case Variant::QUATERNION: {
+ const Quaternion a = p_first;
+ const Quaternion b = p_second;
+ const Quaternion r(a - b); // Element wise subtraction.
+ return (r.x * r.x + r.y * r.y + r.z * r.z + r.w * r.w) <= (p_tolerance * p_tolerance);
+ }
+ case Variant::PLANE: {
+ const Plane a(p_first);
+ const Plane b(p_second);
+ if (Math::is_equal_approx(a.d, b.d, p_tolerance)) {
+ if (compare(a.normal, b.normal, p_tolerance)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ case Variant::AABB: {
+ const AABB a(p_first);
+ const AABB b(p_second);
+ if (compare(a.position, b.position, p_tolerance)) {
+ if (compare(a.size, b.size, p_tolerance)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ case Variant::BASIS: {
+ const Basis a = p_first;
+ const Basis b = p_second;
+ if (compare(a.rows[0], b.rows[0], p_tolerance)) {
+ if (compare(a.rows[1], b.rows[1], p_tolerance)) {
+ if (compare(a.rows[2], b.rows[2], p_tolerance)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ case Variant::TRANSFORM3D: {
+ const Transform3D a = p_first;
+ const Transform3D b = p_second;
+ if (compare(a.origin, b.origin, p_tolerance)) {
+ if (compare(a.basis.rows[0], b.basis.rows[0], p_tolerance)) {
+ if (compare(a.basis.rows[1], b.basis.rows[1], p_tolerance)) {
+ if (compare(a.basis.rows[2], b.basis.rows[2], p_tolerance)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ case Variant::ARRAY: {
+ const Array a = p_first;
+ const Array b = p_second;
+ if (a.size() != b.size()) {
+ return false;
+ }
+ for (int i = 0; i < a.size(); i += 1) {
+ if (compare(a[i], b[i], p_tolerance) == false) {
+ return false;
+ }
+ }
+ return true;
+ }
+ case Variant::DICTIONARY: {
+ const Dictionary a = p_first;
+ const Dictionary b = p_second;
+
+ if (a.size() != b.size()) {
+ return false;
+ }
+
+ List l;
+ a.get_key_list(&l);
+
+ for (const List::Element *key = l.front(); key; key = key->next()) {
+ if (b.has(key->get()) == false) {
+ return false;
+ }
+
+ if (compare(
+ a.get(key->get(), Variant()),
+ b.get(key->get(), Variant()),
+ p_tolerance) == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ default:
+ return p_first == p_second;
+ }
+}
+
+bool SceneSynchronizer::is_server() const {
+ return synchronizer_type == SYNCHRONIZER_TYPE_SERVER;
+}
+
+bool SceneSynchronizer::is_client() const {
+ return synchronizer_type == SYNCHRONIZER_TYPE_CLIENT;
+}
+
+bool SceneSynchronizer::is_no_network() const {
+ return synchronizer_type == SYNCHRONIZER_TYPE_NONETWORK;
+}
+
+bool SceneSynchronizer::is_networked() const {
+ return is_client() || is_server();
+}
+
+#ifdef DEBUG_ENABLED
+void SceneSynchronizer::validate_nodes() {
+ LocalVector null_objects;
+ null_objects.reserve(node_data.size());
+
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (ObjectDB::get_instance(node_data[i]->instance_id) == nullptr) {
+ // Mark for removal.
+ null_objects.push_back(node_data[i]);
+ }
+ }
+
+ // Removes the invalidated `NodeData`.
+ if (null_objects.size()) {
+ NET_DEBUG_ERR("At least one node has been removed from the tree without the SceneSynchronizer noticing. This shouldn't happen.");
+ for (uint32_t i = 0; i < null_objects.size(); i += 1) {
+ drop_node_data(null_objects[i]);
+ }
+ }
+}
+#endif
+
+void SceneSynchronizer::purge_node_dependencies() {
+ if (is_client() == false) {
+ return;
+ }
+
+ // Clear the controller dependencies.
+ ClientSynchronizer *client_sync = static_cast(synchronizer);
+
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ for (
+ int d = 0;
+ d < int(node_data_controllers[i]->dependency_nodes_end.size());
+ d += 1) {
+ if (node_data_controllers[i]->dependency_nodes_end[d] < client_sync->last_checked_input) {
+ // This controller dependency can be cleared because the server
+ // snapshot check has
+ node_data_controllers[i]->dependency_nodes.remove_at_unordered(d);
+ node_data_controllers[i]->dependency_nodes_end.remove_at_unordered(d);
+ d -= 1;
+ }
+ }
+ }
+}
+
+void SceneSynchronizer::expand_organized_node_data_vector(uint32_t p_size) {
+ const uint32_t from = organized_node_data.size();
+ organized_node_data.resize(from + p_size);
+ memset(organized_node_data.ptr() + from, 0, sizeof(void *) * p_size);
+}
+
+NetUtility::NodeData *SceneSynchronizer::find_node_data(const Node *p_node) {
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (node_data[i] == nullptr) {
+ continue;
+ }
+ if (node_data[i]->instance_id == p_node->get_instance_id()) {
+ return node_data[i];
+ }
+ }
+ return nullptr;
+}
+
+const NetUtility::NodeData *SceneSynchronizer::find_node_data(const Node *p_node) const {
+ for (uint32_t i = 0; i < node_data.size(); i += 1) {
+ if (node_data[i] == nullptr) {
+ continue;
+ }
+ if (node_data[i]->instance_id == p_node->get_instance_id()) {
+ return node_data[i];
+ }
+ }
+ return nullptr;
+}
+
+NetUtility::NodeData *SceneSynchronizer::get_node_data(NetNodeId p_id) {
+ ERR_FAIL_UNSIGNED_INDEX_V(p_id, organized_node_data.size(), nullptr);
+ return organized_node_data[p_id];
+}
+
+const NetUtility::NodeData *SceneSynchronizer::get_node_data(NetNodeId p_id) const {
+ ERR_FAIL_UNSIGNED_INDEX_V(p_id, organized_node_data.size(), nullptr);
+ return organized_node_data[p_id];
+}
+
+NetNodeId SceneSynchronizer::get_biggest_node_id() const {
+ return organized_node_data.size() == 0 ? UINT32_MAX : organized_node_data.size() - 1;
+}
+
+void SceneSynchronizer::reset_controllers() {
+ for (uint32_t i = 0; i < node_data_controllers.size(); i += 1) {
+ reset_controller(node_data_controllers[i]);
+ }
+}
+
+void SceneSynchronizer::reset_controller(NetUtility::NodeData *p_controller_nd) {
+#ifdef DEBUG_ENABLED
+ // This can't happen because the callers make sure the `NodeData` is a
+ // controller.
+ CRASH_COND(p_controller_nd->is_controller == false);
+#endif
+
+ NetworkedController *controller = static_cast(p_controller_nd->node);
+
+ // Reset the controller type.
+ if (controller->controller != nullptr) {
+ memdelete(controller->controller);
+ controller->controller = nullptr;
+ controller->controller_type = NetworkedController::CONTROLLER_TYPE_NULL;
+ controller->set_physics_process_internal(false);
+ }
+
+ if (get_tree() == nullptr) {
+ if (synchronizer) {
+ synchronizer->on_controller_reset(p_controller_nd);
+ }
+
+ // Nothing to do.
+ return;
+ }
+
+ if (get_tree()->get_multiplayer()->get_multiplayer_peer().is_null()) {
+ controller->controller_type = NetworkedController::CONTROLLER_TYPE_NONETWORK;
+ controller->controller = memnew(NoNetController(controller));
+ } else if (get_tree()->get_multiplayer()->is_server()) {
+ controller->controller_type = NetworkedController::CONTROLLER_TYPE_SERVER;
+ controller->controller = memnew(ServerController(controller, controller->get_network_traced_frames()));
+ } else if (controller->is_multiplayer_authority()) {
+ controller->controller_type = NetworkedController::CONTROLLER_TYPE_PLAYER;
+ controller->controller = memnew(PlayerController(controller));
+ } else {
+ controller->controller_type = NetworkedController::CONTROLLER_TYPE_DOLL;
+ controller->controller = memnew(DollController(controller));
+ controller->set_physics_process_internal(true);
+ }
+
+ dirty_peers();
+ controller->controller->ready();
+
+ if (synchronizer) {
+ synchronizer->on_controller_reset(p_controller_nd);
+ }
+}
+
+void SceneSynchronizer::process() {
+#ifdef DEBUG_ENABLED
+ validate_nodes();
+ // Never triggered because this function is called by `PHYSICS_PROCESS`,
+ // notification that is emitted only when the node is in the tree.
+ // When the node is in the tree, there is no way that the `synchronizer` is
+ // null.
+ CRASH_COND(synchronizer == nullptr);
+#endif
+
+ synchronizer->process();
+ purge_node_dependencies();
+}
+
+void SceneSynchronizer::pull_node_changes(NetUtility::NodeData *p_node_data) {
+ Node *node = p_node_data->node;
+
+ for (NetVarId var_id = 0; var_id < p_node_data->vars.size(); var_id += 1) {
+ if (p_node_data->vars[var_id].enabled == false) {
+ continue;
+ }
+
+ const Variant old_val = p_node_data->vars[var_id].var.value;
+ const Variant new_val = node->get(p_node_data->vars[var_id].var.name);
+
+ if (!compare(old_val, new_val)) {
+ p_node_data->vars[var_id].var.value = new_val.duplicate(true);
+ change_event_add(
+ p_node_data,
+ var_id,
+ old_val);
+ }
+ }
+}
+
+Synchronizer::Synchronizer(SceneSynchronizer *p_node) :
+ scene_synchronizer(p_node) {
+}
+
+NoNetSynchronizer::NoNetSynchronizer(SceneSynchronizer *p_node) :
+ Synchronizer(p_node) {}
+
+void NoNetSynchronizer::clear() {
+ enabled = true;
+}
+
+void NoNetSynchronizer::process() {
+ if (unlikely(enabled == false)) {
+ return;
+ }
+
+ const real_t delta = scene_synchronizer->get_physics_process_delta_time();
+
+ // Process the scene
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ nd->process(delta);
+ }
+
+ // Process the controllers_node_data
+ for (uint32_t i = 0; i < scene_synchronizer->node_data_controllers.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data_controllers[i];
+ static_cast(nd->node)->get_nonet_controller()->process(delta);
+ }
+
+ // Pull the changes.
+ scene_synchronizer->change_events_begin(NetEventFlag::CHANGE);
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ scene_synchronizer->pull_node_changes(nd);
+ }
+ scene_synchronizer->change_events_flush();
+}
+
+void NoNetSynchronizer::set_enabled(bool p_enabled) {
+ if (enabled == p_enabled) {
+ // Nothing to do.
+ return;
+ }
+
+ enabled = p_enabled;
+
+ if (enabled) {
+ scene_synchronizer->emit_signal("sync_started");
+ } else {
+ scene_synchronizer->emit_signal("sync_paused");
+ }
+}
+
+bool NoNetSynchronizer::is_enabled() const {
+ return enabled;
+}
+
+ServerSynchronizer::ServerSynchronizer(SceneSynchronizer *p_node) :
+ Synchronizer(p_node) {}
+
+void ServerSynchronizer::clear() {
+ state_notifier_timer = 0.0;
+ // Release the internal memory.
+ changes.reset();
+}
+
+void ServerSynchronizer::process() {
+ scene_synchronizer->update_peers();
+
+ const real_t delta = scene_synchronizer->get_physics_process_delta_time();
+
+ // Process the scene
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ nd->process(delta);
+ }
+
+ // Process the controllers_node_data
+ for (uint32_t i = 0; i < scene_synchronizer->node_data_controllers.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data_controllers[i];
+ static_cast(nd->node)->get_server_controller()->process(delta);
+ }
+
+ // Pull the changes.
+ scene_synchronizer->change_events_begin(NetEventFlag::CHANGE);
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ scene_synchronizer->pull_node_changes(nd);
+ }
+ scene_synchronizer->change_events_flush();
+
+ process_snapshot_notificator(delta);
+}
+
+void ServerSynchronizer::on_node_added(NetUtility::NodeData *p_node_data) {
+#ifdef DEBUG_ENABLED
+ // Can't happen on server
+ CRASH_COND(scene_synchronizer->is_recovered());
+ // On server the ID is always known.
+ CRASH_COND(p_node_data->id == UINT32_MAX);
+#endif
+
+ if (changes.size() <= p_node_data->id) {
+ changes.resize(p_node_data->id + 1);
+ }
+
+ changes[p_node_data->id].not_known_before = true;
+}
+
+void ServerSynchronizer::on_variable_added(NetUtility::NodeData *p_node_data, const StringName &p_var_name) {
+#ifdef DEBUG_ENABLED
+ // Can't happen on server
+ CRASH_COND(scene_synchronizer->is_recovered());
+ // On server the ID is always known.
+ CRASH_COND(p_node_data->id == UINT32_MAX);
+#endif
+
+ if (changes.size() <= p_node_data->id) {
+ changes.resize(p_node_data->id + 1);
+ }
+
+ changes[p_node_data->id].vars.insert(p_var_name);
+ changes[p_node_data->id].uknown_vars.insert(p_var_name);
+}
+
+void ServerSynchronizer::on_variable_changed(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old_value, int p_flag) {
+#ifdef DEBUG_ENABLED
+ // Can't happen on server
+ CRASH_COND(scene_synchronizer->is_recovered());
+ // On server the ID is always known.
+ CRASH_COND(p_node_data->id == UINT32_MAX);
+#endif
+
+ if (changes.size() <= p_node_data->id) {
+ changes.resize(p_node_data->id + 1);
+ }
+
+ changes[p_node_data->id].vars.insert(p_node_data->vars[p_var_id].var.name);
+}
+
+void ServerSynchronizer::process_snapshot_notificator(real_t p_delta) {
+ if (scene_synchronizer->peer_data.is_empty()) {
+ // No one is listening.
+ return;
+ }
+
+ // Notify the state if needed
+ state_notifier_timer += p_delta;
+ const bool notify_state = state_notifier_timer >= scene_synchronizer->get_server_notify_state_interval();
+
+ if (notify_state) {
+ state_notifier_timer = 0.0;
+ }
+
+ Vector full_global_nodes_snapshot;
+ Vector delta_global_nodes_snapshot;
+ for (
+ OAHashMap::Iterator peer_it = scene_synchronizer->peer_data.iter();
+ peer_it.valid;
+ peer_it = scene_synchronizer->peer_data.next_iter(peer_it)) {
+ if (unlikely(peer_it.value->controller_id == UINT32_MAX)) {
+ // This peer still does not have a `NetworkedController`.
+ continue;
+ }
+ if (unlikely(peer_it.value->enabled == false)) {
+ // This peer is disabled.
+ continue;
+ }
+ if (peer_it.value->force_notify_snapshot == false && notify_state == false) {
+ // Nothing to do.
+ continue;
+ }
+
+ peer_it.value->force_notify_snapshot = false;
+
+ NetUtility::NodeData *nd = scene_synchronizer->get_node_data(peer_it.value->controller_id);
+ // TODO well that's not really true. I may have peers that doesn't have controllers_node_data in a
+ // certain moment. Please improve this mechanism trying to just use the
+ // node->get_network_master() to get the peer.
+ ERR_CONTINUE_MSG(nd == nullptr, "This should never happen. Likely there is a bug, NedNodeId: " + itos(peer_it.value->controller_id));
+ ERR_CONTINUE_MSG(nd->is_controller == false, "[BUG] A controller il expected, The node " + nd->node->get_path() + " is submitted instead.");
+
+ NetworkedController *controller = static_cast(nd->node);
+
+ Vector snap;
+ if (peer_it.value->need_full_snapshot) {
+ peer_it.value->need_full_snapshot = false;
+ if (full_global_nodes_snapshot.size() == 0) {
+ full_global_nodes_snapshot = global_nodes_generate_snapshot(true);
+ }
+ snap = full_global_nodes_snapshot;
+ controller_generate_snapshot(nd, true, snap);
+ } else {
+ if (delta_global_nodes_snapshot.size() == 0) {
+ delta_global_nodes_snapshot = global_nodes_generate_snapshot(false);
+ }
+ snap = delta_global_nodes_snapshot;
+ controller_generate_snapshot(nd, false, snap);
+ }
+
+ controller->get_server_controller()->notify_send_state();
+ scene_synchronizer->rpc_id(*peer_it.key, SNAME("_rpc_send_state"), snap);
+ }
+
+ if (notify_state) {
+ // The state got notified, mark this as checkpoint so the next state
+ // will contains only the changed things.
+ changes.clear();
+ }
+}
+
+Vector ServerSynchronizer::global_nodes_generate_snapshot(bool p_force_full_snapshot) const {
+ Vector snapshot_data;
+
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ const NetUtility::NodeData *node_data = scene_synchronizer->node_data[i];
+ if (node_data == nullptr) {
+ continue;
+ }
+ if (node_data->is_controller || node_data->controlled_by != nullptr) {
+ // Skip the controllers.
+ continue;
+ }
+ generate_snapshot_node_data(node_data, p_force_full_snapshot, snapshot_data);
+ }
+
+ return snapshot_data;
+}
+
+void ServerSynchronizer::controller_generate_snapshot(
+ const NetUtility::NodeData *p_node_data,
+ bool p_force_full_snapshot,
+ Vector &r_snapshot_result) const {
+ CRASH_COND(p_node_data->is_controller == false);
+
+ generate_snapshot_node_data(
+ p_node_data,
+ p_force_full_snapshot,
+ r_snapshot_result);
+
+ for (uint32_t i = 0; i < p_node_data->controlled_nodes.size(); i += 1) {
+ generate_snapshot_node_data(
+ p_node_data->controlled_nodes[i],
+ p_force_full_snapshot,
+ r_snapshot_result);
+ }
+}
+
+void ServerSynchronizer::generate_snapshot_node_data(
+ const NetUtility::NodeData *p_node_data,
+ bool p_force_full_snapshot,
+ Vector &r_snapshot_data) const {
+ // The packet data is an array that contains the informations to update the
+ // client snapshot.
+ //
+ // It's composed as follows:
+ // [NODE, VARIABLE, Value, VARIABLE, Value, VARIABLE, value, NIL,
+ // NODE, INPUT ID, VARIABLE, Value, VARIABLE, Value, NIL,
+ // NODE, VARIABLE, Value, VARIABLE, Value, NIL]
+ //
+ // Each node ends with a NIL, and the NODE and the VARIABLE are special:
+ // - NODE, can be an array of two variables [Net Node ID, NodePath] or directly
+ // a Node ID. Obviously the array is sent only the first time.
+ // - INPUT ID, this is optional and is used only when the node is a controller.
+ // - VARIABLE, can be an array with the ID and the variable name, or just
+ // the ID; similarly as is for the NODE the array is send only
+ // the first time.
+
+ if (p_node_data->node == nullptr || p_node_data->node->is_inside_tree() == false) {
+ return;
+ }
+
+ const Change *change = p_node_data->id >= changes.size() ? nullptr : changes.ptr() + p_node_data->id;
+
+ // Insert NODE DATA.
+ Variant snap_node_data;
+ if (p_force_full_snapshot || (change != nullptr && change->not_known_before)) {
+ Vector _snap_node_data;
+ _snap_node_data.resize(2);
+ _snap_node_data.write[0] = p_node_data->id;
+ _snap_node_data.write[1] = p_node_data->node->get_path();
+ snap_node_data = _snap_node_data;
+ } else {
+ // This node is already known on clients, just set the node ID.
+ snap_node_data = p_node_data->id;
+ }
+
+ const bool node_has_changes = p_force_full_snapshot || (change != nullptr && change->vars.is_empty() == false);
+
+ if (p_node_data->is_controller) {
+ NetworkedController *controller = static_cast(p_node_data->node);
+
+ // TODO make sure to skip un-active controllers_node_data.
+ // This may no more needed, since the interpolator got integrated and
+ // the only time the controller is sync is when it's needed.
+ if (likely(controller->get_current_input_id() != UINT32_MAX)) {
+ // This is a controller, always sync it.
+ r_snapshot_data.push_back(snap_node_data);
+ r_snapshot_data.push_back(controller->get_current_input_id());
+ } else {
+ // The first ID id is not yet arrived, so just skip this node.
+ return;
+ }
+ } else {
+ if (node_has_changes) {
+ r_snapshot_data.push_back(snap_node_data);
+ } else {
+ // It has no changes, skip this node.
+ return;
+ }
+ }
+
+ if (node_has_changes) {
+ // Insert the node variables.
+ for (uint32_t i = 0; i < p_node_data->vars.size(); i += 1) {
+ const NetUtility::VarData &var = p_node_data->vars[i];
+ if (var.enabled == false) {
+ continue;
+ }
+
+ if (p_force_full_snapshot == false && change->vars.has(var.var.name) == false) {
+ // This is a delta snapshot and this variable is the same as
+ // before. Skip it.
+ continue;
+ }
+
+ Variant var_info;
+ if (p_force_full_snapshot || change->uknown_vars.has(var.var.name)) {
+ Vector _var_info;
+ _var_info.resize(2);
+ _var_info.write[0] = var.id;
+ _var_info.write[1] = var.var.name;
+ var_info = _var_info;
+ } else {
+ var_info = var.id;
+ }
+
+ r_snapshot_data.push_back(var_info);
+ r_snapshot_data.push_back(var.var.value);
+ }
+ }
+
+ // Insert NIL.
+ r_snapshot_data.push_back(Variant());
+}
+
+ClientSynchronizer::ClientSynchronizer(SceneSynchronizer *p_node) :
+ Synchronizer(p_node) {
+ clear();
+}
+
+void ClientSynchronizer::clear() {
+ player_controller_node_data = nullptr;
+ node_paths.clear();
+ last_received_snapshot.input_id = UINT32_MAX;
+ last_received_snapshot.node_vars.clear();
+ client_snapshots.clear();
+ server_snapshots.clear();
+ last_checked_input = 0;
+ enabled = true;
+ need_full_snapshot_notified = false;
+}
+
+void ClientSynchronizer::process() {
+ if (unlikely(player_controller_node_data == nullptr || enabled == false)) {
+ // No player controller or disabled so nothing to do.
+ return;
+ }
+
+ const real_t delta = scene_synchronizer->get_physics_process_delta_time();
+ const real_t physics_ticks_per_second = Engine::get_singleton()->get_physics_ticks_per_second();
+
+#ifdef DEBUG_ENABLED
+ if (unlikely(Engine::get_singleton()->get_frames_per_second() < physics_ticks_per_second)) {
+ WARN_PRINT("Current FPS is " + itos(Engine::get_singleton()->get_frames_per_second()) + ", but the minimum required FPS is " + itos(physics_ticks_per_second) + ", the client is unable to generate enough inputs for the server.");
+ }
+#endif
+
+ NetworkedController *controller = static_cast(player_controller_node_data->node);
+ PlayerController *player_controller = controller->get_player_controller();
+
+ // Reset this here, so even when `sub_ticks` is zero (and it's not
+ // updated due to process is not called), we can still have the corect
+ // data.
+ controller->player_set_has_new_input(false);
+
+ // Due to some lag we may want to speed up the input_packet
+ // generation, for this reason here I'm performing a sub tick.
+ //
+ // keep in mind that we are just pretending that the time
+ // is advancing faster, for this reason we are still using
+ // `delta` to step the controllers_node_data.
+ //
+ // The dolls may want to speed up too, so to consume the inputs faster
+ // and get back in time with the server.
+ int sub_ticks = player_controller->calculates_sub_ticks(delta, physics_ticks_per_second);
+
+ while (sub_ticks > 0) {
+ // Process the scene.
+ for (uint32_t i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ nd->process(delta);
+ }
+
+ // Process the player controllers_node_data.
+ player_controller->process(delta);
+
+ // Pull the changes.
+ scene_synchronizer->change_events_begin(NetEventFlag::CHANGE);
+ for (NetNodeId i = 0; i < scene_synchronizer->node_data.size(); i += 1) {
+ NetUtility::NodeData *nd = scene_synchronizer->node_data[i];
+ scene_synchronizer->pull_node_changes(nd);
+ }
+ scene_synchronizer->change_events_flush();
+
+ if (controller->player_has_new_input()) {
+ store_snapshot();
+ }
+
+ sub_ticks -= 1;
+ }
+
+ process_controllers_recovery(delta);
+
+ // Now trigger the END_SYNC event.
+ scene_synchronizer->change_events_begin(NetEventFlag::END_SYNC);
+ for (const Set::Element *e = sync_end_events.front();
+ e != nullptr;
+ e = e->next()) {
+ // Check if the values between the variables before the sync and the
+ // current one are different.
+ if (scene_synchronizer->compare(
+ e->get().node_data->vars[e->get().var_id].var.value,
+ e->get().old_value) == false) {
+ // Are different so we need to emit the `END_SYNC`.
+ scene_synchronizer->change_event_add(
+ e->get().node_data,
+ e->get().var_id,
+ e->get().old_value);
+ }
+ }
+ sync_end_events.clear();
+
+ scene_synchronizer->change_events_flush();
+}
+
+void ClientSynchronizer::receive_snapshot(Variant p_snapshot) {
+ // The received snapshot is parsed and stored into the `last_received_snapshot`
+ // that contains always the last received snapshot.
+ // Later, the snapshot is stored into the server queue.
+ // In this way, we are free to pop snapshot from the queue without wondering
+ // about losing the data. Indeed the received snapshot is just and
+ // incremental update so the last received data is always needed to fully
+ // reconstruct it.
+
+ // Parse server snapshot.
+ const bool success = parse_snapshot(p_snapshot);
+
+ if (success == false) {
+ return;
+ }
+
+ // Finalize data.
+
+ store_controllers_snapshot(
+ last_received_snapshot,
+ server_snapshots);
+}
+
+void ClientSynchronizer::on_node_added(NetUtility::NodeData *p_node_data) {
+}
+
+void ClientSynchronizer::on_node_removed(NetUtility::NodeData *p_node_data) {
+ if (player_controller_node_data == p_node_data) {
+ player_controller_node_data = nullptr;
+ client_snapshots.clear();
+ }
+}
+
+void ClientSynchronizer::on_variable_changed(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old_value, int p_flag) {
+ if (p_flag & NetEventFlag::SYNC) {
+ sync_end_events.insert(
+ EndSyncEvent{
+ p_node_data,
+ p_var_id,
+ p_old_value });
+ }
+}
+
+void ClientSynchronizer::on_controller_reset(NetUtility::NodeData *p_node_data) {
+#ifdef DEBUG_ENABLED
+ CRASH_COND(p_node_data->is_controller == false);
+#endif
+
+ if (player_controller_node_data == p_node_data) {
+ // Reset the node_data.
+ player_controller_node_data = nullptr;
+ client_snapshots.clear();
+ }
+
+ if (static_cast(p_node_data->node)->is_player_controller()) {
+ if (player_controller_node_data != nullptr) {
+ NET_DEBUG_ERR("Only one player controller is supported, at the moment. Make sure this is the case.");
+ } else {
+ // Set this player controller as active.
+ player_controller_node_data = p_node_data;
+ client_snapshots.clear();
+ }
+ }
+}
+
+void ClientSynchronizer::store_snapshot() {
+ NetworkedController *controller = static_cast(player_controller_node_data->node);
+
+#ifdef DEBUG_ENABLED
+ if (unlikely(client_snapshots.size() > 0 && controller->get_current_input_id() <= client_snapshots.back().input_id)) {
+ CRASH_NOW_MSG("[FATAL] During snapshot creation, for controller " + controller->get_path() + ", was found an ID for an older snapshots. New input ID: " + itos(controller->get_current_input_id()) + " Last saved snapshot input ID: " + itos(client_snapshots.back().input_id) + ".");
+ }
+#endif
+
+ client_snapshots.push_back(NetUtility::Snapshot());
+
+ NetUtility::Snapshot &snap = client_snapshots.back();
+ snap.input_id = controller->get_current_input_id();
+
+ snap.node_vars.resize(scene_synchronizer->organized_node_data.size());
+
+ // Store the nodes state and skip anything is related to the other
+ // controllers.
+ for (uint32_t i = 0; i < scene_synchronizer->organized_node_data.size(); i += 1) {
+ const NetUtility::NodeData *node_data = scene_synchronizer->organized_node_data[i];
+
+ if (node_data == nullptr) {
+ // Nothing to do.
+ continue;
+ }
+
+ if ((node_data->is_controller || node_data->controlled_by != nullptr) &&
+ (node_data != player_controller_node_data && node_data->controlled_by != player_controller_node_data)) {
+ // Ignore this controller.
+ continue;
+ }
+
+ if (node_data->id >= uint32_t(snap.node_vars.size())) {
+ // Make sure this ID is valid.
+ ERR_FAIL_COND_MSG(node_data->id != UINT32_MAX, "[BUG] It's not expected that the client has a node with the NetNodeId (" + itos(node_data->id) + ") bigger than the registered node count: " + itos(snap.node_vars.size()));
+ // Skip this node
+ continue;
+ }
+
+ Vector *snap_node_vars = snap.node_vars.ptrw() + node_data->id;
+ snap_node_vars->resize(node_data->vars.size());
+ NetUtility::Var *vars = snap_node_vars->ptrw();
+ for (uint32_t v = 0; v < node_data->vars.size(); v += 1) {
+ if (node_data->vars[v].enabled) {
+ vars[v] = node_data->vars[v].var;
+ } else {
+ vars[v].name = StringName();
+ }
+ }
+ }
+}
+
+void ClientSynchronizer::store_controllers_snapshot(
+ const NetUtility::Snapshot &p_snapshot,
+ std::deque &r_snapshot_storage) {
+ // Put the parsed snapshot into the queue.
+
+ if (p_snapshot.input_id == UINT32_MAX) {
+ // The snapshot doesn't have any info for this controller; Skip it.
+ return;
+ }
+
+ if (r_snapshot_storage.empty() == false) {
+ // Make sure the snapshots are stored in order.
+ const uint32_t last_stored_input_id = r_snapshot_storage.back().input_id;
+ if (p_snapshot.input_id == last_stored_input_id) {
+ // Update the snapshot.
+ r_snapshot_storage.back() = p_snapshot;
+ return;
+ } else {
+ ERR_FAIL_COND_MSG(p_snapshot.input_id < last_stored_input_id, "This snapshot (with ID: " + itos(p_snapshot.input_id) + ") is not expected because the last stored id is: " + itos(last_stored_input_id));
+ }
+ }
+
+ r_snapshot_storage.push_back(p_snapshot);
+}
+
+// TODO make this function much simpler.
+void ClientSynchronizer::process_controllers_recovery(real_t p_delta) {
+ // The client is responsible to recover only its local controller, while all
+ // the other controllers_node_data (dolls) have their state interpolated. There is
+ // no need to check the correctness of the doll state nor the needs to
+ // rewind those.
+ //
+ // The scene, (global nodes), are always in sync with the reference frame
+ // of the client.
+
+ NetworkedController *controller = static_cast(player_controller_node_data->node);
+ PlayerController *player_controller = controller->get_player_controller();
+
+ // --- Phase one: find the snapshot to check. ---
+ if (server_snapshots.empty()) {
+ // No snapshots to recover for this controller. Nothing to do.
+ return;
+ }
+
+#ifdef DEBUG_ENABLED
+ if (client_snapshots.empty() == false) {
+ // The SceneSynchronizer and the PlayerController are always in sync.
+ CRASH_COND_MSG(client_snapshots.back().input_id != player_controller->last_known_input(), "This should not be possible: snapshot input: " + itos(client_snapshots.back().input_id) + " last_know_input: " + itos(player_controller->last_known_input()));
+ }
+#endif
+
+ // Find the best recoverable input_id.
+ uint32_t checkable_input_id = UINT32_MAX;
+ // Find the best snapshot to recover from the one already
+ // processed.
+ if (client_snapshots.empty() == false) {
+ for (
+ auto s_snap = server_snapshots.rbegin();
+ checkable_input_id == UINT32_MAX && s_snap != server_snapshots.rend();
+ ++s_snap) {
+ for (auto c_snap = client_snapshots.begin(); c_snap != client_snapshots.end(); ++c_snap) {
+ if (c_snap->input_id == s_snap->input_id) {
+ // Server snapshot also found on client, can be checked.
+ checkable_input_id = c_snap->input_id;
+ break;
+ }
+ }
+ }
+ } else {
+ // No client input, this happens when the stream is paused.
+ process_paused_controller_recovery(p_delta);
+ return;
+ }
+
+ if (checkable_input_id == UINT32_MAX) {
+ // No snapshot found, nothing to do.
+ return;
+ }
+
+#ifdef DEBUG_ENABLED
+ // Unreachable cause the above check
+ CRASH_COND(server_snapshots.empty());
+ CRASH_COND(client_snapshots.empty());
+#endif
+
+ // Drop all the old server snapshots until the one that we need.
+ while (server_snapshots.front().input_id < checkable_input_id) {
+ server_snapshots.pop_front();
+ }
+
+ // Drop all the old client snapshots until the one that we need.
+ while (client_snapshots.front().input_id < checkable_input_id) {
+ client_snapshots.pop_front();
+ }
+
+#ifdef DEBUG_ENABLED
+ // These are unreachable at this point.
+ CRASH_COND(server_snapshots.empty());
+ CRASH_COND(server_snapshots.front().input_id != checkable_input_id);
+
+ // This is unreachable, because we store all the client shapshots
+ // each time a new input is processed. Since the `checkable_input_id`
+ // is taken by reading the processed doll inputs, it's guaranteed
+ // that here the snapshot exists.
+ CRASH_COND(client_snapshots.empty());
+ CRASH_COND(client_snapshots.front().input_id != checkable_input_id);
+#endif
+
+ // --- Phase two: compare the server snapshot with the client snapshot. ---
+ bool need_recover = false;
+ bool recover_controller = false;
+ LocalVector nodes_to_recover;
+ LocalVector postponed_recover;
+
+ nodes_to_recover.reserve(server_snapshots.front().node_vars.size());
+ for (uint32_t net_node_id = 0; net_node_id < uint32_t(server_snapshots.front().node_vars.size()); net_node_id += 1) {
+ NetUtility::NodeData *rew_node_data = scene_synchronizer->get_node_data(net_node_id);
+ if (rew_node_data == nullptr || rew_node_data->sync_enabled == false) {
+ continue;
+ }
+
+ bool recover_this_node = false;
+ if (net_node_id >= uint32_t(client_snapshots.front().node_vars.size())) {
+ NET_DEBUG_PRINT("Rewind is needed because the client snapshot doesn't contain this node: " + rew_node_data->node->get_path());
+ recover_this_node = true;
+ } else {
+ NetUtility::PostponedRecover rec;
+
+ const bool different = compare_vars(
+ rew_node_data,
+ server_snapshots.front().node_vars[net_node_id],
+ client_snapshots.front().node_vars[net_node_id],
+ rec.vars);
+
+ if (different) {
+ NET_DEBUG_PRINT("Rewind is needed because the node on client is different: " + rew_node_data->node->get_path());
+ recover_this_node = true;
+ } else if (rec.vars.size() > 0) {
+ rec.node_data = rew_node_data;
+ postponed_recover.push_back(rec);
+ }
+ }
+
+ if (recover_this_node) {
+ need_recover = true;
+ if (rew_node_data->controlled_by != nullptr ||
+ rew_node_data->is_controller ||
+ player_controller_node_data->dependency_nodes.find(rew_node_data) != -1) {
+ // Controller node.
+ recover_controller = true;
+ } else {
+ nodes_to_recover.push_back(rew_node_data);
+ }
+ }
+ }
+
+ // Popout the client snapshot.
+ client_snapshots.pop_front();
+
+ // --- Phase three: recover and reply. ---
+
+ if (need_recover) {
+ NET_DEBUG_PRINT("Recover input: " + itos(checkable_input_id) + " - Last input: " + itos(player_controller->get_stored_input_id(-1)));
+
+ if (recover_controller) {
+ // Put the controlled and the controllers_node_data into the nodes to
+ // rewind.
+ // Note, the controller stuffs are added here to ensure that if the
+ // controller need a recover, all its nodes are added; no matter
+ // at which point the difference is found.
+ nodes_to_recover.reserve(
+ nodes_to_recover.size() +
+ player_controller_node_data->controlled_nodes.size() +
+ player_controller_node_data->dependency_nodes.size() +
+ 1);
+
+ nodes_to_recover.push_back(player_controller_node_data);
+
+ for (
+ uint32_t y = 0;
+ y < player_controller_node_data->controlled_nodes.size();
+ y += 1) {
+ nodes_to_recover.push_back(player_controller_node_data->controlled_nodes[y]);
+ }
+
+ for (
+ uint32_t y = 0;
+ y < player_controller_node_data->dependency_nodes.size();
+ y += 1) {
+ nodes_to_recover.push_back(player_controller_node_data->dependency_nodes[y]);
+ }
+ }
+
+ // Apply the server snapshot so to go back in time till that moment,
+ // so to be able to correctly reply the movements.
+ scene_synchronizer->change_events_begin(NetEventFlag::SYNC_RECOVER | NetEventFlag::SYNC_RESET);
+ for (uint32_t i = 0; i < nodes_to_recover.size(); i += 1) {
+ if (nodes_to_recover[i]->id >= uint32_t(server_snapshots.front().node_vars.size())) {
+ NET_DEBUG_WARN("The node: " + nodes_to_recover[i]->node->get_path() + " was not found on the server snapshot, this is not supposed to happen a lot.");
+ continue;
+ }
+ if (nodes_to_recover[i]->sync_enabled == false) {
+ // Don't sync this node.
+ // This check is also here, because the `recover_controller`
+ // mechanism, may have insert a no sync node.
+ // The check is here because I feel it more clear, here.
+ continue;
+ }
+
+#ifdef DEBUG_ENABLED
+ // The parser make sure to properly initialize the snapshot variable
+ // array size. So the following condition is always `false`.
+ CRASH_COND(uint32_t(server_snapshots.front().node_vars[nodes_to_recover[i]->id].size()) != nodes_to_recover[i]->vars.size());
+#endif
+
+ Node *node = nodes_to_recover[i]->node;
+ const Vector s_vars = server_snapshots.front().node_vars[nodes_to_recover[i]->id];
+ const NetUtility::Var *s_vars_ptr = s_vars.ptr();
+
+ NET_DEBUG_PRINT("Full reset node: " + node->get_path());
+
+ for (int v = 0; v < s_vars.size(); v += 1) {
+ if (s_vars_ptr[v].name == StringName()) {
+ // This variable was not set, skip it.
+ continue;
+ }
+
+ const Variant current_val = nodes_to_recover[i]->vars[v].var.value;
+ nodes_to_recover[i]->vars[v].var.value = s_vars_ptr[v].value.duplicate(true);
+ node->set(s_vars_ptr[v].name, s_vars_ptr[v].value);
+
+ NET_DEBUG_PRINT(" |- Variable: " + s_vars_ptr[v].name + " New value: " + s_vars_ptr[v].value.stringify());
+ scene_synchronizer->change_event_add(
+ nodes_to_recover[i],
+ v,
+ current_val);
+ }
+ }
+ scene_synchronizer->change_events_flush();
+
+ // Rewind phase.
+
+ const int remaining_inputs = player_controller->notify_input_checked(checkable_input_id);
+#ifdef DEBUG_ENABLED
+ // Unreachable because the SceneSynchronizer and the PlayerController
+ // have the same stored data at this point.
+ CRASH_COND(client_snapshots.size() != size_t(remaining_inputs));
+#endif
+
+#ifdef DEBUG_ENABLED
+ // Used to double check all the instants have been processed.
+ bool has_next = false;
+#endif
+ for (int i = 0; i < remaining_inputs; i += 1) {
+ scene_synchronizer->change_events_begin(NetEventFlag::SYNC_RECOVER | NetEventFlag::SYNC_REWIND);
+
+ // Step 1 -- Process the scene nodes.
+ for (uint32_t r = 0; r < nodes_to_recover.size(); r += 1) {
+ if (nodes_to_recover[r]->sync_enabled == false) {
+ // This node is not sync.
+ continue;
+ }
+ nodes_to_recover[r]->process(p_delta);
+#ifdef DEBUG_ENABLED
+ if (nodes_to_recover[r]->functions.size()) {
+ NET_DEBUG_PRINT("Rewind, processed node: " + nodes_to_recover[r]->node->get_path());
+ }
+#endif
+ }
+
+ // Step 2 -- Process the controller.
+ if (recover_controller && player_controller_node_data->sync_enabled) {
+#ifdef DEBUG_ENABLED
+ has_next =
+#endif
+ controller->process_instant(i, p_delta);
+ NET_DEBUG_PRINT("Rewind, processed controller: " + controller->get_path());
+ }
+
+ // Step 3 -- Pull node changes and Update snapshots.
+ for (uint32_t r = 0; r < nodes_to_recover.size(); r += 1) {
+ if (nodes_to_recover[r]->sync_enabled == false) {
+ // This node is not sync.
+ continue;
+ }
+ // Pull changes
+ scene_synchronizer->pull_node_changes(nodes_to_recover[r]);
+
+ // Update client snapshot.
+ if (uint32_t(client_snapshots[i].node_vars.size()) <= nodes_to_recover[r]->id) {
+ client_snapshots[i].node_vars.resize(nodes_to_recover[r]->id + 1);
+ }
+
+ Vector *snap_node_vars = client_snapshots[i].node_vars.ptrw() + nodes_to_recover[r]->id;
+ snap_node_vars->resize(nodes_to_recover[r]->vars.size());
+
+ NetUtility::Var *vars = snap_node_vars->ptrw();
+ for (uint32_t v = 0; v < nodes_to_recover[r]->vars.size(); v += 1) {
+ vars[v] = nodes_to_recover[r]->vars[v].var;
+ }
+ }
+ scene_synchronizer->change_events_flush();
+ }
+
+#ifdef DEBUG_ENABLED
+ // Unreachable because the above loop consume all instants.
+ CRASH_COND(has_next);
+#endif
+
+ } else {
+ // Apply found differences without rewind.
+ scene_synchronizer->change_events_begin(NetEventFlag::SYNC_RECOVER);
+ for (uint32_t i = 0; i < postponed_recover.size(); i += 1) {
+ NetUtility::NodeData *rew_node_data = postponed_recover[i].node_data;
+ if (rew_node_data->sync_enabled == false) {
+ // This node sync is disabled.
+ continue;
+ }
+
+ Node *node = rew_node_data->node;
+ const NetUtility::Var *vars_ptr = postponed_recover[i].vars.ptr();
+
+ NET_DEBUG_PRINT("[Snapshot partial reset] Node: " + node->get_path());
+
+ // Set the value on the synchronizer too.
+ for (int v = 0; v < postponed_recover[i].vars.size(); v += 1) {
+ // We need to search it because the postponed recovered is not
+ // aligned.
+ // TODO This array is generated few lines above.
+ // Can we store the ID too, so to avoid this search????
+ const int rew_var_index = rew_node_data->vars.find(vars_ptr[v].name);
+ // Unreachable, because when the snapshot is received the
+ // algorithm make sure the `scene_synchronizer` is traking the
+ // variable.
+ CRASH_COND(rew_var_index <= -1);
+
+ const Variant old_val = rew_node_data->vars[rew_var_index].var.value;
+ rew_node_data->vars[rew_var_index].var.value = vars_ptr[v].value.duplicate(true);
+ node->set(vars_ptr[v].name, vars_ptr[v].value);
+
+ NET_DEBUG_PRINT(" |- Variable: " + vars_ptr[v].name + "; old value: " + old_val.stringify() + " new value: " + vars_ptr[v].value.stringify());
+ scene_synchronizer->change_event_add(
+ rew_node_data,
+ rew_var_index,
+ old_val);
+ }
+
+ // Update the last client snapshot.
+ if (client_snapshots.empty() == false) {
+ if (uint32_t(client_snapshots.back().node_vars.size()) <= rew_node_data->id) {
+ client_snapshots.back().node_vars.resize(rew_node_data->id + 1);
+ }
+
+ Vector *snap_node_vars = client_snapshots.back().node_vars.ptrw() + rew_node_data->id;
+ snap_node_vars->resize(rew_node_data->vars.size());
+
+ NetUtility::Var *vars = snap_node_vars->ptrw();
+
+ for (uint32_t v = 0; v < rew_node_data->vars.size(); v += 1) {
+ vars[v] = rew_node_data->vars[v].var;
+ }
+ }
+ }
+ scene_synchronizer->change_events_flush();
+
+ player_controller->notify_input_checked(checkable_input_id);
+ }
+
+ // Popout the server snapshot.
+ server_snapshots.pop_front();
+
+ last_checked_input = checkable_input_id;
+}
+
+void ClientSynchronizer::process_paused_controller_recovery(real_t p_delta) {
+#ifdef DEBUG_ENABLED
+ CRASH_COND(server_snapshots.empty());
+ CRASH_COND(client_snapshots.empty() == false);
+#endif
+
+ // Drop the snapshots till the newest.
+ while (server_snapshots.size() != 1) {
+ server_snapshots.pop_front();
+ }
+
+#ifdef DEBUG_ENABLED
+ CRASH_COND(server_snapshots.empty());
+#endif
+ scene_synchronizer->change_events_begin(NetEventFlag::SYNC_RECOVER);
+ for (uint32_t net_node_id = 0; net_node_id < uint32_t(server_snapshots.front().node_vars.size()); net_node_id += 1) {
+ NetUtility::NodeData *rew_node_data = scene_synchronizer->get_node_data(net_node_id);
+ if (rew_node_data == nullptr || rew_node_data->sync_enabled == false) {
+ continue;
+ }
+
+ Node *node = rew_node_data->node;
+
+ const NetUtility::Var *snap_vars_ptr = server_snapshots.front().node_vars[net_node_id].ptr();
+ for (int var_id = 0; var_id < server_snapshots.front().node_vars[net_node_id].size(); var_id += 1) {
+ // Note: the snapshot variable array is ordered per var_id.
+ const Variant old_val = rew_node_data->vars[var_id].var.value;
+ if (!scene_synchronizer->compare(
+ old_val,
+ snap_vars_ptr[var_id].value)) {
+ // Different
+ rew_node_data->vars[var_id].var.value = snap_vars_ptr[var_id].value;
+ node->set(snap_vars_ptr[var_id].name, snap_vars_ptr[var_id].value);
+ NET_DEBUG_PRINT("[Snapshot paused controller] Node: " + node->get_path());
+ NET_DEBUG_PRINT(" |- Variable: " + snap_vars_ptr[var_id].name + "; value: " + snap_vars_ptr[var_id].value.stringify());
+ scene_synchronizer->change_event_add(
+ rew_node_data,
+ var_id,
+ old_val);
+ }
+ }
+ }
+
+ server_snapshots.pop_front();
+
+ scene_synchronizer->change_events_flush();
+}
+
+bool ClientSynchronizer::parse_sync_data(
+ Variant p_sync_data,
+ void *p_user_pointer,
+ void (*p_node_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data),
+ void (*p_controller_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_input_id),
+ void (*p_variable_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_var_id, const Variant &p_value)) {
+ // The sync data is an array that contains the scene informations.
+ // It's used for several things, for this reason this function allows to
+ // customize the parsing.
+ //
+ // The data is composed as follows:
+ // [NODE, VARIABLE, Value, VARIABLE, Value, VARIABLE, value, NIL,
+ // NODE, INPUT ID, VARIABLE, Value, VARIABLE, Value, NIL,
+ // NODE, VARIABLE, Value, VARIABLE, Value, NIL]
+ //
+ // Each node ends with a NIL, and the NODE and the VARIABLE are special:
+ // - NODE, can be an array of two variables [Node ID, NodePath] or directly
+ // a Node ID. Obviously the array is sent only the first time.
+ // - INPUT ID, this is optional and is used only when the node is a controller.
+ // - VARIABLE, can be an array with the ID and the variable name, or just
+ // the ID; similarly as is for the NODE the array is send only
+ // the first time.
+
+ if (p_sync_data.get_type() == Variant::NIL) {
+ // Nothing to do.
+ return true;
+ }
+
+ ERR_FAIL_COND_V(!p_sync_data.is_array(), false);
+
+ const Vector raw_snapshot = p_sync_data;
+ const Variant *raw_snapshot_ptr = raw_snapshot.ptr();
+
+ NetUtility::NodeData *synchronizer_node_data = nullptr;
+ uint32_t var_id = UINT32_MAX;
+
+ for (int snap_data_index = 0; snap_data_index < raw_snapshot.size(); snap_data_index += 1) {
+ const Variant v = raw_snapshot_ptr[snap_data_index];
+ if (synchronizer_node_data == nullptr) {
+ // Node is null so we expect `v` has the node info.
+
+ bool skip_this_node = false;
+ Node *node = nullptr;
+ uint32_t net_node_id = UINT32_MAX;
+ NodePath node_path;
+
+ if (v.is_array()) {
+ // Node info are in verbose form, extract it.
+
+ const Vector node_data = v;
+ ERR_FAIL_COND_V(node_data.size() != 2, false);
+ ERR_FAIL_COND_V_MSG(node_data[0].get_type() != Variant::INT, false, "This snapshot is corrupted.");
+ ERR_FAIL_COND_V_MSG(node_data[1].get_type() != Variant::NODE_PATH, false, "This snapshot is corrupted.");
+
+ net_node_id = node_data[0];
+ node_path = node_data[1];
+
+ // Associate the ID with the path.
+ node_paths.set(net_node_id, node_path);
+
+ } else if (v.get_type() == Variant::INT) {
+ // Node info are in short form.
+ net_node_id = v;
+ NetUtility::NodeData *nd = scene_synchronizer->get_node_data(net_node_id);
+ if (nd != nullptr) {
+ synchronizer_node_data = nd;
+ goto node_lookup_out;
+ }
+ } else {
+ // The arrived snapshot does't seems to be in the expected form.
+ ERR_FAIL_V_MSG(false, "This snapshot is corrupted. Now the node is expected, " + String(v) + " was submitted instead.");
+ }
+
+ if (synchronizer_node_data == nullptr) {
+ if (node_path.is_empty()) {
+ const NodePath *node_path_ptr = node_paths.lookup_ptr(net_node_id);
+
+ if (node_path_ptr == nullptr) {
+ // Was not possible lookup the node_path.
+ NET_DEBUG_WARN("The node with ID `" + itos(net_node_id) + "` is not know by this peer, this is not supposed to happen.");
+ notify_server_full_snapshot_is_needed();
+ skip_this_node = true;
+ goto node_lookup_check;
+ } else {
+ node_path = *node_path_ptr;
+ }
+ }
+
+ node = scene_synchronizer->get_tree()->get_root()->get_node(node_path);
+
+ if (node == nullptr) {
+ // The node doesn't exists.
+ NET_DEBUG_ERR("The node " + node_path + " still doesn't exist.");
+ skip_this_node = true;
+ goto node_lookup_check;
+ }
+
+ // Register this node, so to make sure the client is tracking it.
+ NetUtility::NodeData *nd = scene_synchronizer->register_node(node);
+ if (nd != nullptr) {
+ // Set the node ID.
+ scene_synchronizer->set_node_data_id(nd, net_node_id);
+ synchronizer_node_data = nd;
+ } else {
+ NET_DEBUG_ERR("[BUG] This node " + node->get_path() + " was not know on this client. Though, was not possible to register it.");
+ skip_this_node = true;
+ }
+ }
+
+ node_lookup_check:
+ if (skip_this_node || synchronizer_node_data == nullptr) {
+ // This node does't exist; skip it entirely.
+ for (snap_data_index += 1; snap_data_index < raw_snapshot.size(); snap_data_index += 1) {
+ if (raw_snapshot_ptr[snap_data_index].get_type() == Variant::NIL) {
+ break;
+ }
+ }
+ ERR_CONTINUE_MSG(true, "This NetNodeId " + itos(net_node_id) + " doesn't exist on this client.");
+ }
+
+ node_lookup_out:
+
+#ifdef DEBUG_ENABLED
+ // At this point the ID is never UINT32_MAX thanks to the above
+ // mechanism.
+ CRASH_COND(synchronizer_node_data->id == UINT32_MAX);
+#endif
+
+ p_node_parse(p_user_pointer, synchronizer_node_data);
+
+ if (synchronizer_node_data->is_controller) {
+ // This is a controller, so the next data is the input ID.
+ ERR_FAIL_COND_V(snap_data_index + 1 >= raw_snapshot.size(), false);
+ snap_data_index += 1;
+ const uint32_t input_id = raw_snapshot_ptr[snap_data_index];
+ ERR_FAIL_COND_V_MSG(input_id == UINT32_MAX, false, "The server is always able to send input_id, so this snapshot seems corrupted.");
+
+ p_controller_parse(p_user_pointer, synchronizer_node_data, input_id);
+ }
+
+ } else if (var_id == UINT32_MAX) {
+ // When the node is known and the `var_id` not, we expect a
+ // new variable or the end pf this node data.
+
+ if (v.get_type() == Variant::NIL) {
+ // NIL found, so this node is done.
+ synchronizer_node_data = nullptr;
+ continue;
+ }
+
+ // This is a new variable, so let's take the variable name.
+
+ if (v.is_array()) {
+ // The variable info are stored in verbose mode.
+
+ const Vector var_data = v;
+ ERR_FAIL_COND_V(var_data.size() != 2, false);
+ ERR_FAIL_COND_V(var_data[0].get_type() != Variant::INT, false);
+ ERR_FAIL_COND_V(var_data[1].get_type() != Variant::STRING_NAME, false);
+
+ var_id = var_data[0];
+ StringName variable_name = var_data[1];
+
+ {
+ int64_t index = synchronizer_node_data->vars.find(variable_name);
+ if (index == -1) {
+ // The variable is not known locally, so just add it so
+ // to store the variable ID.
+ index = synchronizer_node_data->vars.size();
+
+ const bool skip_rewinding = false;
+ const bool enabled = false;
+ synchronizer_node_data->vars
+ .push_back(
+ NetUtility::VarData(
+ var_id,
+ variable_name,
+ Variant(),
+ skip_rewinding,
+ enabled));
+ NET_DEBUG_ERR("The variable " + variable_name + " for the node " + synchronizer_node_data->node->get_path() + " was not known on this client. This should never happen, make sure to register the same nodes on the client and server.");
+ }
+
+ if (index != var_id) {
+ if (synchronizer_node_data[var_id].id != UINT32_MAX) {
+ // It's not expected because if index is different to
+ // var_id, var_id should have a not yet initialized
+ // variable.
+ NET_DEBUG_ERR("This snapshot is corrupted. The var_id, at this point, must have a not yet init variable.");
+ notify_server_full_snapshot_is_needed();
+ return false;
+ }
+
+ // Make sure the variable is at the right index.
+ SWAP(synchronizer_node_data->vars[index], synchronizer_node_data->vars[var_id]);
+ }
+ }
+
+ // Make sure the ID is properly assigned.
+ synchronizer_node_data->vars[var_id].id = var_id;
+
+ } else if (v.get_type() == Variant::INT) {
+ // The variable is stored in the compact form.
+
+ var_id = v;
+
+ if (var_id >= synchronizer_node_data->vars.size() ||
+ synchronizer_node_data->vars[var_id].id == UINT32_MAX) {
+ NET_DEBUG_PRINT("The var with ID `" + itos(var_id) + "` is not know by this peer, this is not supposed to happen.");
+
+ notify_server_full_snapshot_is_needed();
+
+ // Skip the next data since it's the value of this variable.
+ snap_data_index += 1;
+ var_id = UINT32_MAX;
+ continue;
+ }
+
+ } else {
+ ERR_FAIL_V_MSG(false, "The snapshot received seems corrupted. The variable is expected but " + String(v) + " received instead.");
+ }
+
+ } else {
+ // The node is known, also the variable name is known, so the value
+ // is expected.
+
+ p_variable_parse(
+ p_user_pointer,
+ synchronizer_node_data,
+ var_id,
+ v);
+
+ // Just reset the variable name so we can continue iterate.
+ var_id = UINT32_MAX;
+ }
+ }
+
+ return true;
+}
+
+void ClientSynchronizer::set_enabled(bool p_enabled) {
+ if (enabled == p_enabled) {
+ // Nothing to do.
+ return;
+ }
+
+ if (p_enabled) {
+ // Postpone enabling when the next server snapshot is received.
+ want_to_enable = true;
+ } else {
+ // Disabling happens immediately.
+ enabled = false;
+ want_to_enable = false;
+ scene_synchronizer->emit_signal("sync_paused");
+ }
+}
+
+bool ClientSynchronizer::parse_snapshot(Variant p_snapshot) {
+ if (want_to_enable) {
+ if (enabled) {
+ NET_DEBUG_ERR("At this point the client is supposed to be disabled. This is a bug that must be solved.");
+ }
+ // The netwroking is disabled and we can re-enable it.
+ enabled = true;
+ want_to_enable = false;
+ scene_synchronizer->emit_signal("sync_started");
+ }
+
+ need_full_snapshot_notified = false;
+ last_received_snapshot.input_id = UINT32_MAX;
+
+ ERR_FAIL_COND_V_MSG(
+ player_controller_node_data == nullptr,
+ false,
+ "Is not possible to receive server snapshots if you are not tracking any NetController.");
+
+ struct ParseData {
+ NetUtility::Snapshot &snapshot;
+ NetUtility::NodeData *player_controller_node_data;
+ };
+
+ ParseData parse_data{
+ last_received_snapshot,
+ player_controller_node_data
+ };
+
+ const bool success = parse_sync_data(
+ p_snapshot,
+ &parse_data,
+
+ // Parse node:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data) {
+ ParseData *pd = static_cast(p_user_pointer);
+
+ // Make sure this node is part of the server node too.
+ if (uint32_t(pd->snapshot.node_vars.size()) <= p_node_data->id) {
+ pd->snapshot.node_vars.resize(p_node_data->id + 1);
+ }
+
+ // Make sure this snapshot has all the variables.
+ pd->snapshot.node_vars.write[p_node_data->id].resize(p_node_data->vars.size());
+ },
+
+ // Parse controller:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_input_id) {
+ ParseData *pd = static_cast(p_user_pointer);
+ if (p_node_data == pd->player_controller_node_data) {
+ // This is the main controller, store the input ID.
+ pd->snapshot.input_id = p_input_id;
+ }
+ },
+
+ // Parse variable:
+ [](void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_var_id, const Variant &p_value) {
+ ParseData *pd = static_cast(p_user_pointer);
+
+#ifdef DEBUG_ENABLED
+ // This can't be triggered because th `Parse Node` function
+ // above make sure to create room for this array.
+ CRASH_COND(uint32_t(pd->snapshot.node_vars.size()) <= p_node_data->id);
+#endif // ~DEBUG_ENABLED
+
+ if (unlikely(p_node_data->vars.size() != uint32_t(pd->snapshot.node_vars[p_node_data->id].size()))) {
+ // This mean the parser just added a new variable.
+ // Already notified by the parser.
+ pd->snapshot.node_vars.write[p_node_data->id].resize(p_node_data->vars.size());
+ }
+
+ pd->snapshot.node_vars.write[p_node_data->id].write[p_var_id].name = p_node_data->vars[p_var_id].var.name;
+ pd->snapshot.node_vars.write[p_node_data->id].write[p_var_id].value = p_value.duplicate(true);
+ });
+
+ if (success == false) {
+ NET_DEBUG_ERR("Snapshot:");
+ NET_DEBUG_ERR(p_snapshot.stringify());
+ return false;
+ }
+
+ // We espect that the player_controller is updated by this new snapshot,
+ // so make sure it's done so.
+ if (unlikely(last_received_snapshot.input_id == UINT32_MAX)) {
+ NET_DEBUG_PRINT("Recovery aborted, the player controller (" + player_controller_node_data->node->get_path() + ") was not part of the received snapshot, probably the server doesn't have important informations for this peer. NetUtility::Snapshot:");
+ NET_DEBUG_PRINT(p_snapshot.stringify());
+ return false;
+ } else {
+ // Success.
+
+ return true;
+ }
+}
+
+bool ClientSynchronizer::compare_vars(
+ const NetUtility::NodeData *p_synchronizer_node_data,
+ const Vector &p_server_vars,
+ const Vector &p_client_vars,
+ Vector &r_postponed_recover) {
+ const NetUtility::Var *s_vars = p_server_vars.ptr();
+ const NetUtility::Var *c_vars = p_client_vars.ptr();
+
+#ifdef DEBUG_ENABLED
+ bool diff = false;
+#endif
+
+ if (p_server_vars.size() != p_client_vars.size() ||
+ uint32_t(p_server_vars.size()) > p_synchronizer_node_data->vars.size()) {
+ NET_DEBUG_PRINT("Difference found: The server has a different variable count (" + itos(p_server_vars.size()) + ") compared to the client (" + itos(p_client_vars.size()) + ").");
+ NET_DEBUG_PRINT("Server variables:");
+ for (int i = 0; i < p_server_vars.size(); i += 1) {
+ NET_DEBUG_PRINT(" |- " + p_server_vars[i].name + ": " + p_server_vars[i].value.stringify());
+ }
+ NET_DEBUG_PRINT(" ------");
+
+ NET_DEBUG_PRINT("Client variables:");
+ for (int i = 0; i < p_client_vars.size(); i += 1) {
+ NET_DEBUG_PRINT(" |- " + p_client_vars[i].name + ": " + p_client_vars[i].value.stringify());
+ }
+ NET_DEBUG_PRINT(" ------");
+ return true;
+ }
+
+ for (uint32_t var_index = 0; var_index < uint32_t(p_server_vars.size()); var_index += 1) {
+ if (s_vars[var_index].name == StringName()) {
+ // This variable was not set, skip the check.
+ continue;
+ }
+
+ // Compare.
+ const bool different =
+ // Make sure this variable is set.
+ c_vars[var_index].name == StringName() ||
+ // Check if the value is different.
+ !scene_synchronizer->compare(
+ s_vars[var_index].value,
+ c_vars[var_index].value);
+
+ if (different) {
+ if (p_synchronizer_node_data->vars[var_index].skip_rewinding) {
+ // The vars are different, but this variable don't what to
+ // trigger a rewind.
+ r_postponed_recover.push_back(s_vars[var_index]);
+ } else {
+ // The vars are different.
+ NET_DEBUG_PRINT("Difference found on var #" + itos(var_index) + " " + p_synchronizer_node_data->vars[var_index].var.name + " " +
+ "Server value: `" + s_vars[var_index].value.stringify() + "` " +
+ "Client value: `" + c_vars[var_index].value.stringify() + "`. " +
+ "[Server name: `" + s_vars[var_index].name + "` " +
+ "Client name: `" + c_vars[var_index].name + "`].");
+#ifdef DEBUG_ENABLED
+ diff = true;
+#else
+ return true;
+#endif
+ }
+ }
+ }
+
+#ifdef DEBUG_ENABLED
+ return diff;
+#else
+
+ // The vars are not different.
+ return false;
+#endif
+}
+
+void ClientSynchronizer::notify_server_full_snapshot_is_needed() {
+ if (need_full_snapshot_notified) {
+ return;
+ }
+
+ // Notify the server that a full snapshot is needed.
+ need_full_snapshot_notified = true;
+ scene_synchronizer->rpc_id(1, SNAME("_rpc_notify_need_full_snapshot"));
+}
diff --git a/modules/network_synchronizer/scene_synchronizer.h b/modules/network_synchronizer/scene_synchronizer.h
new file mode 100644
index 000000000000..cda18e891d30
--- /dev/null
+++ b/modules/network_synchronizer/scene_synchronizer.h
@@ -0,0 +1,464 @@
+/*************************************************************************/
+/* scene_synchronizer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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. */
+/*************************************************************************/
+
+/**
+ @author AndreaCatania
+*/
+
+#include "scene/main/node.h"
+
+#include "core/templates/local_vector.h"
+#include "core/templates/oa_hash_map.h"
+#include "net_utilities.h"
+#include
+
+#ifndef SCENE_SYNCHRONIZER_H
+#define SCENE_SYNCHRONIZER_H
+
+class Synchronizer;
+class NetworkedController;
+
+/// # SceneSynchronizer
+///
+/// The `SceneSynchronizer` is responsible to keep the scene of all peers in sync.
+/// Usually each peer has it istantiated, and depending if it's istantiated in
+/// the server or in the client, it does a different thing.
+///
+/// ## The `Player` is playing the game on the server.
+///
+/// The server is authoritative and it can't never be wrong. For this reason
+/// the `SceneSynchronizer` on the server sends at a fixed interval (defined by
+/// `server_notify_state_interval`) a snapshot to all peers.
+///
+/// The clients receives the server snapshot, so it compares with the local
+/// snapshot and if it's necessary perform the recovery.
+///
+/// ## Variable traking
+///
+/// The `SceneSynchronizer` is able to track any node variable. It's possible to specify
+/// the variables to track using the function `register_variable`.
+///
+/// ## NetworkedController
+/// The `NetworkedController` is able to aquire the `Player` input and perform
+/// operation in sync with other peers. When a discrepancy is found by the
+/// `SceneSynchronizer`, it will drive the `NetworkedController` so to recover that
+/// missalignment.
+///
+///
+/// ## Processing function
+/// Some objects, that are not direclty controlled by a `Player`, may need to be
+/// in sync between peers; since those are not controlled by a `Player` is
+/// not necessary use the `NetworkedController`.
+///
+/// It's possible to specify some process functions using `register_process`.
+/// The `SceneSynchronizer` will call these functions each frame, in sync with the
+/// other peers.
+///
+/// As example object we may think about a moving platform, or a bridge that
+/// opens and close, or even a simple timer to track the match time.
+/// An example implementation would be:
+/// ```
+/// var time := 0.0
+///
+/// func _ready():
+/// # Make sure this never go out of sync.
+/// SceneSynchronizer.register_variable(self, "time")
+///
+/// # Make sure to call this in sync with other peers.
+/// SceneSynchronizer.register_process(self, "in_sync_process")
+///
+/// func in_sync_process(delta: float):
+/// time += delta
+/// ```
+/// In the above code the variable `time` will always be in sync.
+///
+//
+// # Implementation details.
+//
+// The entry point of the above mechanism is the function `SceneSynchronizer::process()`.
+// The server `SceneSynchronizer` code is inside the class `ServerSynchronizer`.
+// The client `SceneSynchronizer` code is inside the class `ClientSynchronizer`.
+// The no networking `SceneSynchronizer` code is inside the class `NoNetSynchronizer`.
+class SceneSynchronizer : public Node {
+ GDCLASS(SceneSynchronizer, Node);
+
+ friend class Synchronizer;
+ friend class ServerSynchronizer;
+ friend class ClientSynchronizer;
+ friend class NoNetSynchronizer;
+ friend class SceneDiff;
+
+public:
+ enum SynchronizerType {
+ SYNCHRONIZER_TYPE_NULL,
+ SYNCHRONIZER_TYPE_NONETWORK,
+ SYNCHRONIZER_TYPE_CLIENT,
+ SYNCHRONIZER_TYPE_SERVER
+ };
+
+private:
+ real_t server_notify_state_interval = 1.0;
+ real_t comparison_float_tolerance = 0.001;
+
+ SynchronizerType synchronizer_type = SYNCHRONIZER_TYPE_NULL;
+ Synchronizer *synchronizer = nullptr;
+ bool recover_in_progress = false;
+ bool reset_in_progress = false;
+ bool rewinding_in_progress = false;
+ bool end_sync = false;
+
+ bool peer_dirty = false;
+ OAHashMap peer_data;
+
+ bool generate_id = false;
+
+ // All possible registered nodes.
+ LocalVector node_data;
+
+ // All registered nodes, that have the NetworkNodeId assigned, organized per
+ // NetworkNodeId.
+ LocalVector organized_node_data;
+
+ // Controller nodes.
+ LocalVector node_data_controllers;
+
+ // Just used to detect when the peer change. TODO Remove this and use a singnal instead.
+ void *peer_ptr = nullptr;
+
+ int event_flag;
+ LocalVector event_listener;
+
+public:
+ static void _bind_methods();
+
+ virtual void _notification(int p_what);
+
+public:
+ SceneSynchronizer();
+ ~SceneSynchronizer();
+
+ void set_doll_desync_tolerance(int p_tolerance);
+ int get_doll_desync_tolerance() const;
+
+ void set_server_notify_state_interval(real_t p_interval);
+ real_t get_server_notify_state_interval() const;
+
+ void set_comparison_float_tolerance(real_t p_tolerance);
+ real_t get_comparison_float_tolerance() const;
+
+ /// Register a new node and returns its `NodeData`.
+ NetUtility::NodeData *register_node(Node *p_node);
+ uint32_t register_node_gdscript(Node *p_node);
+ void unregister_node(Node *p_node);
+
+ /// Returns the node ID.
+ /// This may return `UINT32_MAX` in various cases:
+ /// - The node is not registered.
+ /// - The client doesn't know the ID yet.
+ uint32_t get_node_id(Node *p_node);
+ Node *get_node_from_id(uint32_t p_id);
+
+ void register_variable(Node *p_node, const StringName &p_variable, const StringName &p_on_change_notify_to = StringName(), NetEventFlag p_flags = NetEventFlag::DEFAULT);
+ void unregister_variable(Node *p_node, const StringName &p_variable);
+
+ /// Start local node sync.
+ void start_node_sync(const Node *p_node);
+ /// Stop local node sync.
+ void stop_node_sync(const Node *p_node);
+ bool is_node_sync(const Node *p_node) const;
+
+ /// Returns the variable ID relative to the `Node`.
+ /// This may return `UINT32_MAX` in various cases:
+ /// - The node is not registered.
+ /// - The variable is not registered.
+ /// - The client doesn't know the ID yet.
+ uint32_t get_variable_id(Node *p_node, const StringName &p_variable);
+
+ void set_skip_rewinding(Node *p_node, const StringName &p_variable, bool p_skip_rewinding);
+
+ void track_variable_changes(Node *p_node, const StringName &p_variable, Object *p_object, const StringName &p_method, NetEventFlag p_flags = NetEventFlag::DEFAULT);
+ void untrack_variable_changes(Node *p_node, const StringName &p_variable, Object *p_object, const StringName &p_method);
+
+ void set_node_as_controlled_by(Node *p_node, Node *p_controller);
+
+ /// Add a dependency to a controller, so that the rewinding mechanism can
+ /// make sure to rewind that node when the controller is rewinded.
+ /// You can remove and add dependency at any time. This operation
+ /// don't need to be perfomed on server.
+ void controller_add_dependency(Node *p_controller, Node *p_node);
+ void controller_remove_dependency(Node *p_controller, Node *p_node);
+ int controller_get_dependency_count(Node *p_controller) const;
+ Node *controller_get_dependency(Node *p_controller, int p_index);
+
+ void register_process(Node *p_node, const StringName &p_function);
+ void unregister_process(Node *p_node, const StringName &p_function);
+
+ void start_tracking_scene_changes(Object *p_diff_handle) const;
+ void stop_tracking_scene_changes(Object *p_diff_handle) const;
+ Variant pop_scene_changes(Object *p_diff_handle) const;
+ void apply_scene_changes(const Variant &p_sync_data);
+
+ bool is_recovered() const;
+ bool is_resetted() const;
+ bool is_rewinding() const;
+ bool is_end_sync() const;
+
+ /// This function works only on server.
+ void force_state_notify();
+ /// Make peers as dirty, so they will be reloaded next frame.
+ void dirty_peers();
+
+ void set_enabled(bool p_enable);
+ bool is_enabled() const;
+
+ void set_peer_networking_enable(int p_peer, bool p_enable);
+ bool is_peer_networking_enable(int p_peer) const;
+
+ void _on_peer_connected(int p_peer);
+ void _on_peer_disconnected(int p_peer);
+
+ void _on_node_removed(Node *p_node);
+
+ void reset_synchronizer_mode();
+ void clear();
+
+ void _rpc_send_state(const Variant &p_snapshot);
+ void _rpc_notify_need_full_snapshot();
+ void _rpc_set_network_enabled(bool p_enabled);
+ void _rpc_notify_peer_status(bool p_enabled);
+
+ void update_peers();
+ void clear_peers();
+
+ void change_events_begin(int p_flag);
+ void change_event_add(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old);
+ void change_events_flush();
+
+private:
+ void expand_organized_node_data_vector(uint32_t p_size);
+
+ /// This function is slow, but allow to take the node data even if the
+ /// `NetNodeId` is not yet assigned.
+ NetUtility::NodeData *find_node_data(const Node *p_node);
+ const NetUtility::NodeData *find_node_data(const Node *p_node) const;
+
+ /// This function is super fast, but only nodes with a `NetNodeId` assigned
+ /// can be returned.
+ NetUtility::NodeData *get_node_data(NetNodeId p_id);
+ const NetUtility::NodeData *get_node_data(NetNodeId p_id) const;
+
+ /// Returns the latest generated `NetNodeId`.
+ NetNodeId get_biggest_node_id() const;
+
+ void reset_controllers();
+ void reset_controller(NetUtility::NodeData *p_controller);
+
+ void process();
+
+#ifdef DEBUG_ENABLED
+ void validate_nodes();
+#endif
+ void purge_node_dependencies();
+
+ real_t get_pretended_delta() const;
+
+ /// Read the node variables and store the value if is different from the
+ /// previous one and emits a signal.
+ void pull_node_changes(NetUtility::NodeData *p_node_data);
+
+ /// Add node data and generates the `NetNodeId` if allowed.
+ void add_node_data(NetUtility::NodeData *p_node_data);
+ void drop_node_data(NetUtility::NodeData *p_node_data);
+
+ /// Set the node data net id.
+ void set_node_data_id(NetUtility::NodeData *p_node_data, NetNodeId p_id);
+
+public:
+ /// Returns true when the vectors are the same. Uses comparison_float_tolerance member.
+ bool compare(const Vector2 &p_first, const Vector2 &p_second) const;
+ /// Returns true when the vectors are the same. Uses comparison_float_tolerance member.
+ bool compare(const Vector3 &p_first, const Vector3 &p_second) const;
+ /// Returns true when the variants are the same. Uses comparison_float_tolerance member.
+ bool compare(const Variant &p_first, const Variant &p_second) const;
+
+ /// Returns true when the vectors are the same.
+ static bool compare(const Vector2 &p_first, const Vector2 &p_second, real_t p_tolerance);
+ /// Returns true when the vectors are the same.
+ static bool compare(const Vector3 &p_first, const Vector3 &p_second, real_t p_tolerance);
+ /// Returns true when the variants are the same.
+ static bool compare(const Variant &p_first, const Variant &p_second, real_t p_tolerance);
+
+ /// Returns true if this peer is server.
+ bool is_server() const;
+ /// Returns true if this peer is client.
+ bool is_client() const;
+ /// Returns true if there is no network.
+ bool is_no_network() const;
+ /// Returns true if network is enabled.
+ bool is_networked() const;
+};
+
+class Synchronizer {
+protected:
+ SceneSynchronizer *scene_synchronizer;
+
+public:
+ Synchronizer(SceneSynchronizer *p_node);
+ virtual ~Synchronizer() = default;
+
+ virtual void clear() = 0;
+
+ virtual void process() = 0;
+ virtual void on_node_added(NetUtility::NodeData *p_node_data) {}
+ virtual void on_node_removed(NetUtility::NodeData *p_node_data) {}
+ virtual void on_variable_added(NetUtility::NodeData *p_node_data, const StringName &p_var_name) {}
+ virtual void on_variable_changed(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old_value, int p_flag) {}
+ virtual void on_controller_reset(NetUtility::NodeData *p_node_data) {}
+};
+
+class NoNetSynchronizer : public Synchronizer {
+ friend class SceneSynchronizer;
+
+ bool enabled = true;
+
+public:
+ NoNetSynchronizer(SceneSynchronizer *p_node);
+
+ virtual void clear() override;
+ virtual void process() override;
+
+ void set_enabled(bool p_enabled);
+ bool is_enabled() const;
+};
+
+class ServerSynchronizer : public Synchronizer {
+ friend class SceneSynchronizer;
+
+ real_t state_notifier_timer = 0.0;
+
+ struct Change {
+ bool not_known_before = false;
+ Set uknown_vars;
+ Set vars;
+ };
+
+ /// The changes; the order matters because the index is the NetNodeId.
+ LocalVector changes;
+
+public:
+ ServerSynchronizer(SceneSynchronizer *p_node);
+
+ virtual void clear() override;
+ virtual void process() override;
+ virtual void on_node_added(NetUtility::NodeData *p_node_data) override;
+ virtual void on_variable_added(NetUtility::NodeData *p_node_data, const StringName &p_var_name) override;
+ virtual void on_variable_changed(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old_value, int p_flag) override;
+
+ void process_snapshot_notificator(real_t p_delta);
+ Vector global_nodes_generate_snapshot(bool p_force_full_snapshot) const;
+ void controller_generate_snapshot(const NetUtility::NodeData *p_node_data, bool p_force_full_snapshot, Vector &r_snapshot_result) const;
+ void generate_snapshot_node_data(const NetUtility::NodeData *p_node_data, bool p_force_full_snapshot, Vector &r_result) const;
+};
+
+class ClientSynchronizer : public Synchronizer {
+ friend class SceneSynchronizer;
+
+ NetUtility::NodeData *player_controller_node_data = nullptr;
+ OAHashMap node_paths;
+
+ NetUtility::Snapshot last_received_snapshot;
+ std::deque client_snapshots;
+ std::deque server_snapshots;
+ uint32_t last_checked_input = 0;
+ bool enabled = true;
+ bool want_to_enable = false;
+
+ bool need_full_snapshot_notified = false;
+
+ struct EndSyncEvent {
+ NetUtility::NodeData *node_data;
+ NetVarId var_id;
+ Variant old_value;
+
+ bool operator<(const EndSyncEvent &p_other) const {
+ if (node_data->id == p_other.node_data->id) {
+ return var_id < p_other.var_id;
+ } else {
+ return node_data->id < p_other.node_data->id;
+ }
+ }
+ };
+
+ Set sync_end_events;
+
+public:
+ ClientSynchronizer(SceneSynchronizer *p_node);
+
+ virtual void clear() override;
+
+ virtual void process() override;
+ virtual void on_node_added(NetUtility::NodeData *p_node_data) override;
+ virtual void on_node_removed(NetUtility::NodeData *p_node_data) override;
+ virtual void on_variable_changed(NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_old_value, int p_flag) override;
+ virtual void on_controller_reset(NetUtility::NodeData *p_node_data) override;
+
+ void receive_snapshot(Variant p_snapshot);
+ bool parse_sync_data(
+ Variant p_snapshot,
+ void *p_user_pointer,
+ void (*p_node_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data),
+ void (*p_controller_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data, uint32_t p_input_id),
+ void (*p_variable_parse)(void *p_user_pointer, NetUtility::NodeData *p_node_data, NetVarId p_var_id, const Variant &p_value));
+
+ void set_enabled(bool p_enabled);
+
+private:
+ /// Store node data organized per controller.
+ void store_snapshot();
+
+ void store_controllers_snapshot(
+ const NetUtility::Snapshot &p_snapshot,
+ std::deque &r_snapshot_storage);
+
+ void process_controllers_recovery(real_t p_delta);
+ void process_paused_controller_recovery(real_t p_delta);
+ bool parse_snapshot(Variant p_snapshot);
+ bool compare_vars(
+ const NetUtility::NodeData *p_synchronizer_node_data,
+ const Vector &p_server_vars,
+ const Vector &p_client_vars,
+ Vector &r_postponed_recover);
+
+ void notify_server_full_snapshot_is_needed();
+};
+
+VARIANT_ENUM_CAST(NetEventFlag)
+
+#endif
diff --git a/modules/network_synchronizer/tests/test_bit_array.h b/modules/network_synchronizer/tests/test_bit_array.h
new file mode 100644
index 000000000000..9aa13124e0ab
--- /dev/null
+++ b/modules/network_synchronizer/tests/test_bit_array.h
@@ -0,0 +1,120 @@
+/*************************************************************************/
+/* test_bit_array.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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 TEST_BIT_ARRAY_H
+#define TEST_BIT_ARRAY_H
+
+#include "modules/network_synchronizer/bit_array.h"
+
+#include "tests/test_macros.h"
+
+namespace TestBitArray {
+
+TEST_CASE("[Modules][BitArray] Read and write") {
+ BitArray array;
+ int offset = 0;
+ int bits = {};
+ uint64_t value = {};
+
+ SUBCASE("[Modules][BitArray] One bit") {
+ bits = 1;
+
+ SUBCASE("[Modules][BitArray] One") {
+ value = 0b1;
+ }
+ SUBCASE("[Modules][BitArray] Zero") {
+ value = 0b0;
+ }
+ }
+ SUBCASE("[Modules][BitArray] 16 mixed bits") {
+ bits = 16;
+ value = 0b1010101010101010;
+ }
+ SUBCASE("[Modules][BitArray] One and 4 zeroes") {
+ bits = 5;
+ value = 0b10000;
+ }
+ SUBCASE("[Modules][BitArray] 64 bits") {
+ bits = 64;
+
+ SUBCASE("[Modules][BitArray] One") {
+ value = UINT64_MAX;
+ }
+ SUBCASE("[Modules][BitArray] Zero") {
+ value = 0;
+ }
+ }
+ SUBCASE("[Modules][BitArray] One bit with offset") {
+ bits = 1;
+ offset = 64;
+ array.resize_in_bits(offset);
+
+ SUBCASE("[Modules][BitArray] One") {
+ array.store_bits(0, UINT64_MAX, 64);
+ value = 0b0;
+ }
+ SUBCASE("[Modules][BitArray] Zero") {
+ array.store_bits(0, 0, 64);
+ value = 0b1;
+ }
+ }
+
+ array.resize_in_bits(offset + bits);
+ array.store_bits(offset, value, bits);
+ CHECK_MESSAGE(array.read_bits(offset, bits) == value, "Should read the same value");
+}
+
+TEST_CASE("[Modules][BitArray] Constructing from Vector") {
+ Vector data;
+ data.push_back(-1);
+ data.push_back(0);
+ data.push_back(1);
+
+ const BitArray array(data);
+ CHECK_MESSAGE(array.size_in_bits() == data.size() * 8.0, "Number of bits must be equal to size of original data");
+ CHECK_MESSAGE(array.size_in_bytes() == data.size(), "Number of bytes must be equal to size of original data");
+ for (int i = 0; i < data.size(); ++i) {
+ CHECK_MESSAGE(array.read_bits(i * 8, 8) == data[i], "Readed bits should be equal to the original");
+ }
+}
+
+TEST_CASE("[Modules][BitArray] Pre-allocation and zeroing") {
+ constexpr uint64_t value = UINT64_MAX;
+ constexpr int bits = sizeof(value);
+
+ BitArray array(bits);
+ CHECK_MESSAGE(array.size_in_bits() == bits, "Number of bits must be equal to allocated");
+ array.store_bits(0, value, bits);
+ array.zero();
+ CHECK_MESSAGE(array.read_bits(0, bits) == 0, "Should read zero");
+}
+} // namespace TestBitArray
+
+#endif // TEST_BIT_ARRAY_H
diff --git a/modules/network_synchronizer/tests/test_data_buffer.h b/modules/network_synchronizer/tests/test_data_buffer.h
new file mode 100644
index 000000000000..bee7336f7f6e
--- /dev/null
+++ b/modules/network_synchronizer/tests/test_data_buffer.h
@@ -0,0 +1,550 @@
+/*************************************************************************/
+/* test_data_buffer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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 TEST_DATA_BUFFER_H
+#define TEST_DATA_BUFFER_H
+
+#include "modules/network_synchronizer/data_buffer.h"
+#include "modules/network_synchronizer/scene_synchronizer.h"
+
+#include "tests/test_macros.h"
+
+namespace TestDataBuffer {
+
+inline Vector real_values(DataBuffer::CompressionLevel p_compression_level) {
+ Vector values;
+ values.append(Math_PI);
+ values.append(0.0);
+ values.append(-3.04);
+ values.append(3.04);
+ values.append(0.5);
+ values.append(-0.5);
+ values.append(1);
+ values.append(-1);
+ values.append(0.9);
+ values.append(-0.9);
+ values.append(3.9);
+ values.append(-3.9);
+ values.append(8);
+
+ switch (p_compression_level) {
+ case DataBuffer::COMPRESSION_LEVEL_3: {
+ values.append(-15'360);
+ values.append(15'360);
+ } break;
+ case DataBuffer::COMPRESSION_LEVEL_2: {
+ // https://en.wikipedia.org/wiki/Half-precision_floating-point_format#Half_precision_examples
+ values.append(-65'504);
+ values.append(65'504);
+ values.append(Math::pow(2.0, -14) / 1024);
+ values.append(Math::pow(2.0, -14) * 1023 / 1024);
+ values.append(Math::pow(2.0, -1) * (1 + 1023.0 / 1024));
+ values.append((1 + 1.0 / 1024));
+ } break;
+ case DataBuffer::COMPRESSION_LEVEL_1: {
+ // https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Single-precision_examples
+ values.append(FLT_MIN);
+ values.append(-FLT_MAX);
+ values.append(FLT_MAX);
+ values.append(Math::pow(2.0, -149));
+ values.append(Math::pow(2.0, -126) * (1 - Math::pow(2.0, -23)));
+ values.append(1 - Math::pow(2.0, -24));
+ values.append(1 + Math::pow(2.0, -23));
+ } break;
+ case DataBuffer::COMPRESSION_LEVEL_0: {
+ // https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Double-precision_examples
+ values.append(DBL_MIN);
+ values.append(DBL_MAX);
+ values.append(-DBL_MAX);
+ values.append(1.0000000000000002);
+ values.append(4.9406564584124654 * Math::pow(10.0, -324));
+ values.append(2.2250738585072009 * Math::pow(10.0, -308));
+ } break;
+ }
+
+ return values;
+}
+
+TEST_CASE("[Modules][DataBuffer] Bool") {
+ bool value = {};
+
+ SUBCASE("[Modules][DataBuffer] false") {
+ value = false;
+ }
+ SUBCASE("[Modules][DataBuffer] true") {
+ value = true;
+ }
+
+ DataBuffer buffer;
+ buffer.begin_write(0);
+ CHECK_MESSAGE(buffer.add_bool(value) == value, "Should return the same value");
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_bool() == value, "Should read the same value");
+}
+
+TEST_CASE("[Modules][DataBuffer] Int") {
+ DataBuffer::CompressionLevel compression_level = {};
+ int64_t value = {};
+
+ DataBuffer buffer;
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+
+ SUBCASE("[Modules][DataBuffer] Positive") {
+ value = 127;
+ }
+ SUBCASE("[Modules][DataBuffer] Zero") {
+ value = 0;
+ }
+ SUBCASE("[Modules][DataBuffer] Negative") {
+ value = -128;
+ }
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+
+ SUBCASE("[Modules][DataBuffer] Positive") {
+ value = 32767;
+ }
+ SUBCASE("[Modules][DataBuffer] Zero") {
+ value = 0;
+ }
+ SUBCASE("[Modules][DataBuffer] Negative") {
+ value = -32768;
+ }
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+
+ SUBCASE("[Modules][DataBuffer] Positive") {
+ value = 2147483647;
+ }
+ SUBCASE("[Modules][DataBuffer] Zero") {
+ value = 0;
+ }
+ SUBCASE("[Modules][DataBuffer] Negative") {
+ value = -2147483648LL;
+ }
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+
+ SUBCASE("[Modules][DataBuffer] Positive") {
+ value = 2147483647;
+ }
+ SUBCASE("[Modules][DataBuffer] Zero") {
+ value = 0;
+ }
+ SUBCASE("[Modules][DataBuffer] Negative") {
+ value = -9223372036854775807LL;
+ }
+ }
+
+ buffer.begin_write(0);
+ CHECK_MESSAGE(buffer.add_int(value, compression_level) == value, "Should return the same value");
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_int(compression_level) == value, "Should read the same value");
+}
+
+TEST_CASE("[Modules][DataBuffer] Real") {
+ DataBuffer::CompressionLevel compression_level = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3 (Minifloat)") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2 (Half perception)") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1 (Single perception)") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0 (Double perception)") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ }
+
+ DataBuffer buffer;
+ const Vector values = real_values(compression_level);
+ const double epsilon = Math::pow(2.0, DataBuffer::get_mantissa_bits(compression_level) - 1);
+ for (int i = 0; i < values.size(); ++i) {
+ buffer.begin_write(0);
+ const double value = values[i];
+ CHECK_MESSAGE(buffer.add_real(value, compression_level) == doctest::Approx(value).epsilon(epsilon), "Should return the same value");
+
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_real(compression_level) == doctest::Approx(value).epsilon(epsilon), "Should read the same value");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Positive unit real") {
+ DataBuffer::CompressionLevel compression_level = {};
+ double epsilon = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ epsilon = 0.033335;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ epsilon = 0.007935;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ epsilon = 0.00196;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ epsilon = 0.00049;
+ }
+
+ DataBuffer buffer;
+ const Vector values = real_values(compression_level);
+ for (int i = 0; i < values.size(); ++i) {
+ const double value = values[i];
+ if (value < 0) {
+ // Skip negative values
+ continue;
+ }
+ double value_integral;
+ const double value_unit = modf(values[i], &value_integral);
+ buffer.begin_write(0);
+ CHECK_MESSAGE(buffer.add_positive_unit_real(value_unit, compression_level) == doctest::Approx(value_unit).epsilon(epsilon), "Should return the same value");
+
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_positive_unit_real(compression_level) == doctest::Approx(value_unit).epsilon(epsilon), "Should read the same value");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Unit real") {
+ DataBuffer::CompressionLevel compression_level = {};
+ double epsilon = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ epsilon = 0.033335;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ epsilon = 0.007935;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ epsilon = 0.00196;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ epsilon = 0.00049;
+ }
+
+ DataBuffer buffer;
+ const Vector values = real_values(compression_level);
+ for (int i = 0; i < values.size(); ++i) {
+ double value_integral;
+ const double value_unit = modf(values[i], &value_integral);
+ buffer.begin_write(0);
+ CHECK_MESSAGE(buffer.add_unit_real(value_unit, compression_level) == doctest::Approx(value_unit).epsilon(epsilon), "Should return the same value");
+
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_unit_real(compression_level) == doctest::Approx(value_unit).epsilon(epsilon), "Should read the same value");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Vector2") {
+ DataBuffer::CompressionLevel compression_level = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ }
+
+ DataBuffer buffer;
+ const double epsilon = Math::pow(2.0, DataBuffer::get_mantissa_bits(compression_level) - 1);
+ const Vector values = real_values(compression_level);
+ for (int i = 0; i < values.size(); ++i) {
+#ifdef REAL_T_IS_DOUBLE
+ const Vector2 value = Vector2(values[i], values[i]);
+#else
+ const real_t clamped_value = CLAMP(values[i], -FLT_MIN, FLT_MAX);
+ const Vector2 value = Vector2(clamped_value, clamped_value);
+#endif
+ buffer.begin_write(0);
+ const Vector2 added_value = buffer.add_vector2(value, compression_level);
+ CHECK_MESSAGE(added_value.x == doctest::Approx(value.x).epsilon(epsilon), "Added Vector2 should have the same x axis");
+ CHECK_MESSAGE(added_value.y == doctest::Approx(value.y).epsilon(epsilon), "Added Vector2 should have the same y axis");
+
+ buffer.begin_read();
+ const Vector2 read_value = buffer.read_vector2(compression_level);
+ CHECK_MESSAGE(read_value.x == doctest::Approx(value.x).epsilon(epsilon), "Read Vector2 should have the same x axis");
+ CHECK_MESSAGE(read_value.y == doctest::Approx(value.y).epsilon(epsilon), "Read Vector2 should have the same y axis");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Vector3") {
+ DataBuffer::CompressionLevel compression_level = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ }
+
+ DataBuffer buffer;
+ const Vector values = real_values(compression_level);
+ const double epsilon = Math::pow(2.0, DataBuffer::get_mantissa_bits(compression_level) - 1);
+ for (int i = 0; i < values.size(); ++i) {
+#ifdef REAL_T_IS_DOUBLE
+ const Vector3 value = Vector3(values[i], values[i], values[i]);
+#else
+ const real_t clamped_value = CLAMP(values[i], -FLT_MIN, FLT_MAX);
+ const Vector3 value = Vector3(clamped_value, clamped_value, clamped_value);
+#endif
+ buffer.begin_write(0);
+ const Vector3 added_value = buffer.add_vector3(value, compression_level);
+ CHECK_MESSAGE(added_value.x == doctest::Approx(value.x).epsilon(epsilon), "Added Vector3 should have the same x axis");
+ CHECK_MESSAGE(added_value.y == doctest::Approx(value.y).epsilon(epsilon), "Added Vector3 should have the same y axis");
+ CHECK_MESSAGE(added_value.z == doctest::Approx(value.z).epsilon(epsilon), "Added Vector3 should have the same z axis");
+
+ buffer.begin_read();
+ const Vector3 read_value = buffer.read_vector3(compression_level);
+ CHECK_MESSAGE(read_value.x == doctest::Approx(value.x).epsilon(epsilon), "Read Vector3 should have the same x axis");
+ CHECK_MESSAGE(read_value.y == doctest::Approx(value.y).epsilon(epsilon), "Read Vector3 should have the same y axis");
+ CHECK_MESSAGE(read_value.z == doctest::Approx(value.z).epsilon(epsilon), "Read Vector3 should have the same z axis");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Normalized Vector3") {
+ DataBuffer::CompressionLevel compression_level = {};
+ double epsilon = {};
+
+ SUBCASE("[Modules][DataBuffer] Compression level 3") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_3;
+ epsilon = 0.033335;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 2") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_2;
+ epsilon = 0.007935;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 1") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_1;
+ epsilon = 0.00196;
+ }
+
+ SUBCASE("[Modules][DataBuffer] Compression level 0") {
+ compression_level = DataBuffer::COMPRESSION_LEVEL_0;
+ epsilon = 0.00049;
+ }
+
+ DataBuffer buffer;
+ const Vector values = real_values(compression_level);
+ for (int i = 0; i < values.size(); ++i) {
+ Vector3 value = Vector3(values[i], values[i], values[i]).normalized();
+ if (!value.is_normalized()) {
+ // Normalization fails for some numbers, probably a bug!
+ continue;
+ }
+ buffer.begin_write(0);
+ const Vector3 added_value = buffer.add_normalized_vector3(value, compression_level);
+ CHECK_MESSAGE(added_value.x == doctest::Approx(value.x).epsilon(epsilon), "Added Vector3 should have the same x axis");
+ CHECK_MESSAGE(added_value.y == doctest::Approx(value.y).epsilon(epsilon), "Added Vector3 should have the same y axis");
+ CHECK_MESSAGE(added_value.z == doctest::Approx(value.z).epsilon(epsilon), "Added Vector3 should have the same z axis");
+
+ buffer.begin_read();
+ const Vector3 read_value = buffer.read_normalized_vector3(compression_level);
+ CHECK_MESSAGE(read_value.x == doctest::Approx(value.x).epsilon(epsilon), "Read Vector3 should have the same x axis");
+ CHECK_MESSAGE(read_value.y == doctest::Approx(value.y).epsilon(epsilon), "Read Vector3 should have the same y axis");
+ CHECK_MESSAGE(read_value.z == doctest::Approx(value.z).epsilon(epsilon), "Read Vector3 should have the same z axis");
+ }
+}
+
+TEST_CASE("[Modules][DataBuffer] Variant") {
+ Variant value = {};
+
+ SUBCASE("[Modules][DataBuffer] Invalid value") {
+ value = {};
+ }
+ SUBCASE("[Modules][DataBuffer] String") {
+ value = "VariantString";
+ }
+ SUBCASE("[Modules][DataBuffer] Vector") {
+ value = sarray("VariantString1", "VariantString2", "VariantString3");
+ }
+ SUBCASE("[Modules][DataBuffer] Dictionary") {
+ Dictionary dictionary;
+ dictionary[1] = "Value";
+ dictionary["Key"] = -1;
+ value = dictionary;
+ }
+ SUBCASE("[Modules][DataBuffer] Array") {
+ Array array;
+ array.append("VariantString");
+ array.append(0);
+ array.append(-1.2);
+ value = array;
+ }
+
+ DataBuffer buffer;
+ buffer.begin_write(0);
+ CHECK_MESSAGE(SceneSynchronizer::compare(buffer.add_variant(value), value, DBL_EPSILON), "Should return the same value");
+ buffer.begin_read();
+ CHECK_MESSAGE(SceneSynchronizer::compare(buffer.read_variant(), value, DBL_EPSILON), "Should read the same value");
+}
+
+TEST_CASE("[Modules][DataBuffer] Seek") {
+ DataBuffer buffer;
+ buffer.begin_write(0);
+ buffer.add_bool(true);
+ buffer.add_bool(false);
+ buffer.begin_read();
+
+ ERR_PRINT_OFF
+ buffer.seek(-1);
+ CHECK_MESSAGE(buffer.get_bit_offset() == 0, "Bit offset should fail for negative values");
+ ERR_PRINT_ON
+
+ buffer.seek(1);
+ CHECK_MESSAGE(buffer.get_bit_offset() == 1, "Bit offset should be 1 after seek to 1");
+ CHECK_MESSAGE(buffer.read_bool() == false, "Should read false at position 1");
+
+ buffer.seek(0);
+ CHECK_MESSAGE(buffer.get_bit_offset() == 0, "Bit offset should be 0 after seek to 0");
+ CHECK_MESSAGE(buffer.read_bool() == true, "Should read true at position 0");
+}
+
+TEST_CASE("[Modules][DataBuffer] Metadata") {
+ bool value = {};
+ bool metadata = {};
+
+ SUBCASE("[Modules][DataBuffer] True") {
+ metadata = true;
+ value = false;
+ }
+
+ SUBCASE("[Modules][DataBuffer] False") {
+ metadata = false;
+ value = true;
+ }
+
+ const int metadata_size = DataBuffer::get_bit_taken(DataBuffer::DATA_TYPE_BOOL, DataBuffer::COMPRESSION_LEVEL_0);
+ DataBuffer buffer;
+ buffer.begin_write(metadata_size);
+ buffer.add_bool(metadata);
+ buffer.add_bool(value);
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_bool() == metadata, "Should return correct metadata");
+ CHECK_MESSAGE(buffer.read_bool() == value, "Should return correct value after metadata");
+ CHECK_MESSAGE(buffer.get_metadata_size() == metadata_size, "Metadata size should be equal to expected");
+ CHECK_MESSAGE(buffer.size() == DataBuffer::get_bit_taken(DataBuffer::DATA_TYPE_BOOL, DataBuffer::COMPRESSION_LEVEL_0), "Size should be equal to expected");
+ CHECK_MESSAGE(buffer.total_size() == DataBuffer::get_bit_taken(DataBuffer::DATA_TYPE_BOOL, DataBuffer::COMPRESSION_LEVEL_0) + metadata_size, "Total size should be equal to expected");
+}
+
+TEST_CASE("[Modules][DataBuffer] Zero") {
+ constexpr DataBuffer::CompressionLevel compression = DataBuffer::COMPRESSION_LEVEL_0;
+ DataBuffer buffer;
+ buffer.begin_write(0);
+ buffer.add_int(-1, compression);
+ buffer.zero();
+ buffer.begin_read();
+ CHECK_MESSAGE(buffer.read_int(compression) == 0, "Should return 0");
+}
+
+TEST_CASE("[Modules][DataBuffer] Shrinking") {
+ DataBuffer buffer;
+ buffer.begin_write(0);
+ for (int i = 0; i < 2; ++i) {
+ buffer.add_real(3.14, DataBuffer::COMPRESSION_LEVEL_0);
+ }
+ const int original_size = buffer.total_size();
+
+ ERR_PRINT_OFF;
+ buffer.shrink_to(0, original_size + 1);
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(buffer.total_size() == original_size, "Shrinking to a larger size should fail.");
+
+ ERR_PRINT_OFF;
+ buffer.shrink_to(0, -1);
+ ERR_PRINT_ON;
+ CHECK_MESSAGE(buffer.total_size() == original_size, "Shrinking with a negative bits size should fail.");
+
+ buffer.shrink_to(0, original_size - 8);
+ CHECK_MESSAGE(buffer.total_size() == original_size - 8, "Shrinking by 1 byte should succeed.");
+ CHECK_MESSAGE(buffer.get_buffer().size_in_bits() == original_size, "Buffer size after shrinking by 1 byte should be the same.");
+
+ buffer.dry();
+ CHECK_MESSAGE(buffer.get_buffer().size_in_bits() == original_size - 8, "Buffer size after dry should changed to the smallest posiible.");
+}
+
+TEST_CASE("[Modules][DataBuffer] Skip") {
+ const bool value = true;
+
+ DataBuffer buffer;
+ buffer.add_bool(!value);
+ buffer.add_bool(value);
+
+ buffer.begin_read();
+ buffer.seek(DataBuffer::get_bit_taken(DataBuffer::DATA_TYPE_BOOL, DataBuffer::COMPRESSION_LEVEL_0));
+ CHECK_MESSAGE(buffer.read_bool() == value, "Should read the same value");
+}
+} // namespace TestDataBuffer
+
+#endif // TEST_DATA_BUFFER_H
diff --git a/modules/network_synchronizer/tests/test_interpolator.h b/modules/network_synchronizer/tests/test_interpolator.h
new file mode 100644
index 000000000000..031bd9765b7a
--- /dev/null
+++ b/modules/network_synchronizer/tests/test_interpolator.h
@@ -0,0 +1,131 @@
+/*************************************************************************/
+/* test_interpolator.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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 TEST_INTERPOLATOR_H
+#define TEST_INTERPOLATOR_H
+
+#include "modules/network_synchronizer/interpolator.h"
+
+#include "tests/test_macros.h"
+
+namespace TestInterpolator {
+
+template
+T generate_value(real_t value) {
+ if constexpr (std::is_same_v || std::is_same_v) {
+ return T(value, value);
+ } else if constexpr (std::is_same_v || std::is_same_v) {
+ return T(value, value, value);
+ } else {
+ return static_cast(value);
+ }
+}
+
+// TODO: Add other types
+TEST_CASE_TEMPLATE("[Modules][Interpolator] Interpolation", T, int, real_t, Vector2, Vector2i, Vector3) {
+ LocalVector fractions;
+ fractions.reserve(7);
+ fractions.push_back(0.0);
+ fractions.push_back(1.0);
+ fractions.push_back(0.5);
+ fractions.push_back(0.001);
+ fractions.push_back(0.999);
+ fractions.push_back(0.25);
+ fractions.push_back(0.75);
+
+ Map values;
+ values.insert(0.0, 1.0);
+ values.insert(-1.0, 1.0);
+ values.insert(0.0, -1.0);
+ values.insert(10, 15);
+
+ Interpolator interpolator;
+ for (const Map::Element *E = values.front(); E; E = E->next()) {
+ for (uint32_t j = 0; j < fractions.size(); ++j) {
+ // Skip custom interpolator for now
+ for (int k = Interpolator::FALLBACK_INTERPOLATE; k < Interpolator::FALLBACK_CUSTOM_INTERPOLATOR; ++k) {
+ const T first_value = generate_value(E->key());
+ const T second_value = generate_value(E->value());
+
+ interpolator.reset();
+ const int variable_id = interpolator.register_variable(T(), static_cast(k));
+ interpolator.terminate_init();
+ interpolator.begin_write(0);
+ interpolator.epoch_insert(variable_id, first_value);
+ interpolator.end_write();
+
+ interpolator.begin_write(1);
+ interpolator.epoch_insert(variable_id, second_value);
+ interpolator.end_write();
+
+ CAPTURE(k);
+ CAPTURE(fractions[j]);
+ CAPTURE(first_value);
+ CAPTURE(second_value);
+ const T result = interpolator.pop_epoch(0, fractions[j])[0];
+ switch (k) {
+ case Interpolator::FALLBACK_INTERPOLATE: {
+ CHECK(result == Interpolator::interpolate(first_value, second_value, fractions[j]).operator T());
+ } break;
+ case Interpolator::FALLBACK_DEFAULT: {
+ if (fractions[j] == 0.0) {
+ CHECK(result == first_value);
+ } else if (fractions[j] == 1.0) {
+ CHECK(result == second_value);
+ } else {
+ CHECK(result == T());
+ }
+ } break;
+ case Interpolator::FALLBACK_OLD_OR_NEAREST: {
+ if (fractions[j] == 0.0) {
+ CHECK(result == first_value);
+ } else if (fractions[j] == 1.0) {
+ CHECK(result == second_value);
+ } else {
+ CHECK(result == first_value);
+ }
+ } break;
+ case Interpolator::FALLBACK_NEW_OR_NEAREST: {
+ if (fractions[j] == 0.0) {
+ CHECK(result == first_value);
+ } else if (fractions[j] == 1.0) {
+ CHECK(result == second_value);
+ } else {
+ CHECK(result == second_value);
+ }
+ } break;
+ }
+ }
+ }
+ }
+}
+} // namespace TestInterpolator
+
+#endif // TEST_INTERPOLATOR_H