summaryrefslogtreecommitdiff
path: root/modules/multiplayer
diff options
context:
space:
mode:
authorFabio Alessandrelli <fabio.alessandrelli@gmail.com>2022-07-12 23:12:42 +0200
committerFabio Alessandrelli <fabio.alessandrelli@gmail.com>2022-07-26 09:31:12 +0200
commitca7d572908c58c587214b8f65bdd4078d0185ab2 (patch)
treeefd1507df5b3cd45e20b36bfc22818fcfadc5c85 /modules/multiplayer
parentc3dc887c410c5fa01eba197acba3afd8ce357ada (diff)
[Net] Modularize multiplayer, expose MultiplayerAPI to extensions.
- RPC configurations are now dictionaries. - Script.get_rpc_methods renamed to Script.get_rpc_config. - Node.rpc[_id] and Callable.rpc now return an Error. - Refactor MultiplayerAPI to allow extension. - New MultiplayerAPI.rpc method with Array argument (for scripts). - Move the default MultiplayerAPI implementation to a module.
Diffstat (limited to 'modules/multiplayer')
-rw-r--r--modules/multiplayer/SCsub14
-rw-r--r--modules/multiplayer/config.py19
-rw-r--r--modules/multiplayer/doc_classes/MultiplayerSpawner.xml66
-rw-r--r--modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml70
-rw-r--r--modules/multiplayer/doc_classes/SceneMultiplayer.xml55
-rw-r--r--modules/multiplayer/doc_classes/SceneReplicationConfig.xml67
-rw-r--r--modules/multiplayer/editor/replication_editor_plugin.cpp645
-rw-r--r--modules/multiplayer/editor/replication_editor_plugin.h148
-rw-r--r--modules/multiplayer/multiplayer_spawner.cpp302
-rw-r--r--modules/multiplayer/multiplayer_spawner.h118
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.cpp304
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.h96
-rw-r--r--modules/multiplayer/register_types.cpp60
-rw-r--r--modules/multiplayer/register_types.h39
-rw-r--r--modules/multiplayer/scene_cache_interface.cpp268
-rw-r--r--modules/multiplayer/scene_cache_interface.h82
-rw-r--r--modules/multiplayer/scene_multiplayer.cpp332
-rw-r--r--modules/multiplayer/scene_multiplayer.h136
-rw-r--r--modules/multiplayer/scene_replication_config.cpp205
-rw-r--r--modules/multiplayer/scene_replication_config.h91
-rw-r--r--modules/multiplayer/scene_replication_interface.cpp527
-rw-r--r--modules/multiplayer/scene_replication_interface.h86
-rw-r--r--modules/multiplayer/scene_replication_state.cpp267
-rw-r--r--modules/multiplayer/scene_replication_state.h135
-rw-r--r--modules/multiplayer/scene_rpc_interface.cpp521
-rw-r--r--modules/multiplayer/scene_rpc_interface.h102
26 files changed, 4755 insertions, 0 deletions
diff --git a/modules/multiplayer/SCsub b/modules/multiplayer/SCsub
new file mode 100644
index 0000000000..ff33655537
--- /dev/null
+++ b/modules/multiplayer/SCsub
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+Import("env")
+Import("env_modules")
+
+env_mp = env_modules.Clone()
+
+module_obj = []
+env_mp.add_source_files(module_obj, "*.cpp")
+
+if env["tools"]:
+ env_mp.add_source_files(module_obj, "editor/*.cpp")
+
+env.modules_sources += module_obj
diff --git a/modules/multiplayer/config.py b/modules/multiplayer/config.py
new file mode 100644
index 0000000000..414bf0afcf
--- /dev/null
+++ b/modules/multiplayer/config.py
@@ -0,0 +1,19 @@
+def can_build(env, platform):
+ return True
+
+
+def configure(env):
+ pass
+
+
+def get_doc_classes():
+ return [
+ "SceneReplicationConfig",
+ "SceneMultiplayer",
+ "MultiplayerSpawner",
+ "MultiplayerSynchronizer",
+ ]
+
+
+def get_doc_path():
+ return "doc_classes"
diff --git a/modules/multiplayer/doc_classes/MultiplayerSpawner.xml b/modules/multiplayer/doc_classes/MultiplayerSpawner.xml
new file mode 100644
index 0000000000..44ab34f52c
--- /dev/null
+++ b/modules/multiplayer/doc_classes/MultiplayerSpawner.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="MultiplayerSpawner" inherits="Node" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ </brief_description>
+ <description>
+ This node uses [method MultiplayerAPI.object_configuration_add] to notify spawns passing the spawned node as the [code]object[/code] and itself as the [code]configuration[/code], and [method MultiplayerAPI.object_configuration_remove] to notify despawns in a similar way.
+ </description>
+ <tutorials>
+ </tutorials>
+ <methods>
+ <method name="_spawn_custom" qualifiers="virtual">
+ <return type="Object" />
+ <argument index="0" name="data" type="Variant" />
+ <description>
+ </description>
+ </method>
+ <method name="add_spawnable_scene">
+ <return type="void" />
+ <argument index="0" name="path" type="String" />
+ <description>
+ </description>
+ </method>
+ <method name="clear_spawnable_scenes">
+ <return type="void" />
+ <description>
+ </description>
+ </method>
+ <method name="get_spawnable_scene" qualifiers="const">
+ <return type="String" />
+ <argument index="0" name="path" type="int" />
+ <description>
+ </description>
+ </method>
+ <method name="get_spawnable_scene_count" qualifiers="const">
+ <return type="int" />
+ <description>
+ </description>
+ </method>
+ <method name="spawn">
+ <return type="Node" />
+ <argument index="0" name="data" type="Variant" default="null" />
+ <description>
+ </description>
+ </method>
+ </methods>
+ <members>
+ <member name="spawn_limit" type="int" setter="set_spawn_limit" getter="get_spawn_limit" default="0">
+ </member>
+ <member name="spawn_path" type="NodePath" setter="set_spawn_path" getter="get_spawn_path" default="NodePath(&quot;&quot;)">
+ </member>
+ </members>
+ <signals>
+ <signal name="despawned">
+ <argument index="0" name="scene_id" type="int" />
+ <argument index="1" name="node" type="Node" />
+ <description>
+ </description>
+ </signal>
+ <signal name="spawned">
+ <argument index="0" name="scene_id" type="int" />
+ <argument index="1" name="node" type="Node" />
+ <description>
+ </description>
+ </signal>
+ </signals>
+</class>
diff --git a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
new file mode 100644
index 0000000000..ebd1b50201
--- /dev/null
+++ b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="MultiplayerSynchronizer" inherits="Node" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ </brief_description>
+ <description>
+ The [MultiplayerSynchronizer] uses [method MultiplayerAPI.object_configuration_add] to notify synchronization start passing the [Node] at [member root_path] as the [code]object[/code] and itself as the [code]configuration[/code], and uses [method MultiplayerAPI.object_configuration_remove] to notify synchronization end in a similar way.
+ </description>
+ <tutorials>
+ </tutorials>
+ <methods>
+ <method name="add_visibility_filter">
+ <return type="void" />
+ <argument index="0" name="filter" type="Callable" />
+ <description>
+ </description>
+ </method>
+ <method name="get_visibility_for" qualifiers="const">
+ <return type="bool" />
+ <argument index="0" name="peer" type="int" />
+ <description>
+ </description>
+ </method>
+ <method name="remove_visibility_filter">
+ <return type="void" />
+ <argument index="0" name="filter" type="Callable" />
+ <description>
+ </description>
+ </method>
+ <method name="set_visibility_for">
+ <return type="void" />
+ <argument index="0" name="peer" type="int" />
+ <argument index="1" name="visible" type="bool" />
+ <description>
+ </description>
+ </method>
+ <method name="update_visibility">
+ <return type="void" />
+ <argument index="0" name="for_peer" type="int" default="0" />
+ <description>
+ </description>
+ </method>
+ </methods>
+ <members>
+ <member name="public_visibility" type="bool" setter="set_visibility_public" getter="is_visibility_public" default="true">
+ </member>
+ <member name="replication_config" type="SceneReplicationConfig" setter="set_replication_config" getter="get_replication_config">
+ </member>
+ <member name="replication_interval" type="float" setter="set_replication_interval" getter="get_replication_interval" default="0.0">
+ </member>
+ <member name="root_path" type="NodePath" setter="set_root_path" getter="get_root_path" default="NodePath(&quot;..&quot;)">
+ </member>
+ <member name="visibility_update_mode" type="int" setter="set_visibility_update_mode" getter="get_visibility_update_mode" enum="MultiplayerSynchronizer.VisibilityUpdateMode" default="0">
+ </member>
+ </members>
+ <signals>
+ <signal name="visibility_changed">
+ <argument index="0" name="for_peer" type="int" />
+ <description>
+ </description>
+ </signal>
+ </signals>
+ <constants>
+ <constant name="VISIBILITY_PROCESS_IDLE" value="0" enum="VisibilityUpdateMode">
+ </constant>
+ <constant name="VISIBILITY_PROCESS_PHYSICS" value="1" enum="VisibilityUpdateMode">
+ </constant>
+ <constant name="VISIBILITY_PROCESS_NONE" value="2" enum="VisibilityUpdateMode">
+ </constant>
+ </constants>
+</class>
diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
new file mode 100644
index 0000000000..0c3ed2d784
--- /dev/null
+++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SceneMultiplayer" inherits="MultiplayerAPI" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ High-level multiplayer API implementation.
+ </brief_description>
+ <description>
+ This class is the default implementation of [MultiplayerAPI], used to provide multiplayer functionalities in Godot Engine.
+ This implementation supports RPCs via [method Node.rpc] and [method Node.rpc_id] and requires [method MultiplayerAPI.rpc] to be passed a [Node] (it will fail for other object types).
+ This implementation additionally provide [SceneTree] replication via the [MultiplayerSpawner] and [MultiplayerSynchronizer] nodes, and the [SceneReplicationConfig] resource.
+ [b]Note:[/b] The high-level multiplayer API protocol is an implementation detail and isn't meant to be used by non-Godot servers. It may change without notice.
+ [b]Note:[/b] When exporting to Android, make sure to enable the [code]INTERNET[/code] permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.
+ </description>
+ <tutorials>
+ </tutorials>
+ <methods>
+ <method name="clear">
+ <return type="void" />
+ <description>
+ Clears the current SceneMultiplayer network state (you shouldn't call this unless you know what you are doing).
+ </description>
+ </method>
+ <method name="send_bytes">
+ <return type="int" enum="Error" />
+ <argument index="0" name="bytes" type="PackedByteArray" />
+ <argument index="1" name="id" type="int" default="0" />
+ <argument index="2" name="mode" type="int" enum="MultiplayerPeer.TransferMode" default="2" />
+ <argument index="3" name="channel" type="int" default="0" />
+ <description>
+ Sends the given raw [code]bytes[/code] to a specific peer identified by [code]id[/code] (see [method MultiplayerPeer.set_target_peer]). Default ID is [code]0[/code], i.e. broadcast to all peers.
+ </description>
+ </method>
+ </methods>
+ <members>
+ <member name="allow_object_decoding" type="bool" setter="set_allow_object_decoding" getter="is_object_decoding_allowed" default="false">
+ If [code]true[/code], the MultiplayerAPI will allow encoding and decoding of object during RPCs.
+ [b]Warning:[/b] Deserialized objects can contain code which gets executed. Do not use this option if the serialized object comes from untrusted sources to avoid potential security threat such as remote code execution.
+ </member>
+ <member name="refuse_new_connections" type="bool" setter="set_refuse_new_connections" getter="is_refusing_new_connections" default="false">
+ If [code]true[/code], the MultiplayerAPI's [member MultiplayerAPI.multiplayer_peer] refuses new incoming connections.
+ </member>
+ <member name="root_path" type="NodePath" setter="set_root_path" getter="get_root_path" default="NodePath(&quot;&quot;)">
+ The root path to use for RPCs and replication. Instead of an absolute path, a relative path will be used to find the node upon which the RPC should be executed.
+ This effectively allows to have different branches of the scene tree to be managed by different MultiplayerAPI, allowing for example to run both client and server in the same scene.
+ </member>
+ </members>
+ <signals>
+ <signal name="peer_packet">
+ <argument index="0" name="id" type="int" />
+ <argument index="1" name="packet" type="PackedByteArray" />
+ <description>
+ Emitted when this MultiplayerAPI's [member MultiplayerAPI.multiplayer_peer] receives a [code]packet[/code] with custom data (see [method send_bytes]). ID is the peer ID of the peer that sent the packet.
+ </description>
+ </signal>
+ </signals>
+</class>
diff --git a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
new file mode 100644
index 0000000000..1d6dec2f92
--- /dev/null
+++ b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SceneReplicationConfig" inherits="Resource" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ </brief_description>
+ <description>
+ </description>
+ <tutorials>
+ </tutorials>
+ <methods>
+ <method name="add_property">
+ <return type="void" />
+ <argument index="0" name="path" type="NodePath" />
+ <argument index="1" name="index" type="int" default="-1" />
+ <description>
+ </description>
+ </method>
+ <method name="get_properties" qualifiers="const">
+ <return type="NodePath[]" />
+ <description>
+ </description>
+ </method>
+ <method name="has_property" qualifiers="const">
+ <return type="bool" />
+ <argument index="0" name="path" type="NodePath" />
+ <description>
+ </description>
+ </method>
+ <method name="property_get_index" qualifiers="const">
+ <return type="int" />
+ <argument index="0" name="path" type="NodePath" />
+ <description>
+ </description>
+ </method>
+ <method name="property_get_spawn">
+ <return type="bool" />
+ <argument index="0" name="path" type="NodePath" />
+ <description>
+ </description>
+ </method>
+ <method name="property_get_sync">
+ <return type="bool" />
+ <argument index="0" name="path" type="NodePath" />
+ <description>
+ </description>
+ </method>
+ <method name="property_set_spawn">
+ <return type="void" />
+ <argument index="0" name="path" type="NodePath" />
+ <argument index="1" name="enabled" type="bool" />
+ <description>
+ </description>
+ </method>
+ <method name="property_set_sync">
+ <return type="void" />
+ <argument index="0" name="path" type="NodePath" />
+ <argument index="1" name="enabled" type="bool" />
+ <description>
+ </description>
+ </method>
+ <method name="remove_property">
+ <return type="void" />
+ <argument index="0" name="path" type="NodePath" />
+ <description>
+ </description>
+ </method>
+ </methods>
+</class>
diff --git a/modules/multiplayer/editor/replication_editor_plugin.cpp b/modules/multiplayer/editor/replication_editor_plugin.cpp
new file mode 100644
index 0000000000..5891327db4
--- /dev/null
+++ b/modules/multiplayer/editor/replication_editor_plugin.cpp
@@ -0,0 +1,645 @@
+/*************************************************************************/
+/* replication_editor_plugin.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "replication_editor_plugin.h"
+
+#include "editor/editor_node.h"
+#include "editor/editor_scale.h"
+#include "editor/inspector_dock.h"
+#include "modules/multiplayer/multiplayer_synchronizer.h"
+#include "scene/gui/dialogs.h"
+#include "scene/gui/tree.h"
+
+void ReplicationEditor::_pick_node_filter_text_changed(const String &p_newtext) {
+ TreeItem *root_item = pick_node->get_scene_tree()->get_scene_tree()->get_root();
+
+ Vector<Node *> select_candidates;
+ Node *to_select = nullptr;
+
+ String filter = pick_node->get_filter_line_edit()->get_text();
+
+ _pick_node_select_recursive(root_item, filter, select_candidates);
+
+ if (!select_candidates.is_empty()) {
+ for (int i = 0; i < select_candidates.size(); ++i) {
+ Node *candidate = select_candidates[i];
+
+ if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) {
+ to_select = candidate;
+ break;
+ }
+ }
+
+ if (!to_select) {
+ to_select = select_candidates[0];
+ }
+ }
+
+ pick_node->get_scene_tree()->set_selected(to_select);
+}
+
+void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates) {
+ if (!p_item) {
+ return;
+ }
+
+ NodePath np = p_item->get_metadata(0);
+ Node *node = get_node(np);
+
+ if (!p_filter.is_empty() && ((String)node->get_name()).findn(p_filter) != -1) {
+ p_select_candidates.push_back(node);
+ }
+
+ TreeItem *c = p_item->get_first_child();
+
+ while (c) {
+ _pick_node_select_recursive(c, p_filter, p_select_candidates);
+ c = c->get_next();
+ }
+}
+
+void ReplicationEditor::_pick_node_filter_input(const Ref<InputEvent> &p_ie) {
+ Ref<InputEventKey> k = p_ie;
+
+ if (k.is_valid()) {
+ switch (k->get_keycode()) {
+ case Key::UP:
+ case Key::DOWN:
+ case Key::PAGEUP:
+ case Key::PAGEDOWN: {
+ pick_node->get_scene_tree()->get_scene_tree()->gui_input(k);
+ pick_node->get_filter_line_edit()->accept_event();
+ } break;
+ default:
+ break;
+ }
+ }
+}
+
+void ReplicationEditor::_pick_node_selected(NodePath p_path) {
+ Node *root = current->get_node(current->get_root_path());
+ ERR_FAIL_COND(!root);
+ Node *node = get_node(p_path);
+ ERR_FAIL_COND(!node);
+ NodePath path_to = root->get_path_to(node);
+ adding_node_path = path_to;
+ prop_selector->select_property_from_instance(node);
+}
+
+void ReplicationEditor::_pick_new_property() {
+ if (current == nullptr) {
+ EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it."));
+ return;
+ }
+ Node *root = current->get_node(current->get_root_path());
+ if (!root) {
+ EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root."));
+ return;
+ }
+ pick_node->popup_scenetree_dialog();
+ pick_node->get_filter_line_edit()->clear();
+ pick_node->get_filter_line_edit()->grab_focus();
+}
+
+void ReplicationEditor::_add_sync_property(String p_path) {
+ config = current->get_replication_config();
+
+ if (config.is_valid() && config->has_property(p_path)) {
+ EditorNode::get_singleton()->show_warning(TTR("Property is already being synchronized."));
+ return;
+ }
+
+ UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+ undo_redo->create_action(TTR("Add property to synchronizer"));
+
+ if (config.is_null()) {
+ config.instantiate();
+ current->set_replication_config(config);
+ undo_redo->add_do_method(current, "set_replication_config", config);
+ undo_redo->add_undo_method(current, "set_replication_config", Ref<SceneReplicationConfig>());
+ _update_config();
+ }
+
+ undo_redo->add_do_method(config.ptr(), "add_property", p_path);
+ undo_redo->add_undo_method(config.ptr(), "remove_property", p_path);
+ undo_redo->add_do_method(this, "_update_config");
+ undo_redo->add_undo_method(this, "_update_config");
+ undo_redo->commit_action();
+}
+
+void ReplicationEditor::_pick_node_property_selected(String p_name) {
+ String adding_prop_path = String(adding_node_path) + ":" + p_name;
+
+ _add_sync_property(adding_prop_path);
+}
+
+/// ReplicationEditor
+ReplicationEditor::ReplicationEditor() {
+ set_v_size_flags(SIZE_EXPAND_FILL);
+ set_custom_minimum_size(Size2(0, 200) * EDSCALE);
+
+ delete_dialog = memnew(ConfirmationDialog);
+ delete_dialog->connect("cancelled", callable_mp(this, &ReplicationEditor::_dialog_closed), varray(false));
+ delete_dialog->connect("confirmed", callable_mp(this, &ReplicationEditor::_dialog_closed), varray(true));
+ add_child(delete_dialog);
+
+ error_dialog = memnew(AcceptDialog);
+ error_dialog->set_ok_button_text(TTR("Close"));
+ error_dialog->set_title(TTR("Error!"));
+ add_child(error_dialog);
+
+ VBoxContainer *vb = memnew(VBoxContainer);
+ vb->set_v_size_flags(SIZE_EXPAND_FILL);
+ add_child(vb);
+
+ pick_node = memnew(SceneTreeDialog);
+ add_child(pick_node);
+ pick_node->register_text_enter(pick_node->get_filter_line_edit());
+ pick_node->set_title(TTR("Pick a node to synchronize:"));
+ pick_node->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_selected));
+ pick_node->get_filter_line_edit()->connect("text_changed", callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed));
+ pick_node->get_filter_line_edit()->connect("gui_input", callable_mp(this, &ReplicationEditor::_pick_node_filter_input));
+
+ prop_selector = memnew(PropertySelector);
+ add_child(prop_selector);
+ prop_selector->connect("selected", callable_mp(this, &ReplicationEditor::_pick_node_property_selected));
+
+ HBoxContainer *hb = memnew(HBoxContainer);
+ vb->add_child(hb);
+
+ add_pick_button = memnew(Button);
+ add_pick_button->connect("pressed", callable_mp(this, &ReplicationEditor::_pick_new_property));
+ add_pick_button->set_text(TTR("Add property to sync.."));
+ hb->add_child(add_pick_button);
+ VSeparator *vs = memnew(VSeparator);
+ vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0));
+ hb->add_child(vs);
+ hb->add_child(memnew(Label(TTR("Path:"))));
+ np_line_edit = memnew(LineEdit);
+ np_line_edit->set_placeholder(":property");
+ np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
+ hb->add_child(np_line_edit);
+ add_from_path_button = memnew(Button);
+ add_from_path_button->connect("pressed", callable_mp(this, &ReplicationEditor::_add_pressed));
+ add_from_path_button->set_text(TTR("Add from path"));
+ hb->add_child(add_from_path_button);
+ vs = memnew(VSeparator);
+ vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0));
+ hb->add_child(vs);
+ pin = memnew(Button);
+ pin->set_flat(true);
+ pin->set_toggle_mode(true);
+ hb->add_child(pin);
+
+ tree = memnew(Tree);
+ tree->set_hide_root(true);
+ tree->set_columns(4);
+ tree->set_column_titles_visible(true);
+ tree->set_column_title(0, TTR("Properties"));
+ tree->set_column_expand(0, true);
+ tree->set_column_title(1, TTR("Spawn"));
+ tree->set_column_expand(1, false);
+ tree->set_column_custom_minimum_width(1, 100);
+ tree->set_column_title(2, TTR("Sync"));
+ tree->set_column_custom_minimum_width(2, 100);
+ tree->set_column_expand(2, false);
+ tree->set_column_expand(3, false);
+ tree->create_item();
+ tree->connect("button_clicked", callable_mp(this, &ReplicationEditor::_tree_button_pressed));
+ tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited));
+ tree->set_v_size_flags(SIZE_EXPAND_FILL);
+ vb->add_child(tree);
+
+ drop_label = memnew(Label);
+ drop_label->set_text(TTR("Add properties using the buttons above or\ndrag them them from the inspector and drop them here."));
+ drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
+ tree->add_child(drop_label);
+ drop_label->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+
+ tree->set_drag_forwarding(this);
+}
+
+void ReplicationEditor::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_update_config"), &ReplicationEditor::_update_config);
+ ClassDB::bind_method(D_METHOD("_update_checked", "property", "column", "checked"), &ReplicationEditor::_update_checked);
+ ClassDB::bind_method("_can_drop_data_fw", &ReplicationEditor::_can_drop_data_fw);
+ ClassDB::bind_method("_drop_data_fw", &ReplicationEditor::_drop_data_fw);
+
+ ADD_SIGNAL(MethodInfo("keying_changed"));
+}
+
+bool ReplicationEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
+ Dictionary d = p_data;
+ if (!d.has("type")) {
+ return false;
+ }
+ String t = d["type"];
+ if (t != "obj_property") {
+ return false;
+ }
+ Object *obj = d["object"];
+ if (!obj) {
+ return false;
+ }
+ Node *node = Object::cast_to<Node>(obj);
+ if (!node) {
+ return false;
+ }
+
+ return true;
+}
+
+void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
+ if (current == nullptr) {
+ EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it."));
+ return;
+ }
+ Node *root = current->get_node(current->get_root_path());
+ if (!root) {
+ EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root."));
+ return;
+ }
+
+ Dictionary d = p_data;
+ if (!d.has("type")) {
+ return;
+ }
+ String t = d["type"];
+ if (t != "obj_property") {
+ return;
+ }
+ Object *obj = d["object"];
+ if (!obj) {
+ return;
+ }
+ Node *node = Object::cast_to<Node>(obj);
+ if (!node) {
+ return;
+ }
+
+ String path = root->get_path_to(node);
+ path += ":" + String(d["property"]);
+
+ _add_sync_property(path);
+}
+
+void ReplicationEditor::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE:
+ case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
+ add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox(SNAME("panel"), SNAME("Panel")));
+ add_pick_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
+ pin->set_icon(get_theme_icon(SNAME("Pin"), SNAME("EditorIcons")));
+ } break;
+
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ update_keying();
+ } break;
+ }
+}
+
+void ReplicationEditor::_add_pressed() {
+ if (!current) {
+ error_dialog->set_text(TTR("Please select a MultiplayerSynchronizer first."));
+ error_dialog->popup_centered();
+ return;
+ }
+ if (current->get_root_path().is_empty()) {
+ error_dialog->set_text(TTR("The MultiplayerSynchronizer needs a root path."));
+ error_dialog->popup_centered();
+ return;
+ }
+ String np_text = np_line_edit->get_text();
+ if (np_text.find(":") == -1) {
+ np_text = ":" + np_text;
+ }
+ NodePath prop = NodePath(np_text);
+ if (prop.is_empty()) {
+ return;
+ }
+ UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+ undo_redo->create_action(TTR("Add property"));
+ config = current->get_replication_config();
+ if (config.is_null()) {
+ config.instantiate();
+ current->set_replication_config(config);
+ undo_redo->add_do_method(current, "set_replication_config", config);
+ undo_redo->add_undo_method(current, "set_replication_config", Ref<SceneReplicationConfig>());
+ _update_config();
+ }
+ undo_redo->add_do_method(config.ptr(), "add_property", prop);
+ undo_redo->add_undo_method(config.ptr(), "remove_property", prop);
+ undo_redo->add_do_method(this, "_update_config");
+ undo_redo->add_undo_method(this, "_update_config");
+ undo_redo->commit_action();
+}
+
+void ReplicationEditor::_tree_item_edited() {
+ TreeItem *ti = tree->get_edited();
+ if (!ti || config.is_null()) {
+ return;
+ }
+ int column = tree->get_edited_column();
+ ERR_FAIL_COND(column < 1 || column > 2);
+ const NodePath prop = ti->get_metadata(0);
+ UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+ bool value = ti->is_checked(column);
+ String method;
+ if (column == 1) {
+ undo_redo->create_action(TTR("Set spawn property"));
+ method = "property_set_spawn";
+ } else {
+ undo_redo->create_action(TTR("Set sync property"));
+ method = "property_set_sync";
+ }
+ undo_redo->add_do_method(config.ptr(), method, prop, value);
+ undo_redo->add_undo_method(config.ptr(), method, prop, !value);
+ undo_redo->add_do_method(this, "_update_checked", prop, column, value);
+ undo_redo->add_undo_method(this, "_update_checked", prop, column, !value);
+ undo_redo->commit_action();
+}
+
+void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
+ if (p_button != MouseButton::LEFT) {
+ return;
+ }
+
+ TreeItem *ti = Object::cast_to<TreeItem>(p_item);
+ if (!ti) {
+ return;
+ }
+ deleting = ti->get_metadata(0);
+ delete_dialog->set_text(TTR("Delete Property?") + "\n\"" + ti->get_text(0) + "\"");
+ delete_dialog->popup_centered();
+}
+
+void ReplicationEditor::_dialog_closed(bool p_confirmed) {
+ if (deleting.is_empty() || config.is_null()) {
+ return;
+ }
+ if (p_confirmed) {
+ const NodePath prop = deleting;
+ int idx = config->property_get_index(prop);
+ bool spawn = config->property_get_spawn(prop);
+ bool sync = config->property_get_sync(prop);
+ UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+ undo_redo->create_action(TTR("Remove Property"));
+ undo_redo->add_do_method(config.ptr(), "remove_property", prop);
+ undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx);
+ undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn);
+ undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync);
+ undo_redo->add_do_method(this, "_update_config");
+ undo_redo->add_undo_method(this, "_update_config");
+ undo_redo->commit_action();
+ }
+ deleting = NodePath();
+}
+
+void ReplicationEditor::_update_checked(const NodePath &p_prop, int p_column, bool p_checked) {
+ if (!tree->get_root()) {
+ return;
+ }
+ TreeItem *ti = tree->get_root()->get_first_child();
+ while (ti) {
+ if (ti->get_metadata(0).operator NodePath() == p_prop) {
+ ti->set_checked(p_column, p_checked);
+ return;
+ }
+ ti = ti->get_next();
+ }
+}
+
+void ReplicationEditor::update_keying() {
+ /// TODO make keying usable.
+#if 0
+ bool keying_enabled = false;
+ EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
+ if (is_visible_in_tree() && config.is_valid() && editor_history->get_path_size() > 0) {
+ Object *obj = ObjectDB::get_instance(editor_history->get_path_object(0));
+ keying_enabled = Object::cast_to<Node>(obj) != nullptr;
+ }
+
+ if (keying_enabled == keying) {
+ return;
+ }
+
+ keying = keying_enabled;
+ emit_signal(SNAME("keying_changed"));
+#endif
+}
+
+void ReplicationEditor::_update_config() {
+ deleting = NodePath();
+ tree->clear();
+ tree->create_item();
+ drop_label->set_visible(true);
+ if (!config.is_valid()) {
+ update_keying();
+ return;
+ }
+ TypedArray<NodePath> props = config->get_properties();
+ if (props.size()) {
+ drop_label->set_visible(false);
+ }
+ for (int i = 0; i < props.size(); i++) {
+ const NodePath path = props[i];
+ _add_property(path, config->property_get_spawn(path), config->property_get_sync(path));
+ }
+ update_keying();
+}
+
+void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) {
+ if (current == p_sync) {
+ return;
+ }
+ current = p_sync;
+ if (current) {
+ config = current->get_replication_config();
+ } else {
+ config.unref();
+ }
+ _update_config();
+}
+
+Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) {
+ if (!p_node || !has_theme_icon(p_node->get_class(), "EditorIcons")) {
+ return get_theme_icon(SNAME("ImportFail"), SNAME("EditorIcons"));
+ }
+ return get_theme_icon(p_node->get_class(), "EditorIcons");
+}
+
+void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) {
+ String prop = String(p_property);
+ TreeItem *item = tree->create_item();
+ item->set_selectable(0, false);
+ item->set_selectable(1, false);
+ item->set_selectable(2, false);
+ item->set_selectable(3, false);
+ item->set_text(0, prop);
+ item->set_metadata(0, prop);
+ Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr;
+ Ref<Texture2D> icon = _get_class_icon(root_node);
+ if (root_node) {
+ String path = prop.substr(0, prop.find(":"));
+ String subpath = prop.substr(path.size());
+ Node *node = root_node->get_node_or_null(path);
+ if (!node) {
+ node = root_node;
+ }
+ item->set_text(0, String(node->get_name()) + ":" + subpath);
+ icon = _get_class_icon(node);
+ }
+ item->set_icon(0, icon);
+ item->add_button(3, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
+ item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER);
+ item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK);
+ item->set_checked(1, p_spawn);
+ item->set_editable(1, true);
+ item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER);
+ item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK);
+ item->set_checked(2, p_sync);
+ item->set_editable(2, true);
+}
+
+void ReplicationEditor::property_keyed(const String &p_property) {
+ ERR_FAIL_COND(!current || config.is_null());
+ Node *root = current->get_node(current->get_root_path());
+ ERR_FAIL_COND(!root);
+ EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();
+ ERR_FAIL_COND(history->get_path_size() == 0);
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(history->get_path_object(0)));
+ ERR_FAIL_COND(!node);
+ if (node->is_class("MultiplayerSynchronizer")) {
+ error_dialog->set_text(TTR("Properties of 'MultiplayerSynchronizer' cannot be configured for replication."));
+ error_dialog->popup_centered();
+ return;
+ }
+ if (history->get_path_size() > 1 || p_property.get_slice_count(":") > 1) {
+ error_dialog->set_text(TTR("Subresources cannot yet be configured for replication."));
+ error_dialog->popup_centered();
+ return;
+ }
+
+ String path = root->get_path_to(node);
+ for (int i = 1; i < history->get_path_size(); i++) {
+ String prop = history->get_path_property(i);
+ ERR_FAIL_COND(prop == "");
+ path += ":" + prop;
+ }
+ path += ":" + p_property;
+
+ NodePath prop = path;
+ UndoRedo *undo_redo = EditorNode::get_singleton()->get_undo_redo();
+ undo_redo->create_action(TTR("Add property"));
+ undo_redo->add_do_method(config.ptr(), "add_property", prop);
+ undo_redo->add_undo_method(config.ptr(), "remove_property", prop);
+ undo_redo->add_do_method(this, "_update_config");
+ undo_redo->add_undo_method(this, "_update_config");
+ undo_redo->commit_action();
+}
+
+/// ReplicationEditorPlugin
+ReplicationEditorPlugin::ReplicationEditorPlugin() {
+ repl_editor = memnew(ReplicationEditor);
+ button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor);
+ button->hide();
+ repl_editor->get_pin()->connect("pressed", callable_mp(this, &ReplicationEditorPlugin::_pinned));
+}
+
+ReplicationEditorPlugin::~ReplicationEditorPlugin() {
+}
+
+void ReplicationEditorPlugin::_keying_changed() {
+ // TODO make lock usable.
+ //InspectorDock::get_inspector_singleton()->set_keying(repl_editor->has_keying(), this);
+}
+
+void ReplicationEditorPlugin::_property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance) {
+ if (!repl_editor->has_keying()) {
+ return;
+ }
+ repl_editor->property_keyed(p_keyed);
+}
+
+void ReplicationEditorPlugin::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ //Node3DEditor::get_singleton()->connect("transform_key_request", callable_mp(this, &AnimationPlayerEditorPlugin::_transform_key_request));
+ InspectorDock::get_inspector_singleton()->connect("property_keyed", callable_mp(this, &ReplicationEditorPlugin::_property_keyed));
+ repl_editor->connect("keying_changed", callable_mp(this, &ReplicationEditorPlugin::_keying_changed));
+ // TODO make lock usable.
+ //InspectorDock::get_inspector_singleton()->connect("object_inspected", callable_mp(repl_editor, &ReplicationEditor::update_keying));
+ get_tree()->connect("node_removed", callable_mp(this, &ReplicationEditorPlugin::_node_removed));
+ } break;
+ }
+}
+
+void ReplicationEditorPlugin::_node_removed(Node *p_node) {
+ if (p_node && p_node == repl_editor->get_current()) {
+ repl_editor->edit(nullptr);
+ if (repl_editor->is_visible_in_tree()) {
+ EditorNode::get_singleton()->hide_bottom_panel();
+ }
+ button->hide();
+ repl_editor->get_pin()->set_pressed(false);
+ }
+}
+
+void ReplicationEditorPlugin::_pinned() {
+ if (!repl_editor->get_pin()->is_pressed()) {
+ if (repl_editor->is_visible_in_tree()) {
+ EditorNode::get_singleton()->hide_bottom_panel();
+ }
+ button->hide();
+ }
+}
+
+void ReplicationEditorPlugin::edit(Object *p_object) {
+ repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object));
+}
+
+bool ReplicationEditorPlugin::handles(Object *p_object) const {
+ return p_object->is_class("MultiplayerSynchronizer");
+}
+
+void ReplicationEditorPlugin::make_visible(bool p_visible) {
+ if (p_visible) {
+ //editor->hide_animation_player_editors();
+ //editor->animation_panel_make_visible(true);
+ button->show();
+ EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor);
+ } else if (!repl_editor->get_pin()->is_pressed()) {
+ if (repl_editor->is_visible_in_tree()) {
+ EditorNode::get_singleton()->hide_bottom_panel();
+ }
+ button->hide();
+ }
+}
diff --git a/modules/multiplayer/editor/replication_editor_plugin.h b/modules/multiplayer/editor/replication_editor_plugin.h
new file mode 100644
index 0000000000..57fa4c82fa
--- /dev/null
+++ b/modules/multiplayer/editor/replication_editor_plugin.h
@@ -0,0 +1,148 @@
+/*************************************************************************/
+/* replication_editor_plugin.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef REPLICATION_EDITOR_PLUGIN_H
+#define REPLICATION_EDITOR_PLUGIN_H
+
+#include "editor/editor_plugin.h"
+
+#include "editor/editor_spin_slider.h"
+#include "editor/property_editor.h"
+#include "editor/property_selector.h"
+
+#include "../scene_replication_config.h"
+
+class ConfirmationDialog;
+class MultiplayerSynchronizer;
+class Tree;
+
+class ReplicationEditor : public VBoxContainer {
+ GDCLASS(ReplicationEditor, VBoxContainer);
+
+private:
+ MultiplayerSynchronizer *current = nullptr;
+
+ AcceptDialog *error_dialog = nullptr;
+ ConfirmationDialog *delete_dialog = nullptr;
+ Button *add_pick_button = nullptr;
+ Button *add_from_path_button = nullptr;
+ LineEdit *np_line_edit = nullptr;
+
+ Label *drop_label = nullptr;
+
+ Ref<SceneReplicationConfig> config;
+ NodePath deleting;
+ Tree *tree = nullptr;
+ bool keying = false;
+
+ PropertySelector *prop_selector = nullptr;
+ SceneTreeDialog *pick_node = nullptr;
+ NodePath adding_node_path;
+
+ Button *pin = nullptr;
+
+ Ref<Texture2D> _get_class_icon(const Node *p_node);
+
+ void _add_pressed();
+ void _tree_item_edited();
+ void _tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
+ void _update_checked(const NodePath &p_prop, int p_column, bool p_checked);
+ void _update_config();
+ void _dialog_closed(bool p_confirmed);
+ void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true);
+
+ void _pick_node_filter_text_changed(const String &p_newtext);
+ void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates);
+ void _pick_node_filter_input(const Ref<InputEvent> &p_ie);
+ void _pick_node_selected(NodePath p_path);
+
+ void _pick_new_property();
+ void _pick_node_property_selected(String p_name);
+
+ bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
+ void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from);
+
+ void _add_sync_property(String p_path);
+
+protected:
+ static void _bind_methods();
+
+ void _notification(int p_what);
+
+public:
+ void update_keying();
+ void edit(MultiplayerSynchronizer *p_object);
+ bool has_keying() const { return keying; }
+ MultiplayerSynchronizer *get_current() const { return current; }
+ void property_keyed(const String &p_property);
+
+ Button *get_pin() { return pin; }
+ ReplicationEditor();
+ ~ReplicationEditor() {}
+};
+
+class ReplicationEditorPlugin : public EditorPlugin {
+ GDCLASS(ReplicationEditorPlugin, EditorPlugin);
+
+private:
+ Button *button = nullptr;
+ ReplicationEditor *repl_editor = nullptr;
+
+ void _node_removed(Node *p_node);
+ void _keying_changed();
+ void _property_keyed(const String &p_keyed, const Variant &p_value, bool p_advance);
+
+ void _pinned();
+
+protected:
+ void _notification(int p_what);
+
+public:
+ virtual void edit(Object *p_object) override;
+ virtual bool handles(Object *p_object) const override;
+ virtual void make_visible(bool p_visible) override;
+
+ ReplicationEditorPlugin();
+ ~ReplicationEditorPlugin();
+};
+#else
+class ReplicationEditorPlugin : public EditorPlugin {
+ GDCLASS(ReplicationEditorPlugin, EditorPlugin);
+
+public:
+ virtual void edit(Object *p_object) override {}
+ virtual bool handles(Object *p_object) const override { return false; }
+ virtual void make_visible(bool p_visible) override {}
+
+ ReplicationEditorPlugin() {}
+ ~ReplicationEditorPlugin() {}
+};
+
+#endif // REPLICATION_EDITOR_PLUGIN_H
diff --git a/modules/multiplayer/multiplayer_spawner.cpp b/modules/multiplayer/multiplayer_spawner.cpp
new file mode 100644
index 0000000000..ca2fd1c023
--- /dev/null
+++ b/modules/multiplayer/multiplayer_spawner.cpp
@@ -0,0 +1,302 @@
+/*************************************************************************/
+/* multiplayer_spawner.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "multiplayer_spawner.h"
+
+#include "core/io/marshalls.h"
+#include "scene/main/multiplayer_api.h"
+#include "scene/main/window.h"
+#include "scene/scene_string_names.h"
+
+#ifdef TOOLS_ENABLED
+/* This is editor only */
+bool MultiplayerSpawner::_set(const StringName &p_name, const Variant &p_value) {
+ if (p_name == "_spawnable_scene_count") {
+ spawnable_scenes.resize(p_value);
+ notify_property_list_changed();
+ return true;
+ } else {
+ String ns = p_name;
+ if (ns.begins_with("scenes/")) {
+ uint32_t index = ns.get_slicec('/', 1).to_int();
+ ERR_FAIL_UNSIGNED_INDEX_V(index, spawnable_scenes.size(), false);
+ spawnable_scenes[index].path = p_value;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MultiplayerSpawner::_get(const StringName &p_name, Variant &r_ret) const {
+ if (p_name == "_spawnable_scene_count") {
+ r_ret = spawnable_scenes.size();
+ return true;
+ } else {
+ String ns = p_name;
+ if (ns.begins_with("scenes/")) {
+ uint32_t index = ns.get_slicec('/', 1).to_int();
+ ERR_FAIL_UNSIGNED_INDEX_V(index, spawnable_scenes.size(), false);
+ r_ret = spawnable_scenes[index].path;
+ return true;
+ }
+ }
+ return false;
+}
+
+void MultiplayerSpawner::_get_property_list(List<PropertyInfo> *p_list) const {
+ p_list->push_back(PropertyInfo(Variant::INT, "_spawnable_scene_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_ARRAY, "Auto Spawn List,scenes/"));
+ List<String> exts;
+ ResourceLoader::get_recognized_extensions_for_type("PackedScene", &exts);
+ String ext_hint;
+ for (const String &E : exts) {
+ if (!ext_hint.is_empty()) {
+ ext_hint += ",";
+ }
+ ext_hint += "*." + E;
+ }
+ for (uint32_t i = 0; i < spawnable_scenes.size(); i++) {
+ p_list->push_back(PropertyInfo(Variant::STRING, "scenes/" + itos(i), PROPERTY_HINT_FILE, ext_hint, PROPERTY_USAGE_EDITOR));
+ }
+}
+#endif
+void MultiplayerSpawner::add_spawnable_scene(const String &p_path) {
+ SpawnableScene sc;
+ sc.path = p_path;
+ if (Engine::get_singleton()->is_editor_hint()) {
+ ERR_FAIL_COND(!FileAccess::exists(p_path));
+ } else {
+ sc.cache = ResourceLoader::load(p_path);
+ ERR_FAIL_COND_MSG(sc.cache.is_null(), "Invalid spawnable scene: " + p_path);
+ }
+ spawnable_scenes.push_back(sc);
+}
+int MultiplayerSpawner::get_spawnable_scene_count() const {
+ return spawnable_scenes.size();
+}
+String MultiplayerSpawner::get_spawnable_scene(int p_idx) const {
+ return spawnable_scenes[p_idx].path;
+}
+void MultiplayerSpawner::clear_spawnable_scenes() {
+ spawnable_scenes.clear();
+}
+
+Vector<String> MultiplayerSpawner::_get_spawnable_scenes() const {
+ Vector<String> ss;
+ ss.resize(spawnable_scenes.size());
+ for (int i = 0; i < ss.size(); i++) {
+ ss.write[i] = spawnable_scenes[i].path;
+ }
+ return ss;
+}
+
+void MultiplayerSpawner::_set_spawnable_scenes(const Vector<String> &p_scenes) {
+ clear_spawnable_scenes();
+ for (int i = 0; i < p_scenes.size(); i++) {
+ add_spawnable_scene(p_scenes[i]);
+ }
+}
+
+void MultiplayerSpawner::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("add_spawnable_scene", "path"), &MultiplayerSpawner::add_spawnable_scene);
+ ClassDB::bind_method(D_METHOD("get_spawnable_scene_count"), &MultiplayerSpawner::get_spawnable_scene_count);
+ ClassDB::bind_method(D_METHOD("get_spawnable_scene", "path"), &MultiplayerSpawner::get_spawnable_scene);
+ ClassDB::bind_method(D_METHOD("clear_spawnable_scenes"), &MultiplayerSpawner::clear_spawnable_scenes);
+
+ ClassDB::bind_method(D_METHOD("_get_spawnable_scenes"), &MultiplayerSpawner::_get_spawnable_scenes);
+ ClassDB::bind_method(D_METHOD("_set_spawnable_scenes", "scenes"), &MultiplayerSpawner::_set_spawnable_scenes);
+
+ ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_spawnable_scenes", PROPERTY_HINT_NONE, "", (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL)), "_set_spawnable_scenes", "_get_spawnable_scenes");
+
+ ClassDB::bind_method(D_METHOD("spawn", "data"), &MultiplayerSpawner::spawn, DEFVAL(Variant()));
+
+ ClassDB::bind_method(D_METHOD("get_spawn_path"), &MultiplayerSpawner::get_spawn_path);
+ ClassDB::bind_method(D_METHOD("set_spawn_path", "path"), &MultiplayerSpawner::set_spawn_path);
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "spawn_path", PROPERTY_HINT_NONE, ""), "set_spawn_path", "get_spawn_path");
+
+ ClassDB::bind_method(D_METHOD("get_spawn_limit"), &MultiplayerSpawner::get_spawn_limit);
+ ClassDB::bind_method(D_METHOD("set_spawn_limit", "limit"), &MultiplayerSpawner::set_spawn_limit);
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "spawn_limit", PROPERTY_HINT_RANGE, "0,1024,1,or_greater"), "set_spawn_limit", "get_spawn_limit");
+
+ GDVIRTUAL_BIND(_spawn_custom, "data");
+
+ ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
+ ADD_SIGNAL(MethodInfo("spawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
+}
+
+void MultiplayerSpawner::_update_spawn_node() {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ if (spawn_node.is_valid()) {
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(spawn_node));
+ if (node && node->is_connected("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added))) {
+ node->disconnect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added));
+ }
+ }
+ Node *node = spawn_path.is_empty() && is_inside_tree() ? nullptr : get_node_or_null(spawn_path);
+ if (node) {
+ spawn_node = node->get_instance_id();
+ if (get_spawnable_scene_count() && !GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom)) {
+ node->connect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added));
+ }
+ } else {
+ spawn_node = ObjectID();
+ }
+}
+
+void MultiplayerSpawner::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_POST_ENTER_TREE: {
+ _update_spawn_node();
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ _update_spawn_node();
+
+ for (const KeyValue<ObjectID, SpawnInfo> &E : tracked_nodes) {
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(E.key));
+ ERR_CONTINUE(!node);
+ node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit));
+ // This is unlikely, but might still crash the engine.
+ if (node->is_connected(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready))) {
+ node->disconnect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready));
+ }
+ get_multiplayer()->object_configuration_remove(node, this);
+ }
+ tracked_nodes.clear();
+ } break;
+ }
+}
+
+void MultiplayerSpawner::_node_added(Node *p_node) {
+ if (!get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority()) {
+ return;
+ }
+ if (tracked_nodes.has(p_node->get_instance_id())) {
+ return;
+ }
+ const Node *parent = get_spawn_node();
+ if (!parent || p_node->get_parent() != parent) {
+ return;
+ }
+ int id = find_spawnable_scene_index_from_path(p_node->get_scene_file_path());
+ if (id == INVALID_ID) {
+ return;
+ }
+ const String name = p_node->get_name();
+ ERR_FAIL_COND_MSG(name.validate_node_name() != name, vformat("Unable to auto-spawn node with reserved name: %s. Make sure to add your replicated scenes via 'add_child(node, true)' to produce valid names.", name));
+ _track(p_node, Variant(), id);
+}
+
+NodePath MultiplayerSpawner::get_spawn_path() const {
+ return spawn_path;
+}
+
+void MultiplayerSpawner::set_spawn_path(const NodePath &p_path) {
+ spawn_path = p_path;
+ _update_spawn_node();
+}
+
+void MultiplayerSpawner::_track(Node *p_node, const Variant &p_argument, int p_scene_id) {
+ ObjectID oid = p_node->get_instance_id();
+ if (!tracked_nodes.has(oid)) {
+ tracked_nodes[oid] = SpawnInfo(p_argument.duplicate(true), p_scene_id);
+ p_node->connect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &MultiplayerSpawner::_node_exit), varray(p_node->get_instance_id()), CONNECT_ONESHOT);
+ p_node->connect(SceneStringNames::get_singleton()->ready, callable_mp(this, &MultiplayerSpawner::_node_ready), varray(p_node->get_instance_id()), CONNECT_ONESHOT);
+ }
+}
+
+void MultiplayerSpawner::_node_ready(ObjectID p_id) {
+ get_multiplayer()->object_configuration_add(ObjectDB::get_instance(p_id), this);
+}
+
+void MultiplayerSpawner::_node_exit(ObjectID p_id) {
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id));
+ ERR_FAIL_COND(!node);
+ if (tracked_nodes.has(p_id)) {
+ tracked_nodes.erase(p_id);
+ get_multiplayer()->object_configuration_remove(node, this);
+ }
+}
+
+int MultiplayerSpawner::find_spawnable_scene_index_from_path(const String &p_scene) const {
+ for (uint32_t i = 0; i < spawnable_scenes.size(); i++) {
+ if (spawnable_scenes[i].path == p_scene) {
+ return i;
+ }
+ }
+ return INVALID_ID;
+}
+
+int MultiplayerSpawner::find_spawnable_scene_index_from_object(const ObjectID &p_id) const {
+ const SpawnInfo *info = tracked_nodes.getptr(p_id);
+ return info ? info->id : INVALID_ID;
+}
+
+const Variant MultiplayerSpawner::get_spawn_argument(const ObjectID &p_id) const {
+ const SpawnInfo *info = tracked_nodes.getptr(p_id);
+ return info ? info->args : Variant();
+}
+
+Node *MultiplayerSpawner::instantiate_scene(int p_id) {
+ ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!");
+ ERR_FAIL_UNSIGNED_INDEX_V((uint32_t)p_id, spawnable_scenes.size(), nullptr);
+ Ref<PackedScene> scene = spawnable_scenes[p_id].cache;
+ ERR_FAIL_COND_V(scene.is_null(), nullptr);
+ return scene->instantiate();
+}
+
+Node *MultiplayerSpawner::instantiate_custom(const Variant &p_data) {
+ ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!");
+ Object *obj = nullptr;
+ Node *node = nullptr;
+ if (GDVIRTUAL_CALL(_spawn_custom, p_data, obj)) {
+ node = Object::cast_to<Node>(obj);
+ }
+ return node;
+}
+
+Node *MultiplayerSpawner::spawn(const Variant &p_data) {
+ ERR_FAIL_COND_V(!is_inside_tree() || !get_multiplayer()->has_multiplayer_peer() || !is_multiplayer_authority(), nullptr);
+ ERR_FAIL_COND_V_MSG(spawn_limit && spawn_limit <= tracked_nodes.size(), nullptr, "Spawn limit reached!");
+ ERR_FAIL_COND_V_MSG(!GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom), nullptr, "Custom spawn requires the '_spawn_custom' virtual method to be implemented via script.");
+
+ Node *parent = get_spawn_node();
+ ERR_FAIL_COND_V_MSG(!parent, nullptr, "Cannot find spawn node.");
+
+ Node *node = instantiate_custom(p_data);
+ ERR_FAIL_COND_V_MSG(!node, nullptr, "The '_spawn_custom' implementation must return a valid Node.");
+
+ _track(node, p_data);
+ parent->add_child(node, true);
+ return node;
+}
diff --git a/modules/multiplayer/multiplayer_spawner.h b/modules/multiplayer/multiplayer_spawner.h
new file mode 100644
index 0000000000..80bb878a74
--- /dev/null
+++ b/modules/multiplayer/multiplayer_spawner.h
@@ -0,0 +1,118 @@
+/*************************************************************************/
+/* multiplayer_spawner.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef MULTIPLAYER_SPAWNER_H
+#define MULTIPLAYER_SPAWNER_H
+
+#include "scene/main/node.h"
+
+#include "core/templates/local_vector.h"
+#include "core/variant/typed_array.h"
+#include "scene/resources/packed_scene.h"
+
+#include "scene_replication_config.h"
+
+class MultiplayerSpawner : public Node {
+ GDCLASS(MultiplayerSpawner, Node);
+
+public:
+ enum {
+ INVALID_ID = 0xFF,
+ };
+
+private:
+ struct SpawnableScene {
+ String path;
+ Ref<PackedScene> cache;
+ };
+
+ LocalVector<SpawnableScene> spawnable_scenes;
+
+ HashSet<ResourceUID::ID> spawnable_ids;
+ NodePath spawn_path;
+
+ struct SpawnInfo {
+ Variant args;
+ int id = INVALID_ID;
+ SpawnInfo(Variant p_args, int p_id) {
+ id = p_id;
+ args = p_args;
+ }
+ SpawnInfo() {}
+ };
+
+ ObjectID spawn_node;
+ HashMap<ObjectID, SpawnInfo> tracked_nodes;
+ uint32_t spawn_limit = 0;
+
+ void _update_spawn_node();
+ void _track(Node *p_node, const Variant &p_argument, int p_scene_id = INVALID_ID);
+ void _node_added(Node *p_node);
+ void _node_exit(ObjectID p_id);
+ void _node_ready(ObjectID p_id);
+
+ Vector<String> _get_spawnable_scenes() const;
+ void _set_spawnable_scenes(const Vector<String> &p_scenes);
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_what);
+
+#ifdef TOOLS_ENABLED
+ bool _set(const StringName &p_name, const Variant &p_value);
+ bool _get(const StringName &p_name, Variant &r_ret) const;
+ void _get_property_list(List<PropertyInfo> *p_list) const;
+#endif
+public:
+ Node *get_spawn_node() const { return spawn_node.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)) : nullptr; }
+
+ void add_spawnable_scene(const String &p_path);
+ int get_spawnable_scene_count() const;
+ String get_spawnable_scene(int p_idx) const;
+ void clear_spawnable_scenes();
+
+ NodePath get_spawn_path() const;
+ void set_spawn_path(const NodePath &p_path);
+ uint32_t get_spawn_limit() const { return spawn_limit; }
+ void set_spawn_limit(uint32_t p_limit) { spawn_limit = p_limit; }
+
+ const Variant get_spawn_argument(const ObjectID &p_id) const;
+ int find_spawnable_scene_index_from_object(const ObjectID &p_id) const;
+ int find_spawnable_scene_index_from_path(const String &p_path) const;
+ Node *spawn(const Variant &p_data = Variant());
+ Node *instantiate_custom(const Variant &p_data);
+ Node *instantiate_scene(int p_idx);
+
+ GDVIRTUAL1R(Object *, _spawn_custom, const Variant &);
+
+ MultiplayerSpawner() {}
+};
+
+#endif // MULTIPLAYER_SPAWNER_H
diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp
new file mode 100644
index 0000000000..621d62c4c7
--- /dev/null
+++ b/modules/multiplayer/multiplayer_synchronizer.cpp
@@ -0,0 +1,304 @@
+/*************************************************************************/
+/* multiplayer_synchronizer.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "multiplayer_synchronizer.h"
+
+#include "core/config/engine.h"
+#include "scene/main/multiplayer_api.h"
+
+Object *MultiplayerSynchronizer::_get_prop_target(Object *p_obj, const NodePath &p_path) {
+ if (p_path.get_name_count() == 0) {
+ return p_obj;
+ }
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V_MSG(!node || !node->has_node(p_path), nullptr, vformat("Node '%s' not found.", p_path));
+ return node->get_node(p_path);
+}
+
+void MultiplayerSynchronizer::_stop() {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr;
+ if (node) {
+ get_multiplayer()->object_configuration_remove(node, this);
+ }
+}
+
+void MultiplayerSynchronizer::_start() {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr;
+ if (node) {
+ get_multiplayer()->object_configuration_add(node, this);
+ _update_process();
+ }
+}
+
+void MultiplayerSynchronizer::_update_process() {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr;
+ if (!node) {
+ return;
+ }
+ set_process_internal(false);
+ set_physics_process_internal(false);
+ if (!visibility_filters.size()) {
+ return;
+ }
+ switch (visibility_update_mode) {
+ case VISIBILITY_PROCESS_IDLE:
+ set_process_internal(true);
+ break;
+ case VISIBILITY_PROCESS_PHYSICS:
+ set_physics_process_internal(true);
+ break;
+ case VISIBILITY_PROCESS_NONE:
+ break;
+ }
+}
+
+Error MultiplayerSynchronizer::get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs) {
+ ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER);
+ r_variant.resize(p_properties.size());
+ r_variant_ptrs.resize(r_variant.size());
+ int i = 0;
+ for (const NodePath &prop : p_properties) {
+ bool valid = false;
+ const Object *obj = _get_prop_target(p_obj, prop);
+ ERR_FAIL_COND_V(!obj, FAILED);
+ r_variant.write[i] = obj->get(prop.get_concatenated_subnames(), &valid);
+ r_variant_ptrs.write[i] = &r_variant[i];
+ ERR_FAIL_COND_V_MSG(!valid, ERR_INVALID_DATA, vformat("Property '%s' not found.", prop));
+ i++;
+ }
+ return OK;
+}
+
+Error MultiplayerSynchronizer::set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state) {
+ ERR_FAIL_COND_V(!p_obj, ERR_INVALID_PARAMETER);
+ int i = 0;
+ for (const NodePath &prop : p_properties) {
+ Object *obj = _get_prop_target(p_obj, prop);
+ ERR_FAIL_COND_V(!obj, FAILED);
+ obj->set(prop.get_concatenated_subnames(), p_state[i]);
+ i += 1;
+ }
+ return OK;
+}
+
+bool MultiplayerSynchronizer::is_visibility_public() const {
+ return peer_visibility.has(0);
+}
+
+void MultiplayerSynchronizer::set_visibility_public(bool p_visible) {
+ set_visibility_for(0, p_visible);
+}
+
+bool MultiplayerSynchronizer::is_visible_to(int p_peer) {
+ if (visibility_filters.size()) {
+ Variant arg = p_peer;
+ const Variant *argv[1] = { &arg };
+ for (Callable filter : visibility_filters) {
+ Variant ret;
+ Callable::CallError err;
+ filter.call(argv, 1, ret, err);
+ ERR_FAIL_COND_V(err.error != Callable::CallError::CALL_OK || ret.get_type() != Variant::BOOL, false);
+ if (!ret.operator bool()) {
+ return false;
+ }
+ }
+ }
+ return peer_visibility.has(0) || peer_visibility.has(p_peer);
+}
+
+void MultiplayerSynchronizer::add_visibility_filter(Callable p_callback) {
+ visibility_filters.insert(p_callback);
+ _update_process();
+}
+
+void MultiplayerSynchronizer::remove_visibility_filter(Callable p_callback) {
+ visibility_filters.erase(p_callback);
+ _update_process();
+}
+
+void MultiplayerSynchronizer::set_visibility_for(int p_peer, bool p_visible) {
+ if (peer_visibility.has(p_peer) == p_visible) {
+ return;
+ }
+ if (p_visible) {
+ peer_visibility.insert(p_peer);
+ } else {
+ peer_visibility.erase(p_peer);
+ }
+ update_visibility(p_peer);
+}
+
+bool MultiplayerSynchronizer::get_visibility_for(int p_peer) const {
+ return peer_visibility.has(p_peer);
+}
+
+void MultiplayerSynchronizer::set_visibility_update_mode(VisibilityUpdateMode p_mode) {
+ visibility_update_mode = p_mode;
+ _update_process();
+}
+
+MultiplayerSynchronizer::VisibilityUpdateMode MultiplayerSynchronizer::get_visibility_update_mode() const {
+ return visibility_update_mode;
+}
+
+void MultiplayerSynchronizer::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_root_path", "path"), &MultiplayerSynchronizer::set_root_path);
+ ClassDB::bind_method(D_METHOD("get_root_path"), &MultiplayerSynchronizer::get_root_path);
+
+ ClassDB::bind_method(D_METHOD("set_replication_interval", "milliseconds"), &MultiplayerSynchronizer::set_replication_interval);
+ ClassDB::bind_method(D_METHOD("get_replication_interval"), &MultiplayerSynchronizer::get_replication_interval);
+
+ ClassDB::bind_method(D_METHOD("set_replication_config", "config"), &MultiplayerSynchronizer::set_replication_config);
+ ClassDB::bind_method(D_METHOD("get_replication_config"), &MultiplayerSynchronizer::get_replication_config);
+
+ ClassDB::bind_method(D_METHOD("set_visibility_update_mode", "mode"), &MultiplayerSynchronizer::set_visibility_update_mode);
+ ClassDB::bind_method(D_METHOD("get_visibility_update_mode"), &MultiplayerSynchronizer::get_visibility_update_mode);
+ ClassDB::bind_method(D_METHOD("update_visibility", "for_peer"), &MultiplayerSynchronizer::update_visibility, DEFVAL(0));
+
+ ClassDB::bind_method(D_METHOD("set_visibility_public", "visible"), &MultiplayerSynchronizer::set_visibility_public);
+ ClassDB::bind_method(D_METHOD("is_visibility_public"), &MultiplayerSynchronizer::is_visibility_public);
+
+ ClassDB::bind_method(D_METHOD("add_visibility_filter", "filter"), &MultiplayerSynchronizer::add_visibility_filter);
+ ClassDB::bind_method(D_METHOD("remove_visibility_filter", "filter"), &MultiplayerSynchronizer::remove_visibility_filter);
+ ClassDB::bind_method(D_METHOD("set_visibility_for", "peer", "visible"), &MultiplayerSynchronizer::set_visibility_for);
+ ClassDB::bind_method(D_METHOD("get_visibility_for", "peer"), &MultiplayerSynchronizer::get_visibility_for);
+
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_replication_interval", "get_replication_interval");
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replication_config", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig", PROPERTY_USAGE_NO_EDITOR), "set_replication_config", "get_replication_config");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_update_mode", PROPERTY_HINT_ENUM, "Idle,Physics,None"), "set_visibility_update_mode", "get_visibility_update_mode");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "public_visibility"), "set_visibility_public", "is_visibility_public");
+
+ BIND_ENUM_CONSTANT(VISIBILITY_PROCESS_IDLE);
+ BIND_ENUM_CONSTANT(VISIBILITY_PROCESS_PHYSICS);
+ BIND_ENUM_CONSTANT(VISIBILITY_PROCESS_NONE);
+
+ ADD_SIGNAL(MethodInfo("visibility_changed", PropertyInfo(Variant::INT, "for_peer")));
+}
+
+void MultiplayerSynchronizer::_notification(int p_what) {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ if (root_path.is_empty()) {
+ return;
+ }
+
+ switch (p_what) {
+ case NOTIFICATION_ENTER_TREE: {
+ _start();
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ _stop();
+ } break;
+
+ case NOTIFICATION_INTERNAL_PROCESS:
+ case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+ update_visibility(0);
+ } break;
+ }
+}
+
+void MultiplayerSynchronizer::set_replication_interval(double p_interval) {
+ ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)");
+ interval_msec = uint64_t(p_interval * 1000);
+}
+
+double MultiplayerSynchronizer::get_replication_interval() const {
+ return double(interval_msec) / 1000.0;
+}
+
+uint64_t MultiplayerSynchronizer::get_replication_interval_msec() const {
+ return interval_msec;
+}
+
+void MultiplayerSynchronizer::set_replication_config(Ref<SceneReplicationConfig> p_config) {
+ replication_config = p_config;
+}
+
+Ref<SceneReplicationConfig> MultiplayerSynchronizer::get_replication_config() {
+ return replication_config;
+}
+
+void MultiplayerSynchronizer::update_visibility(int p_for_peer) {
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ return;
+ }
+#endif
+ Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr;
+ if (node && get_multiplayer()->has_multiplayer_peer() && is_multiplayer_authority()) {
+ emit_signal(SNAME("visibility_changed"), p_for_peer);
+ }
+}
+
+void MultiplayerSynchronizer::set_root_path(const NodePath &p_path) {
+ _stop();
+ root_path = p_path;
+ _start();
+}
+
+NodePath MultiplayerSynchronizer::get_root_path() const {
+ return root_path;
+}
+
+void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_recursive) {
+ Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr;
+ if (!node) {
+ Node::set_multiplayer_authority(p_peer_id, p_recursive);
+ return;
+ }
+ get_multiplayer()->object_configuration_remove(node, this);
+ Node::set_multiplayer_authority(p_peer_id, p_recursive);
+ get_multiplayer()->object_configuration_add(node, this);
+}
+
+MultiplayerSynchronizer::MultiplayerSynchronizer() {
+ // Publicly visible by default.
+ peer_visibility.insert(0);
+}
diff --git a/modules/multiplayer/multiplayer_synchronizer.h b/modules/multiplayer/multiplayer_synchronizer.h
new file mode 100644
index 0000000000..e84d41db86
--- /dev/null
+++ b/modules/multiplayer/multiplayer_synchronizer.h
@@ -0,0 +1,96 @@
+/*************************************************************************/
+/* multiplayer_synchronizer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef MULTIPLAYER_SYNCHRONIZER_H
+#define MULTIPLAYER_SYNCHRONIZER_H
+
+#include "scene/main/node.h"
+
+#include "scene_replication_config.h"
+
+class MultiplayerSynchronizer : public Node {
+ GDCLASS(MultiplayerSynchronizer, Node);
+
+public:
+ enum VisibilityUpdateMode {
+ VISIBILITY_PROCESS_IDLE,
+ VISIBILITY_PROCESS_PHYSICS,
+ VISIBILITY_PROCESS_NONE,
+ };
+
+private:
+ Ref<SceneReplicationConfig> replication_config;
+ NodePath root_path = NodePath(".."); // Start with parent, like with AnimationPlayer.
+ uint64_t interval_msec = 0;
+ VisibilityUpdateMode visibility_update_mode = VISIBILITY_PROCESS_IDLE;
+ HashSet<Callable> visibility_filters;
+ HashSet<int> peer_visibility;
+
+ static Object *_get_prop_target(Object *p_obj, const NodePath &p_prop);
+ void _start();
+ void _stop();
+ void _update_process();
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_what);
+
+public:
+ static Error get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs);
+ static Error set_state(const List<NodePath> &p_properties, Object *p_obj, const Vector<Variant> &p_state);
+
+ void set_replication_interval(double p_interval);
+ double get_replication_interval() const;
+ uint64_t get_replication_interval_msec() const;
+
+ void set_replication_config(Ref<SceneReplicationConfig> p_config);
+ Ref<SceneReplicationConfig> get_replication_config();
+
+ void set_root_path(const NodePath &p_path);
+ NodePath get_root_path() const;
+ virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true) override;
+
+ bool is_visibility_public() const;
+ void set_visibility_public(bool p_public);
+ bool is_visible_to(int p_peer);
+ void set_visibility_for(int p_peer, bool p_visible);
+ bool get_visibility_for(int p_peer) const;
+ void update_visibility(int p_for_peer);
+ void set_visibility_update_mode(VisibilityUpdateMode p_mode);
+ void add_visibility_filter(Callable p_callback);
+ void remove_visibility_filter(Callable p_callback);
+ VisibilityUpdateMode get_visibility_update_mode() const;
+
+ MultiplayerSynchronizer();
+};
+
+VARIANT_ENUM_CAST(MultiplayerSynchronizer::VisibilityUpdateMode);
+
+#endif // MULTIPLAYER_SYNCHRONIZER_H
diff --git a/modules/multiplayer/register_types.cpp b/modules/multiplayer/register_types.cpp
new file mode 100644
index 0000000000..a2c524da80
--- /dev/null
+++ b/modules/multiplayer/register_types.cpp
@@ -0,0 +1,60 @@
+/*************************************************************************/
+/* register_types.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "register_types.h"
+
+#include "multiplayer_spawner.h"
+#include "multiplayer_synchronizer.h"
+#include "scene_multiplayer.h"
+#include "scene_replication_interface.h"
+#include "scene_rpc_interface.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/editor_plugin.h"
+#include "editor/replication_editor_plugin.h"
+#endif
+
+void initialize_multiplayer_module(ModuleInitializationLevel p_level) {
+ if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
+ GDREGISTER_CLASS(SceneReplicationConfig);
+ GDREGISTER_CLASS(MultiplayerSpawner);
+ GDREGISTER_CLASS(MultiplayerSynchronizer);
+ GDREGISTER_CLASS(SceneMultiplayer);
+ MultiplayerAPI::set_default_interface("SceneMultiplayer");
+ }
+#ifdef TOOLS_ENABLED
+ if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
+ EditorPlugins::add_by_type<ReplicationEditorPlugin>();
+ }
+#endif
+}
+
+void uninitialize_multiplayer_module(ModuleInitializationLevel p_level) {
+}
diff --git a/modules/multiplayer/register_types.h b/modules/multiplayer/register_types.h
new file mode 100644
index 0000000000..aca6cf46ed
--- /dev/null
+++ b/modules/multiplayer/register_types.h
@@ -0,0 +1,39 @@
+/*************************************************************************/
+/* register_types.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef MULTIPLAYER_REGISTER_TYPES_H
+#define MULTIPLAYER_REGISTER_TYPES_H
+
+#include "modules/register_module_types.h"
+
+void initialize_multiplayer_module(ModuleInitializationLevel p_level);
+void uninitialize_multiplayer_module(ModuleInitializationLevel p_level);
+
+#endif // MULTIPLAYER_REGISTER_TYPES_H
diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp
new file mode 100644
index 0000000000..4dc01321b9
--- /dev/null
+++ b/modules/multiplayer/scene_cache_interface.cpp
@@ -0,0 +1,268 @@
+/*************************************************************************/
+/* scene_cache_interface.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_cache_interface.h"
+
+#include "core/io/marshalls.h"
+#include "scene/main/node.h"
+#include "scene/main/window.h"
+
+#include "scene_multiplayer.h"
+
+void SceneCacheInterface::on_peer_change(int p_id, bool p_connected) {
+ if (p_connected) {
+ path_get_cache.insert(p_id, PathGetCache());
+ } else {
+ // Cleanup get cache.
+ path_get_cache.erase(p_id);
+ // Cleanup sent cache.
+ // Some refactoring is needed to make this faster and do paths GC.
+ for (const KeyValue<NodePath, PathSentCache> &E : path_send_cache) {
+ PathSentCache *psc = path_send_cache.getptr(E.key);
+ psc->confirmed_peers.erase(p_id);
+ }
+ }
+}
+
+void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
+ ERR_FAIL_COND(!root_node);
+ ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small.");
+ int ofs = 1;
+
+ String methods_md5;
+ methods_md5.parse_utf8((const char *)(p_packet + ofs), 32);
+ ofs += 33;
+
+ int id = decode_uint32(&p_packet[ofs]);
+ ofs += 4;
+
+ String paths;
+ paths.parse_utf8((const char *)(p_packet + ofs), p_packet_len - ofs);
+
+ NodePath path = paths;
+
+ if (!path_get_cache.has(p_from)) {
+ path_get_cache[p_from] = PathGetCache();
+ }
+
+ Node *node = root_node->get_node(path);
+ ERR_FAIL_COND(node == nullptr);
+ const bool valid_rpc_checksum = multiplayer->get_rpc_md5(node) == methods_md5;
+ if (valid_rpc_checksum == false) {
+ ERR_PRINT("The rpc node checksum failed. Make sure to have the same methods on both nodes. Node path: " + path);
+ }
+
+ PathGetCache::NodeInfo ni;
+ ni.path = path;
+
+ path_get_cache[p_from].nodes[id] = ni;
+
+ // Encode path to send ack.
+ CharString pname = String(path).utf8();
+ int len = encode_cstring(pname.get_data(), nullptr);
+
+ Vector<uint8_t> packet;
+
+ packet.resize(1 + 1 + len);
+ packet.write[0] = SceneMultiplayer::NETWORK_COMMAND_CONFIRM_PATH;
+ packet.write[1] = valid_rpc_checksum;
+ encode_cstring(pname.get_data(), &packet.write[2]);
+
+ Ref<MultiplayerPeer> multiplayer_peer = multiplayer->get_multiplayer_peer();
+ ERR_FAIL_COND(multiplayer_peer.is_null());
+
+#ifdef DEBUG_ENABLED
+ multiplayer->profile_bandwidth("out", packet.size());
+#endif
+
+ multiplayer_peer->set_transfer_channel(0);
+ multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
+ multiplayer_peer->set_target_peer(p_from);
+ multiplayer_peer->put_packet(packet.ptr(), packet.size());
+}
+
+void SceneCacheInterface::process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ ERR_FAIL_COND_MSG(p_packet_len < 3, "Invalid packet received. Size too small.");
+
+ const bool valid_rpc_checksum = p_packet[1];
+
+ String paths;
+ paths.parse_utf8((const char *)&p_packet[2], p_packet_len - 2);
+
+ NodePath path = paths;
+
+ if (valid_rpc_checksum == false) {
+ ERR_PRINT("The rpc node checksum failed. Make sure to have the same methods on both nodes. Node path: " + path);
+ }
+
+ PathSentCache *psc = path_send_cache.getptr(path);
+ ERR_FAIL_COND_MSG(!psc, "Invalid packet received. Tries to confirm a path which was not found in cache.");
+
+ HashMap<int, bool>::Iterator E = psc->confirmed_peers.find(p_from);
+ ERR_FAIL_COND_MSG(!E, "Invalid packet received. Source peer was not found in cache for the given path.");
+ E->value = true;
+}
+
+Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, const List<int> &p_peers) {
+ // Encode function name.
+ const CharString path = String(p_path).utf8();
+ const int path_len = encode_cstring(path.get_data(), nullptr);
+
+ // Extract MD5 from rpc methods list.
+ const String methods_md5 = multiplayer->get_rpc_md5(p_node);
+ const int methods_md5_len = 33; // 32 + 1 for the `0` that is added by the encoder.
+
+ Vector<uint8_t> packet;
+ packet.resize(1 + 4 + path_len + methods_md5_len);
+ int ofs = 0;
+
+ packet.write[ofs] = SceneMultiplayer::NETWORK_COMMAND_SIMPLIFY_PATH;
+ ofs += 1;
+
+ ofs += encode_cstring(methods_md5.utf8().get_data(), &packet.write[ofs]);
+
+ ofs += encode_uint32(psc->id, &packet.write[ofs]);
+
+ ofs += encode_cstring(path.get_data(), &packet.write[ofs]);
+
+ Ref<MultiplayerPeer> multiplayer_peer = multiplayer->get_multiplayer_peer();
+ ERR_FAIL_COND_V(multiplayer_peer.is_null(), ERR_BUG);
+
+#ifdef DEBUG_ENABLED
+ multiplayer->profile_bandwidth("out", packet.size() * p_peers.size());
+#endif
+
+ Error err = OK;
+ for (int peer_id : p_peers) {
+ multiplayer_peer->set_target_peer(peer_id);
+ multiplayer_peer->set_transfer_channel(0);
+ multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
+ err = multiplayer_peer->put_packet(packet.ptr(), packet.size());
+ ERR_FAIL_COND_V(err != OK, err);
+ // Insert into confirmed, but as false since it was not confirmed.
+ psc->confirmed_peers.insert(peer_id, false);
+ }
+ return err;
+}
+
+bool SceneCacheInterface::is_cache_confirmed(NodePath p_path, int p_peer) {
+ const PathSentCache *psc = path_send_cache.getptr(p_path);
+ ERR_FAIL_COND_V(!psc, false);
+ HashMap<int, bool>::ConstIterator F = psc->confirmed_peers.find(p_peer);
+ ERR_FAIL_COND_V(!F, false); // Should never happen.
+ return F->value;
+}
+
+int SceneCacheInterface::make_object_cache(Object *p_obj) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node, -1);
+ NodePath for_path = multiplayer->get_root_path().rel_path_to(node->get_path());
+ // See if the path is cached.
+ PathSentCache *psc = path_send_cache.getptr(for_path);
+ if (!psc) {
+ // Path is not cached, create.
+ path_send_cache[for_path] = PathSentCache();
+ psc = path_send_cache.getptr(for_path);
+ psc->id = last_send_cache_id++;
+ }
+ return psc->id;
+}
+
+bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r_id) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node, false);
+
+ r_id = make_object_cache(p_obj);
+ ERR_FAIL_COND_V(r_id < 0, false);
+ NodePath for_path = multiplayer->get_root_path().rel_path_to(node->get_path());
+ PathSentCache *psc = path_send_cache.getptr(for_path);
+
+ bool has_all_peers = true;
+ List<int> peers_to_add; // If one is missing, take note to add it.
+
+ if (p_peer_id > 0) {
+ // Fast single peer check.
+ HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(p_peer_id);
+ if (!F) {
+ peers_to_add.push_back(p_peer_id); // Need to also be notified.
+ has_all_peers = false;
+ } else if (!F->value) {
+ has_all_peers = false;
+ }
+ } else {
+ // Long and painful.
+ for (const int &E : multiplayer->get_connected_peers()) {
+ if (p_peer_id < 0 && E == -p_peer_id) {
+ continue; // Continue, excluded.
+ }
+ if (p_peer_id > 0 && E != p_peer_id) {
+ continue; // Continue, not for this peer.
+ }
+
+ HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(E);
+ if (!F) {
+ peers_to_add.push_back(E); // Need to also be notified.
+ has_all_peers = false;
+ } else if (!F->value) {
+ has_all_peers = false;
+ }
+ }
+ }
+
+ if (peers_to_add.size()) {
+ _send_confirm_path(node, for_path, psc, peers_to_add);
+ }
+
+ return has_all_peers;
+}
+
+Object *SceneCacheInterface::get_cached_object(int p_from, uint32_t p_cache_id) {
+ Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
+ ERR_FAIL_COND_V(!root_node, nullptr);
+ HashMap<int, PathGetCache>::Iterator E = path_get_cache.find(p_from);
+ ERR_FAIL_COND_V_MSG(!E, nullptr, vformat("No cache found for peer %d.", p_from));
+
+ HashMap<int, PathGetCache::NodeInfo>::Iterator F = E->value.nodes.find(p_cache_id);
+ ERR_FAIL_COND_V_MSG(!F, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from));
+
+ PathGetCache::NodeInfo *ni = &F->value;
+ Node *node = root_node->get_node(ni->path);
+ if (!node) {
+ ERR_PRINT("Failed to get cached path: " + String(ni->path) + ".");
+ }
+ return node;
+}
+
+void SceneCacheInterface::clear() {
+ path_get_cache.clear();
+ path_send_cache.clear();
+ last_send_cache_id = 1;
+}
diff --git a/modules/multiplayer/scene_cache_interface.h b/modules/multiplayer/scene_cache_interface.h
new file mode 100644
index 0000000000..1e80792fe7
--- /dev/null
+++ b/modules/multiplayer/scene_cache_interface.h
@@ -0,0 +1,82 @@
+/*************************************************************************/
+/* scene_cache_interface.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_CACHE_INTERFACE_H
+#define SCENE_CACHE_INTERFACE_H
+
+#include "scene/main/multiplayer_api.h"
+
+class SceneMultiplayer;
+
+class SceneCacheInterface : public RefCounted {
+ GDCLASS(SceneCacheInterface, RefCounted);
+
+private:
+ SceneMultiplayer *multiplayer = nullptr;
+
+ //path sent caches
+ struct PathSentCache {
+ HashMap<int, bool> confirmed_peers;
+ int id;
+ };
+
+ //path get caches
+ struct PathGetCache {
+ struct NodeInfo {
+ NodePath path;
+ ObjectID instance;
+ };
+
+ HashMap<int, NodeInfo> nodes;
+ };
+
+ HashMap<NodePath, PathSentCache> path_send_cache;
+ HashMap<int, PathGetCache> path_get_cache;
+ int last_send_cache_id = 1;
+
+protected:
+ Error _send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, const List<int> &p_peers);
+
+public:
+ void clear();
+ void on_peer_change(int p_id, bool p_connected);
+ void process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len);
+ void process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len);
+
+ // Returns true if all peers have cached path.
+ bool send_object_cache(Object *p_obj, int p_target, int &p_id);
+ int make_object_cache(Object *p_obj);
+ Object *get_cached_object(int p_from, uint32_t p_cache_id);
+ bool is_cache_confirmed(NodePath p_path, int p_peer);
+
+ SceneCacheInterface(SceneMultiplayer *p_multiplayer) { multiplayer = p_multiplayer; }
+};
+
+#endif // SCENE_CACHE_INTERFACE_H
diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp
new file mode 100644
index 0000000000..3fc1eef366
--- /dev/null
+++ b/modules/multiplayer/scene_multiplayer.cpp
@@ -0,0 +1,332 @@
+/*************************************************************************/
+/* scene_multiplayer.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_multiplayer.h"
+
+#include "core/debugger/engine_debugger.h"
+#include "core/io/marshalls.h"
+
+#include <stdint.h>
+
+#ifdef DEBUG_ENABLED
+#include "core/os/os.h"
+#endif
+
+#ifdef DEBUG_ENABLED
+void SceneMultiplayer::profile_bandwidth(const String &p_inout, int p_size) {
+ if (EngineDebugger::is_profiling("multiplayer")) {
+ Array values;
+ values.push_back(p_inout);
+ values.push_back(OS::get_singleton()->get_ticks_msec());
+ values.push_back(p_size);
+ EngineDebugger::profiler_add_frame_data("multiplayer", values);
+ }
+}
+#endif
+
+Error SceneMultiplayer::poll() {
+ if (!multiplayer_peer.is_valid() || multiplayer_peer->get_connection_status() == MultiplayerPeer::CONNECTION_DISCONNECTED) {
+ return ERR_UNCONFIGURED;
+ }
+
+ multiplayer_peer->poll();
+
+ if (!multiplayer_peer.is_valid()) { // It's possible that polling might have resulted in a disconnection, so check here.
+ return OK;
+ }
+
+ while (multiplayer_peer->get_available_packet_count()) {
+ int sender = multiplayer_peer->get_packet_peer();
+ const uint8_t *packet;
+ int len;
+
+ Error err = multiplayer_peer->get_packet(&packet, len);
+ ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error getting packet! %d", err));
+
+ remote_sender_id = sender;
+ _process_packet(sender, packet, len);
+ remote_sender_id = 0;
+
+ if (!multiplayer_peer.is_valid()) {
+ return OK; // It's also possible that a packet or RPC caused a disconnection, so also check here.
+ }
+ }
+ replicator->on_network_process();
+ return OK;
+}
+
+void SceneMultiplayer::clear() {
+ connected_peers.clear();
+ packet_cache.clear();
+ cache->clear();
+}
+
+void SceneMultiplayer::set_root_path(const NodePath &p_path) {
+ ERR_FAIL_COND_MSG(!p_path.is_absolute() && !p_path.is_empty(), "SceneMultiplayer root path must be absolute.");
+ root_path = p_path;
+}
+
+NodePath SceneMultiplayer::get_root_path() const {
+ return root_path;
+}
+
+void SceneMultiplayer::set_multiplayer_peer(const Ref<MultiplayerPeer> &p_peer) {
+ if (p_peer == multiplayer_peer) {
+ return; // Nothing to do
+ }
+
+ ERR_FAIL_COND_MSG(p_peer.is_valid() && p_peer->get_connection_status() == MultiplayerPeer::CONNECTION_DISCONNECTED,
+ "Supplied MultiplayerPeer must be connecting or connected.");
+
+ if (multiplayer_peer.is_valid()) {
+ multiplayer_peer->disconnect("peer_connected", callable_mp(this, &SceneMultiplayer::_add_peer));
+ multiplayer_peer->disconnect("peer_disconnected", callable_mp(this, &SceneMultiplayer::_del_peer));
+ multiplayer_peer->disconnect("connection_succeeded", callable_mp(this, &SceneMultiplayer::_connected_to_server));
+ multiplayer_peer->disconnect("connection_failed", callable_mp(this, &SceneMultiplayer::_connection_failed));
+ multiplayer_peer->disconnect("server_disconnected", callable_mp(this, &SceneMultiplayer::_server_disconnected));
+ clear();
+ }
+
+ multiplayer_peer = p_peer;
+
+ if (multiplayer_peer.is_valid()) {
+ multiplayer_peer->connect("peer_connected", callable_mp(this, &SceneMultiplayer::_add_peer));
+ multiplayer_peer->connect("peer_disconnected", callable_mp(this, &SceneMultiplayer::_del_peer));
+ multiplayer_peer->connect("connection_succeeded", callable_mp(this, &SceneMultiplayer::_connected_to_server));
+ multiplayer_peer->connect("connection_failed", callable_mp(this, &SceneMultiplayer::_connection_failed));
+ multiplayer_peer->connect("server_disconnected", callable_mp(this, &SceneMultiplayer::_server_disconnected));
+ }
+ replicator->on_reset();
+}
+
+Ref<MultiplayerPeer> SceneMultiplayer::get_multiplayer_peer() {
+ return multiplayer_peer;
+}
+
+void SceneMultiplayer::_process_packet(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ ERR_FAIL_COND_MSG(root_path.is_empty(), "Multiplayer root was not initialized. If you are using custom multiplayer, remember to set the root path via SceneMultiplayer.set_root_path before using it.");
+ ERR_FAIL_COND_MSG(p_packet_len < 1, "Invalid packet received. Size too small.");
+
+#ifdef DEBUG_ENABLED
+ profile_bandwidth("in", p_packet_len);
+#endif
+
+ // Extract the `packet_type` from the LSB three bits:
+ uint8_t packet_type = p_packet[0] & CMD_MASK;
+
+ switch (packet_type) {
+ case NETWORK_COMMAND_SIMPLIFY_PATH: {
+ cache->process_simplify_path(p_from, p_packet, p_packet_len);
+ } break;
+
+ case NETWORK_COMMAND_CONFIRM_PATH: {
+ cache->process_confirm_path(p_from, p_packet, p_packet_len);
+ } break;
+
+ case NETWORK_COMMAND_REMOTE_CALL: {
+ rpc->process_rpc(p_from, p_packet, p_packet_len);
+ } break;
+
+ case NETWORK_COMMAND_RAW: {
+ _process_raw(p_from, p_packet, p_packet_len);
+ } break;
+ case NETWORK_COMMAND_SPAWN: {
+ replicator->on_spawn_receive(p_from, p_packet, p_packet_len);
+ } break;
+ case NETWORK_COMMAND_DESPAWN: {
+ replicator->on_despawn_receive(p_from, p_packet, p_packet_len);
+ } break;
+ case NETWORK_COMMAND_SYNC: {
+ replicator->on_sync_receive(p_from, p_packet, p_packet_len);
+ } break;
+ }
+}
+
+void SceneMultiplayer::_add_peer(int p_id) {
+ connected_peers.insert(p_id);
+ cache->on_peer_change(p_id, true);
+ replicator->on_peer_change(p_id, true);
+ emit_signal(SNAME("peer_connected"), p_id);
+}
+
+void SceneMultiplayer::_del_peer(int p_id) {
+ replicator->on_peer_change(p_id, false);
+ cache->on_peer_change(p_id, false);
+ connected_peers.erase(p_id);
+ emit_signal(SNAME("peer_disconnected"), p_id);
+}
+
+void SceneMultiplayer::_connected_to_server() {
+ emit_signal(SNAME("connected_to_server"));
+}
+
+void SceneMultiplayer::_connection_failed() {
+ emit_signal(SNAME("connection_failed"));
+}
+
+void SceneMultiplayer::_server_disconnected() {
+ replicator->on_reset();
+ emit_signal(SNAME("server_disconnected"));
+}
+
+Error SceneMultiplayer::send_bytes(Vector<uint8_t> p_data, int p_to, MultiplayerPeer::TransferMode p_mode, int p_channel) {
+ ERR_FAIL_COND_V_MSG(p_data.size() < 1, ERR_INVALID_DATA, "Trying to send an empty raw packet.");
+ ERR_FAIL_COND_V_MSG(!multiplayer_peer.is_valid(), ERR_UNCONFIGURED, "Trying to send a raw packet while no multiplayer peer is active.");
+ ERR_FAIL_COND_V_MSG(multiplayer_peer->get_connection_status() != MultiplayerPeer::CONNECTION_CONNECTED, ERR_UNCONFIGURED, "Trying to send a raw packet via a multiplayer peer which is not connected.");
+
+ if (packet_cache.size() < p_data.size() + 1) {
+ packet_cache.resize(p_data.size() + 1);
+ }
+
+ const uint8_t *r = p_data.ptr();
+ packet_cache.write[0] = NETWORK_COMMAND_RAW;
+ memcpy(&packet_cache.write[1], &r[0], p_data.size());
+
+ multiplayer_peer->set_target_peer(p_to);
+ multiplayer_peer->set_transfer_channel(p_channel);
+ multiplayer_peer->set_transfer_mode(p_mode);
+
+ return multiplayer_peer->put_packet(packet_cache.ptr(), p_data.size() + 1);
+}
+
+void SceneMultiplayer::_process_raw(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ ERR_FAIL_COND_MSG(p_packet_len < 2, "Invalid packet received. Size too small.");
+
+ Vector<uint8_t> out;
+ int len = p_packet_len - 1;
+ out.resize(len);
+ {
+ uint8_t *w = out.ptrw();
+ memcpy(&w[0], &p_packet[1], len);
+ }
+ emit_signal(SNAME("peer_packet"), p_from, out);
+}
+
+int SceneMultiplayer::get_unique_id() {
+ ERR_FAIL_COND_V_MSG(!multiplayer_peer.is_valid(), 0, "No multiplayer peer is assigned. Unable to get unique ID.");
+ return multiplayer_peer->get_unique_id();
+}
+
+void SceneMultiplayer::set_refuse_new_connections(bool p_refuse) {
+ ERR_FAIL_COND_MSG(!multiplayer_peer.is_valid(), "No multiplayer peer is assigned. Unable to set 'refuse_new_connections'.");
+ multiplayer_peer->set_refuse_new_connections(p_refuse);
+}
+
+bool SceneMultiplayer::is_refusing_new_connections() const {
+ ERR_FAIL_COND_V_MSG(!multiplayer_peer.is_valid(), false, "No multiplayer peer is assigned. Unable to get 'refuse_new_connections'.");
+ return multiplayer_peer->is_refusing_new_connections();
+}
+
+Vector<int> SceneMultiplayer::get_peer_ids() {
+ ERR_FAIL_COND_V_MSG(!multiplayer_peer.is_valid(), Vector<int>(), "No multiplayer peer is assigned. Assume no peers are connected.");
+
+ Vector<int> ret;
+ for (const int &E : connected_peers) {
+ ret.push_back(E);
+ }
+
+ return ret;
+}
+
+void SceneMultiplayer::set_allow_object_decoding(bool p_enable) {
+ allow_object_decoding = p_enable;
+}
+
+bool SceneMultiplayer::is_object_decoding_allowed() const {
+ return allow_object_decoding;
+}
+
+String SceneMultiplayer::get_rpc_md5(const Object *p_obj) {
+ return rpc->get_rpc_md5(p_obj);
+}
+
+Error SceneMultiplayer::rpcp(Object *p_obj, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount) {
+ return rpc->rpcp(p_obj, p_peer_id, p_method, p_arg, p_argcount);
+}
+
+Error SceneMultiplayer::object_configuration_add(Object *p_obj, Variant p_config) {
+ if (p_obj == nullptr && p_config.get_type() == Variant::NODE_PATH) {
+ set_root_path(p_config);
+ return OK;
+ }
+ MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object());
+ MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object());
+ if (spawner) {
+ return replicator->on_spawn(p_obj, p_config);
+ } else if (sync) {
+ return replicator->on_replication_start(p_obj, p_config);
+ }
+ return ERR_INVALID_PARAMETER;
+}
+
+Error SceneMultiplayer::object_configuration_remove(Object *p_obj, Variant p_config) {
+ if (p_obj == nullptr && p_config.get_type() == Variant::NODE_PATH) {
+ ERR_FAIL_COND_V(root_path != p_config.operator NodePath(), ERR_INVALID_PARAMETER);
+ set_root_path(NodePath());
+ return OK;
+ }
+ MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object());
+ MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object());
+ if (spawner) {
+ return replicator->on_despawn(p_obj, p_config);
+ }
+ if (sync) {
+ return replicator->on_replication_stop(p_obj, p_config);
+ }
+ return ERR_INVALID_PARAMETER;
+}
+
+void SceneMultiplayer::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_root_path", "path"), &SceneMultiplayer::set_root_path);
+ ClassDB::bind_method(D_METHOD("get_root_path"), &SceneMultiplayer::get_root_path);
+ ClassDB::bind_method(D_METHOD("clear"), &SceneMultiplayer::clear);
+ ClassDB::bind_method(D_METHOD("set_refuse_new_connections", "refuse"), &SceneMultiplayer::set_refuse_new_connections);
+ ClassDB::bind_method(D_METHOD("is_refusing_new_connections"), &SceneMultiplayer::is_refusing_new_connections);
+ ClassDB::bind_method(D_METHOD("set_allow_object_decoding", "enable"), &SceneMultiplayer::set_allow_object_decoding);
+ ClassDB::bind_method(D_METHOD("is_object_decoding_allowed"), &SceneMultiplayer::is_object_decoding_allowed);
+ ClassDB::bind_method(D_METHOD("send_bytes", "bytes", "id", "mode", "channel"), &SceneMultiplayer::send_bytes, DEFVAL(MultiplayerPeer::TARGET_PEER_BROADCAST), DEFVAL(MultiplayerPeer::TRANSFER_MODE_RELIABLE), DEFVAL(0));
+
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_connections"), "set_refuse_new_connections", "is_refusing_new_connections");
+ ADD_PROPERTY_DEFAULT("refuse_new_connections", false);
+
+ ADD_SIGNAL(MethodInfo("peer_packet", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packet")));
+}
+
+SceneMultiplayer::SceneMultiplayer() {
+ replicator = Ref<SceneReplicationInterface>(memnew(SceneReplicationInterface(this)));
+ rpc = Ref<SceneRPCInterface>(memnew(SceneRPCInterface(this)));
+ cache = Ref<SceneCacheInterface>(memnew(SceneCacheInterface(this)));
+}
+
+SceneMultiplayer::~SceneMultiplayer() {
+ clear();
+}
diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h
new file mode 100644
index 0000000000..a99cca7b21
--- /dev/null
+++ b/modules/multiplayer/scene_multiplayer.h
@@ -0,0 +1,136 @@
+/*************************************************************************/
+/* scene_multiplayer.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_MULTIPLAYER_H
+#define SCENE_MULTIPLAYER_H
+
+#include "scene/main/multiplayer_api.h"
+
+#include "scene_cache_interface.h"
+#include "scene_replication_interface.h"
+#include "scene_rpc_interface.h"
+
+class SceneMultiplayer : public MultiplayerAPI {
+ GDCLASS(SceneMultiplayer, MultiplayerAPI);
+
+public:
+ enum NetworkCommands {
+ NETWORK_COMMAND_REMOTE_CALL = 0,
+ NETWORK_COMMAND_SIMPLIFY_PATH,
+ NETWORK_COMMAND_CONFIRM_PATH,
+ NETWORK_COMMAND_RAW,
+ NETWORK_COMMAND_SPAWN,
+ NETWORK_COMMAND_DESPAWN,
+ NETWORK_COMMAND_SYNC,
+ };
+
+ // For each command, the 4 MSB can contain custom flags, as defined by subsystems.
+ enum {
+ CMD_FLAG_0_SHIFT = 4,
+ CMD_FLAG_1_SHIFT = 5,
+ CMD_FLAG_2_SHIFT = 6,
+ CMD_FLAG_3_SHIFT = 7,
+ };
+
+ // This is the mask that will be used to extract the command.
+ enum {
+ CMD_MASK = 7, // 0x7 -> 0b00001111
+ };
+
+private:
+ Ref<MultiplayerPeer> multiplayer_peer;
+ HashSet<int> connected_peers;
+ int remote_sender_id = 0;
+ int remote_sender_override = 0;
+
+ Vector<uint8_t> packet_cache;
+
+ NodePath root_path;
+ bool allow_object_decoding = false;
+
+ Ref<SceneCacheInterface> cache;
+ Ref<SceneReplicationInterface> replicator;
+ Ref<SceneRPCInterface> rpc;
+
+protected:
+ static void _bind_methods();
+
+ void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len);
+ void _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len);
+
+ void _add_peer(int p_id);
+ void _del_peer(int p_id);
+ void _connected_to_server();
+ void _connection_failed();
+ void _server_disconnected();
+
+public:
+ virtual void set_multiplayer_peer(const Ref<MultiplayerPeer> &p_peer) override;
+ virtual Ref<MultiplayerPeer> get_multiplayer_peer() override;
+
+ virtual Error poll() override;
+ virtual int get_unique_id() override;
+ virtual Vector<int> get_peer_ids() override;
+ virtual int get_remote_sender_id() override { return remote_sender_override ? remote_sender_override : remote_sender_id; }
+
+ virtual Error rpcp(Object *p_obj, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount) override;
+
+ virtual Error object_configuration_add(Object *p_obj, Variant p_config) override;
+ virtual Error object_configuration_remove(Object *p_obj, Variant p_config) override;
+
+ void clear();
+
+ // Usually from object_configuration_add/remove
+ void set_root_path(const NodePath &p_path);
+ NodePath get_root_path() const;
+
+ Error send_bytes(Vector<uint8_t> p_data, int p_to = MultiplayerPeer::TARGET_PEER_BROADCAST, MultiplayerPeer::TransferMode p_mode = MultiplayerPeer::TRANSFER_MODE_RELIABLE, int p_channel = 0);
+ String get_rpc_md5(const Object *p_obj);
+
+ const HashSet<int> get_connected_peers() const { return connected_peers; }
+
+ void set_remote_sender_override(int p_id) { remote_sender_override = p_id; }
+ void set_refuse_new_connections(bool p_refuse);
+ bool is_refusing_new_connections() const;
+
+ void set_allow_object_decoding(bool p_enable);
+ bool is_object_decoding_allowed() const;
+
+ Ref<SceneCacheInterface> get_path_cache() { return cache; }
+
+#ifdef DEBUG_ENABLED
+ void profile_bandwidth(const String &p_inout, int p_size);
+#endif
+
+ SceneMultiplayer();
+ ~SceneMultiplayer();
+};
+
+#endif // SCENE_MULTIPLAYER_H
diff --git a/modules/multiplayer/scene_replication_config.cpp b/modules/multiplayer/scene_replication_config.cpp
new file mode 100644
index 0000000000..ae06516b7b
--- /dev/null
+++ b/modules/multiplayer/scene_replication_config.cpp
@@ -0,0 +1,205 @@
+/*************************************************************************/
+/* scene_replication_config.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_replication_config.h"
+
+#include "scene/main/multiplayer_api.h"
+#include "scene/main/node.h"
+
+bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_value) {
+ String name = p_name;
+
+ if (name.begins_with("properties/")) {
+ int idx = name.get_slicec('/', 1).to_int();
+ String what = name.get_slicec('/', 2);
+
+ if (properties.size() == idx && what == "path") {
+ ERR_FAIL_COND_V(p_value.get_type() != Variant::NODE_PATH, false);
+ NodePath path = p_value;
+ ERR_FAIL_COND_V(path.is_empty() || path.get_subname_count() == 0, false);
+ add_property(path);
+ return true;
+ }
+ ERR_FAIL_COND_V(p_value.get_type() != Variant::BOOL, false);
+ ERR_FAIL_INDEX_V(idx, properties.size(), false);
+ ReplicationProperty &prop = properties[idx];
+ if (what == "sync") {
+ prop.sync = p_value;
+ if (prop.sync) {
+ sync_props.push_back(prop.name);
+ } else {
+ sync_props.erase(prop.name);
+ }
+ return true;
+ } else if (what == "spawn") {
+ prop.spawn = p_value;
+ if (prop.spawn) {
+ spawn_props.push_back(prop.name);
+ } else {
+ spawn_props.erase(prop.name);
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) const {
+ String name = p_name;
+
+ if (name.begins_with("properties/")) {
+ int idx = name.get_slicec('/', 1).to_int();
+ String what = name.get_slicec('/', 2);
+ ERR_FAIL_INDEX_V(idx, properties.size(), false);
+ const ReplicationProperty &prop = properties[idx];
+ if (what == "path") {
+ r_ret = prop.name;
+ return true;
+ } else if (what == "sync") {
+ r_ret = prop.sync;
+ return true;
+ } else if (what == "spawn") {
+ r_ret = prop.spawn;
+ return true;
+ }
+ }
+ return false;
+}
+
+void SceneReplicationConfig::_get_property_list(List<PropertyInfo> *p_list) const {
+ for (int i = 0; i < properties.size(); i++) {
+ p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
+ p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/spawn", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
+ p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
+ }
+}
+
+TypedArray<NodePath> SceneReplicationConfig::get_properties() const {
+ TypedArray<NodePath> paths;
+ for (const ReplicationProperty &prop : properties) {
+ paths.push_back(prop.name);
+ }
+ return paths;
+}
+
+void SceneReplicationConfig::add_property(const NodePath &p_path, int p_index) {
+ ERR_FAIL_COND(properties.find(p_path));
+
+ if (p_index < 0 || p_index == properties.size()) {
+ properties.push_back(ReplicationProperty(p_path));
+ return;
+ }
+
+ ERR_FAIL_INDEX(p_index, properties.size());
+
+ List<ReplicationProperty>::Element *I = properties.front();
+ int c = 0;
+ while (c < p_index) {
+ I = I->next();
+ c++;
+ }
+ properties.insert_before(I, ReplicationProperty(p_path));
+}
+
+void SceneReplicationConfig::remove_property(const NodePath &p_path) {
+ properties.erase(p_path);
+}
+
+bool SceneReplicationConfig::has_property(const NodePath &p_path) const {
+ for (int i = 0; i < properties.size(); i++) {
+ if (properties[i].name == p_path) {
+ return true;
+ }
+ }
+ return false;
+}
+
+int SceneReplicationConfig::property_get_index(const NodePath &p_path) const {
+ for (int i = 0; i < properties.size(); i++) {
+ if (properties[i].name == p_path) {
+ return i;
+ }
+ }
+ ERR_FAIL_V(-1);
+}
+
+bool SceneReplicationConfig::property_get_spawn(const NodePath &p_path) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND_V(!E, false);
+ return E->get().spawn;
+}
+
+void SceneReplicationConfig::property_set_spawn(const NodePath &p_path, bool p_enabled) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND(!E);
+ if (E->get().spawn == p_enabled) {
+ return;
+ }
+ E->get().spawn = p_enabled;
+ spawn_props.clear();
+ for (const ReplicationProperty &prop : properties) {
+ if (prop.spawn) {
+ spawn_props.push_back(p_path);
+ }
+ }
+}
+
+bool SceneReplicationConfig::property_get_sync(const NodePath &p_path) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND_V(!E, false);
+ return E->get().sync;
+}
+
+void SceneReplicationConfig::property_set_sync(const NodePath &p_path, bool p_enabled) {
+ List<ReplicationProperty>::Element *E = properties.find(p_path);
+ ERR_FAIL_COND(!E);
+ if (E->get().sync == p_enabled) {
+ return;
+ }
+ E->get().sync = p_enabled;
+ sync_props.clear();
+ for (const ReplicationProperty &prop : properties) {
+ if (prop.sync) {
+ sync_props.push_back(p_path);
+ }
+ }
+}
+
+void SceneReplicationConfig::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("get_properties"), &SceneReplicationConfig::get_properties);
+ ClassDB::bind_method(D_METHOD("add_property", "path", "index"), &SceneReplicationConfig::add_property, DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("has_property", "path"), &SceneReplicationConfig::has_property);
+ ClassDB::bind_method(D_METHOD("remove_property", "path"), &SceneReplicationConfig::remove_property);
+ ClassDB::bind_method(D_METHOD("property_get_index", "path"), &SceneReplicationConfig::property_get_index);
+ ClassDB::bind_method(D_METHOD("property_get_spawn", "path"), &SceneReplicationConfig::property_get_spawn);
+ ClassDB::bind_method(D_METHOD("property_set_spawn", "path", "enabled"), &SceneReplicationConfig::property_set_spawn);
+ ClassDB::bind_method(D_METHOD("property_get_sync", "path"), &SceneReplicationConfig::property_get_sync);
+ ClassDB::bind_method(D_METHOD("property_set_sync", "path", "enabled"), &SceneReplicationConfig::property_set_sync);
+}
diff --git a/modules/multiplayer/scene_replication_config.h b/modules/multiplayer/scene_replication_config.h
new file mode 100644
index 0000000000..ab3658d2a7
--- /dev/null
+++ b/modules/multiplayer/scene_replication_config.h
@@ -0,0 +1,91 @@
+/*************************************************************************/
+/* scene_replication_config.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_REPLICATION_CONFIG_H
+#define SCENE_REPLICATION_CONFIG_H
+
+#include "core/io/resource.h"
+
+#include "core/variant/typed_array.h"
+
+class SceneReplicationConfig : public Resource {
+ GDCLASS(SceneReplicationConfig, Resource);
+ OBJ_SAVE_TYPE(SceneReplicationConfig);
+ RES_BASE_EXTENSION("repl");
+
+private:
+ struct ReplicationProperty {
+ NodePath name;
+ bool spawn = true;
+ bool sync = true;
+
+ bool operator==(const ReplicationProperty &p_to) {
+ return name == p_to.name;
+ }
+
+ ReplicationProperty() {}
+
+ ReplicationProperty(const NodePath &p_name) {
+ name = p_name;
+ }
+ };
+
+ List<ReplicationProperty> properties;
+ List<NodePath> spawn_props;
+ List<NodePath> sync_props;
+
+protected:
+ static void _bind_methods();
+
+ bool _set(const StringName &p_name, const Variant &p_value);
+ bool _get(const StringName &p_name, Variant &r_ret) const;
+ void _get_property_list(List<PropertyInfo> *p_list) const;
+
+public:
+ TypedArray<NodePath> get_properties() const;
+
+ void add_property(const NodePath &p_path, int p_index = -1);
+ void remove_property(const NodePath &p_path);
+ bool has_property(const NodePath &p_path) const;
+
+ int property_get_index(const NodePath &p_path) const;
+ bool property_get_spawn(const NodePath &p_path);
+ void property_set_spawn(const NodePath &p_path, bool p_enabled);
+
+ bool property_get_sync(const NodePath &p_path);
+ void property_set_sync(const NodePath &p_path, bool p_enabled);
+
+ const List<NodePath> &get_spawn_properties() { return spawn_props; }
+ const List<NodePath> &get_sync_properties() { return sync_props; }
+
+ SceneReplicationConfig() {}
+};
+
+#endif // SCENE_REPLICATION_CONFIG_H
diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp
new file mode 100644
index 0000000000..2dc0766fc3
--- /dev/null
+++ b/modules/multiplayer/scene_replication_interface.cpp
@@ -0,0 +1,527 @@
+/*************************************************************************/
+/* scene_replication_interface.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_replication_interface.h"
+
+#include "core/io/marshalls.h"
+#include "scene/main/node.h"
+
+#include "multiplayer_spawner.h"
+#include "multiplayer_synchronizer.h"
+#include "scene_multiplayer.h"
+
+#define MAKE_ROOM(m_amount) \
+ if (packet_cache.size() < m_amount) \
+ packet_cache.resize(m_amount);
+
+void SceneReplicationInterface::_free_remotes(int p_id) {
+ const HashMap<uint32_t, ObjectID> remotes = rep_state->peer_get_remotes(p_id);
+ for (const KeyValue<uint32_t, ObjectID> &E : remotes) {
+ Node *node = rep_state->get_node(E.value);
+ ERR_CONTINUE(!node);
+ node->queue_delete();
+ }
+}
+
+void SceneReplicationInterface::on_peer_change(int p_id, bool p_connected) {
+ if (p_connected) {
+ rep_state->on_peer_change(p_id, p_connected);
+ for (const ObjectID &oid : rep_state->get_spawned_nodes()) {
+ _update_spawn_visibility(p_id, oid);
+ }
+ for (const ObjectID &oid : rep_state->get_synced_nodes()) {
+ MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid);
+ ERR_CONTINUE(!sync); // ERR_BUG
+ if (sync->is_multiplayer_authority()) {
+ _update_sync_visibility(p_id, oid);
+ }
+ }
+ } else {
+ _free_remotes(p_id);
+ rep_state->on_peer_change(p_id, p_connected);
+ }
+}
+
+void SceneReplicationInterface::on_reset() {
+ for (int pid : rep_state->get_peers()) {
+ _free_remotes(pid);
+ }
+ rep_state->reset();
+}
+
+void SceneReplicationInterface::on_network_process() {
+ uint64_t msec = OS::get_singleton()->get_ticks_msec();
+ for (int peer : rep_state->get_peers()) {
+ _send_sync(peer, msec);
+ }
+}
+
+Error SceneReplicationInterface::on_spawn(Object *p_obj, Variant p_config) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
+ MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object());
+ ERR_FAIL_COND_V(!spawner, ERR_INVALID_PARAMETER);
+ Error err = rep_state->config_add_spawn(node, spawner);
+ ERR_FAIL_COND_V(err != OK, err);
+ const ObjectID oid = node->get_instance_id();
+ if (multiplayer->has_multiplayer_peer() && spawner->is_multiplayer_authority()) {
+ rep_state->ensure_net_id(oid);
+ _update_spawn_visibility(0, oid);
+ }
+ ERR_FAIL_COND_V(err != OK, err);
+ return OK;
+}
+
+Error SceneReplicationInterface::on_despawn(Object *p_obj, Variant p_config) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
+ MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object());
+ ERR_FAIL_COND_V(!p_obj || !spawner, ERR_INVALID_PARAMETER);
+ // Forcibly despawn to all peers that knowns me.
+ int len = 0;
+ Error err = _make_despawn_packet(node, len);
+ ERR_FAIL_COND_V(err != OK, ERR_BUG);
+ const ObjectID oid = p_obj->get_instance_id();
+ for (int pid : rep_state->get_peers()) {
+ if (!rep_state->is_peer_spawn(pid, oid)) {
+ continue;
+ }
+ _send_raw(packet_cache.ptr(), len, pid, true);
+ }
+ // Also remove spawner tracking from the replication state.
+ return rep_state->config_del_spawn(node, spawner);
+}
+
+Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_config) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
+ MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object());
+ ERR_FAIL_COND_V(!sync, ERR_INVALID_PARAMETER);
+
+ // Add to synchronizer list and setup visibility.
+ rep_state->config_add_sync(node, sync);
+ const ObjectID oid = node->get_instance_id();
+ sync->connect("visibility_changed", callable_mp(this, &SceneReplicationInterface::_visibility_changed), varray(oid));
+ if (multiplayer->has_multiplayer_peer() && sync->is_multiplayer_authority()) {
+ _update_sync_visibility(0, oid);
+ }
+
+ // Try to apply initial state if spawning (hack to apply if before ready).
+ if (pending_spawn == p_obj->get_instance_id()) {
+ pending_spawn = ObjectID(); // Make sure this only happens once.
+ const List<NodePath> props = sync->get_replication_config()->get_spawn_properties();
+ Vector<Variant> vars;
+ vars.resize(props.size());
+ int consumed;
+ Error err = MultiplayerAPI::decode_and_decompress_variants(vars, pending_buffer, pending_buffer_size, consumed);
+ ERR_FAIL_COND_V(err, err);
+ err = MultiplayerSynchronizer::set_state(props, node, vars);
+ ERR_FAIL_COND_V(err, err);
+ }
+ return OK;
+}
+
+Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_config) {
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
+ MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object());
+ ERR_FAIL_COND_V(!sync, ERR_INVALID_PARAMETER);
+ sync->disconnect("visibility_changed", callable_mp(this, &SceneReplicationInterface::_visibility_changed));
+ return rep_state->config_del_sync(node, sync);
+}
+
+void SceneReplicationInterface::_visibility_changed(int p_peer, ObjectID p_oid) {
+ if (rep_state->is_spawned_node(p_oid)) {
+ _update_spawn_visibility(p_peer, p_oid);
+ }
+ if (rep_state->is_synced_node(p_oid)) {
+ _update_sync_visibility(p_peer, p_oid);
+ }
+}
+
+Error SceneReplicationInterface::_update_sync_visibility(int p_peer, const ObjectID &p_oid) {
+ MultiplayerSynchronizer *sync = rep_state->get_synchronizer(p_oid);
+ ERR_FAIL_COND_V(!sync || !sync->is_multiplayer_authority(), ERR_BUG);
+ bool is_visible = sync->is_visible_to(p_peer);
+ if (p_peer == 0) {
+ for (int pid : rep_state->get_peers()) {
+ // Might be visible to this specific peer.
+ is_visible = is_visible || sync->is_visible_to(pid);
+ if (rep_state->is_peer_sync(pid, p_oid) == is_visible) {
+ continue;
+ }
+ if (is_visible) {
+ rep_state->peer_add_sync(pid, p_oid);
+ } else {
+ rep_state->peer_del_sync(pid, p_oid);
+ }
+ }
+ return OK;
+ } else {
+ if (is_visible == rep_state->is_peer_sync(p_peer, p_oid)) {
+ return OK;
+ }
+ if (is_visible) {
+ return rep_state->peer_add_sync(p_peer, p_oid);
+ } else {
+ return rep_state->peer_del_sync(p_peer, p_oid);
+ }
+ }
+}
+
+Error SceneReplicationInterface::_update_spawn_visibility(int p_peer, const ObjectID &p_oid) {
+ MultiplayerSpawner *spawner = rep_state->get_spawner(p_oid);
+ MultiplayerSynchronizer *sync = rep_state->get_synchronizer(p_oid);
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_oid));
+ ERR_FAIL_COND_V(!node || !spawner || !spawner->is_multiplayer_authority(), ERR_BUG);
+ bool is_visible = !sync || sync->is_visible_to(p_peer);
+ // Spawn (and despawn) when needed.
+ HashSet<int> to_spawn;
+ HashSet<int> to_despawn;
+ if (p_peer) {
+ if (is_visible == rep_state->is_peer_spawn(p_peer, p_oid)) {
+ return OK;
+ }
+ if (is_visible) {
+ to_spawn.insert(p_peer);
+ } else {
+ to_despawn.insert(p_peer);
+ }
+ } else {
+ // Check visibility for each peers.
+ for (int pid : rep_state->get_peers()) {
+ bool peer_visible = is_visible || sync->is_visible_to(pid);
+ if (peer_visible == rep_state->is_peer_spawn(pid, p_oid)) {
+ continue;
+ }
+ if (peer_visible) {
+ to_spawn.insert(pid);
+ } else {
+ to_despawn.insert(pid);
+ }
+ }
+ }
+ if (to_spawn.size()) {
+ int len = 0;
+ _make_spawn_packet(node, len);
+ for (int pid : to_spawn) {
+ int path_id;
+ multiplayer->get_path_cache()->send_object_cache(spawner, pid, path_id);
+ _send_raw(packet_cache.ptr(), len, pid, true);
+ rep_state->peer_add_spawn(pid, p_oid);
+ }
+ }
+ if (to_despawn.size()) {
+ int len = 0;
+ _make_despawn_packet(node, len);
+ for (int pid : to_despawn) {
+ rep_state->peer_del_spawn(pid, p_oid);
+ _send_raw(packet_cache.ptr(), len, pid, true);
+ }
+ }
+ return OK;
+}
+
+Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable) {
+ ERR_FAIL_COND_V(!p_buffer || p_size < 1, ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(!multiplayer, ERR_UNCONFIGURED);
+ ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED);
+
+#ifdef DEBUG_ENABLED
+ multiplayer->profile_bandwidth("out", p_size);
+#endif
+
+ Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
+ peer->set_target_peer(p_peer);
+ peer->set_transfer_channel(0);
+ peer->set_transfer_mode(p_reliable ? MultiplayerPeer::TRANSFER_MODE_RELIABLE : MultiplayerPeer::TRANSFER_MODE_UNRELIABLE);
+ return peer->put_packet(p_buffer, p_size);
+}
+
+Error SceneReplicationInterface::_make_spawn_packet(Node *p_node, int &r_len) {
+ ERR_FAIL_COND_V(!multiplayer, ERR_BUG);
+
+ const ObjectID oid = p_node->get_instance_id();
+ MultiplayerSpawner *spawner = rep_state->get_spawner(oid);
+ ERR_FAIL_COND_V(!spawner || !p_node, ERR_BUG);
+
+ uint32_t nid = rep_state->get_net_id(oid);
+ ERR_FAIL_COND_V(!nid, ERR_UNCONFIGURED);
+
+ // Prepare custom arg and scene_id
+ uint8_t scene_id = spawner->find_spawnable_scene_index_from_object(oid);
+ bool is_custom = scene_id == MultiplayerSpawner::INVALID_ID;
+ Variant spawn_arg = spawner->get_spawn_argument(oid);
+ int spawn_arg_size = 0;
+ if (is_custom) {
+ Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, nullptr, spawn_arg_size, false);
+ ERR_FAIL_COND_V(err, err);
+ }
+
+ // Prepare spawn state.
+ int state_size = 0;
+ Vector<Variant> state_vars;
+ Vector<const Variant *> state_varp;
+ MultiplayerSynchronizer *synchronizer = rep_state->get_synchronizer(oid);
+ if (synchronizer) {
+ ERR_FAIL_COND_V(synchronizer->get_replication_config().is_null(), ERR_BUG);
+ const List<NodePath> props = synchronizer->get_replication_config()->get_spawn_properties();
+ Error err = MultiplayerSynchronizer::get_state(props, p_node, state_vars, state_varp);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to retrieve spawn state.");
+ err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), nullptr, state_size);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to encode spawn state.");
+ }
+
+ // Encode scene ID, path ID, net ID, node name.
+ int path_id = multiplayer->get_path_cache()->make_object_cache(spawner);
+ CharString cname = p_node->get_name().operator String().utf8();
+ int nlen = encode_cstring(cname.get_data(), nullptr);
+ MAKE_ROOM(1 + 1 + 4 + 4 + 4 + nlen + (is_custom ? 4 + spawn_arg_size : 0) + state_size);
+ uint8_t *ptr = packet_cache.ptrw();
+ ptr[0] = (uint8_t)SceneMultiplayer::NETWORK_COMMAND_SPAWN;
+ ptr[1] = scene_id;
+ int ofs = 2;
+ ofs += encode_uint32(path_id, &ptr[ofs]);
+ ofs += encode_uint32(nid, &ptr[ofs]);
+ ofs += encode_uint32(nlen, &ptr[ofs]);
+ ofs += encode_cstring(cname.get_data(), &ptr[ofs]);
+ // Write args
+ if (is_custom) {
+ ofs += encode_uint32(spawn_arg_size, &ptr[ofs]);
+ Error err = MultiplayerAPI::encode_and_compress_variant(spawn_arg, &ptr[ofs], spawn_arg_size, false);
+ ERR_FAIL_COND_V(err, err);
+ ofs += spawn_arg_size;
+ }
+ // Write state.
+ if (state_size) {
+ Error err = MultiplayerAPI::encode_and_compress_variants(state_varp.ptrw(), state_varp.size(), &ptr[ofs], state_size);
+ ERR_FAIL_COND_V(err, err);
+ ofs += state_size;
+ }
+ r_len = ofs;
+ return OK;
+}
+
+Error SceneReplicationInterface::_make_despawn_packet(Node *p_node, int &r_len) {
+ const ObjectID oid = p_node->get_instance_id();
+ MAKE_ROOM(5);
+ uint8_t *ptr = packet_cache.ptrw();
+ ptr[0] = (uint8_t)SceneMultiplayer::NETWORK_COMMAND_DESPAWN;
+ int ofs = 1;
+ uint32_t nid = rep_state->get_net_id(oid);
+ ofs += encode_uint32(nid, &ptr[ofs]);
+ r_len = ofs;
+ return OK;
+}
+
+Error SceneReplicationInterface::on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
+ ERR_FAIL_COND_V_MSG(p_buffer_len < 14, ERR_INVALID_DATA, "Invalid spawn packet received");
+ int ofs = 1; // The spawn/despawn command.
+ uint8_t scene_id = p_buffer[ofs];
+ ofs += 1;
+ uint32_t node_target = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(multiplayer->get_path_cache()->get_cached_object(p_from, node_target));
+ ERR_FAIL_COND_V(!spawner, ERR_DOES_NOT_EXIST);
+ ERR_FAIL_COND_V(p_from != spawner->get_multiplayer_authority(), ERR_UNAUTHORIZED);
+
+ uint32_t net_id = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ uint32_t name_len = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ ERR_FAIL_COND_V_MSG(name_len > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA, vformat("Invalid spawn packet size: %d, wants: %d", p_buffer_len, ofs + name_len));
+ ERR_FAIL_COND_V_MSG(name_len < 1, ERR_INVALID_DATA, "Zero spawn name size.");
+
+ // We need to make sure no trickery happens here, but we want to allow autogenerated ("@") node names.
+ const String name = String::utf8((const char *)&p_buffer[ofs], name_len);
+ ERR_FAIL_COND_V_MSG(name.validate_node_name() != name, ERR_INVALID_DATA, vformat("Invalid node name received: '%s'. Make sure to add nodes via 'add_child(node, true)' remotely.", name));
+ ofs += name_len;
+
+ // Check that we can spawn.
+ Node *parent = spawner->get_node_or_null(spawner->get_spawn_path());
+ ERR_FAIL_COND_V(!parent, ERR_UNCONFIGURED);
+ ERR_FAIL_COND_V(parent->has_node(name), ERR_INVALID_DATA);
+
+ Node *node = nullptr;
+ if (scene_id == MultiplayerSpawner::INVALID_ID) {
+ // Custom spawn.
+ ERR_FAIL_COND_V(p_buffer_len - ofs < 4, ERR_INVALID_DATA);
+ uint32_t arg_size = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ ERR_FAIL_COND_V(arg_size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA);
+ Variant v;
+ Error err = MultiplayerAPI::decode_and_decompress_variant(v, &p_buffer[ofs], arg_size, nullptr, false);
+ ERR_FAIL_COND_V(err != OK, err);
+ ofs += arg_size;
+ node = spawner->instantiate_custom(v);
+ } else {
+ // Scene based spawn.
+ node = spawner->instantiate_scene(scene_id);
+ }
+ ERR_FAIL_COND_V(!node, ERR_UNAUTHORIZED);
+ node->set_name(name);
+ rep_state->peer_add_remote(p_from, net_id, node, spawner);
+ // The initial state will be applied during the sync config (i.e. before _ready).
+ int state_len = p_buffer_len - ofs;
+ if (state_len) {
+ pending_spawn = node->get_instance_id();
+ pending_buffer = &p_buffer[ofs];
+ pending_buffer_size = state_len;
+ }
+ parent->add_child(node);
+ pending_spawn = ObjectID();
+ pending_buffer = nullptr;
+ pending_buffer_size = 0;
+ return OK;
+}
+
+Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
+ ERR_FAIL_COND_V_MSG(p_buffer_len < 5, ERR_INVALID_DATA, "Invalid spawn packet received");
+ int ofs = 1; // The spawn/despawn command.
+ uint32_t net_id = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ Node *node = nullptr;
+ Error err = rep_state->peer_del_remote(p_from, net_id, &node);
+ ERR_FAIL_COND_V(err != OK, err);
+ ERR_FAIL_COND_V(!node, ERR_BUG);
+ if (node->get_parent() != nullptr) {
+ node->get_parent()->remove_child(node);
+ }
+ node->queue_delete();
+ return OK;
+}
+
+void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) {
+ const HashSet<ObjectID> &to_sync = rep_state->get_peer_sync_nodes(p_peer);
+ if (to_sync.is_empty()) {
+ return;
+ }
+ MAKE_ROOM(sync_mtu);
+ uint8_t *ptr = packet_cache.ptrw();
+ ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC;
+ int ofs = 1;
+ ofs += encode_uint16(rep_state->peer_sync_next(p_peer), &ptr[1]);
+ // Can only send updates for already notified nodes.
+ // This is a lazy implementation, we could optimize much more here with by grouping by replication config.
+ for (const ObjectID &oid : to_sync) {
+ if (!rep_state->update_sync_time(oid, p_msec)) {
+ continue; // nothing to sync.
+ }
+ MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid);
+ ERR_CONTINUE(!sync || !sync->get_replication_config().is_valid());
+ Node *node = rep_state->get_node(oid);
+ ERR_CONTINUE(!node);
+ uint32_t net_id = rep_state->get_net_id(oid);
+ if (net_id == 0 || (net_id & 0x80000000)) {
+ int path_id = 0;
+ bool verified = multiplayer->get_path_cache()->send_object_cache(sync, p_peer, path_id);
+ ERR_CONTINUE_MSG(path_id < 0, "This should never happen!");
+ if (net_id == 0) {
+ // First time path based ID.
+ net_id = path_id | 0x80000000;
+ rep_state->set_net_id(oid, net_id | 0x80000000);
+ }
+ if (!verified) {
+ // The path based sync is not yet confirmed, skipping.
+ continue;
+ }
+ }
+ int size;
+ Vector<Variant> vars;
+ Vector<const Variant *> varp;
+ const List<NodePath> props = sync->get_replication_config()->get_sync_properties();
+ Error err = MultiplayerSynchronizer::get_state(props, node, vars, varp);
+ ERR_CONTINUE_MSG(err != OK, "Unable to retrieve sync state.");
+ err = MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), nullptr, size);
+ ERR_CONTINUE_MSG(err != OK, "Unable to encode sync state.");
+ // TODO Handle single state above MTU.
+ ERR_CONTINUE_MSG(size > 3 + 4 + 4 + sync_mtu, vformat("Node states bigger then MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path()));
+ if (ofs + 4 + 4 + size > sync_mtu) {
+ // Send what we got, and reset write.
+ _send_raw(packet_cache.ptr(), ofs, p_peer, false);
+ ofs = 3;
+ }
+ if (size) {
+ ofs += encode_uint32(rep_state->get_net_id(oid), &ptr[ofs]);
+ ofs += encode_uint32(size, &ptr[ofs]);
+ MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), &ptr[ofs], size);
+ ofs += size;
+ }
+ }
+ if (ofs > 3) {
+ // Got some left over to send.
+ _send_raw(packet_cache.ptr(), ofs, p_peer, false);
+ }
+}
+
+Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
+ ERR_FAIL_COND_V_MSG(p_buffer_len < 11, ERR_INVALID_DATA, "Invalid sync packet received");
+ uint16_t time = decode_uint16(&p_buffer[1]);
+ int ofs = 3;
+ rep_state->peer_sync_recv(p_from, time);
+ while (ofs + 8 < p_buffer_len) {
+ uint32_t net_id = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ uint32_t size = decode_uint32(&p_buffer[ofs]);
+ ofs += 4;
+ Node *node = nullptr;
+ if (net_id & 0x80000000) {
+ MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_path_cache()->get_cached_object(p_from, net_id & 0x7FFFFFFF));
+ ERR_FAIL_COND_V(!sync || sync->get_multiplayer_authority() != p_from, ERR_UNAUTHORIZED);
+ node = sync->get_node(sync->get_root_path());
+ } else {
+ node = rep_state->peer_get_remote(p_from, net_id);
+ }
+ if (!node) {
+ // Not received yet.
+ ofs += size;
+ continue;
+ }
+ const ObjectID oid = node->get_instance_id();
+ if (!rep_state->update_last_node_sync(oid, time)) {
+ // State is too old.
+ ofs += size;
+ continue;
+ }
+ MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid);
+ ERR_FAIL_COND_V(!sync, ERR_BUG);
+ ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_BUG);
+ const List<NodePath> props = sync->get_replication_config()->get_sync_properties();
+ Vector<Variant> vars;
+ vars.resize(props.size());
+ int consumed;
+ Error err = MultiplayerAPI::decode_and_decompress_variants(vars, &p_buffer[ofs], size, consumed);
+ ERR_FAIL_COND_V(err, err);
+ err = MultiplayerSynchronizer::set_state(props, node, vars);
+ ERR_FAIL_COND_V(err, err);
+ ofs += size;
+ }
+ return OK;
+}
diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h
new file mode 100644
index 0000000000..8981647429
--- /dev/null
+++ b/modules/multiplayer/scene_replication_interface.h
@@ -0,0 +1,86 @@
+/*************************************************************************/
+/* scene_replication_interface.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_REPLICATION_INTERFACE_H
+#define SCENE_REPLICATION_INTERFACE_H
+
+#include "scene/main/multiplayer_api.h"
+
+#include "scene_replication_state.h"
+
+class SceneMultiplayer;
+
+class SceneReplicationInterface : public RefCounted {
+ GDCLASS(SceneReplicationInterface, RefCounted);
+
+private:
+ void _send_sync(int p_peer, uint64_t p_msec);
+ Error _make_spawn_packet(Node *p_node, int &r_len);
+ Error _make_despawn_packet(Node *p_node, int &r_len);
+ Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable);
+
+ void _visibility_changed(int p_peer, ObjectID p_oid);
+ Error _update_sync_visibility(int p_peer, const ObjectID &p_oid);
+ Error _update_spawn_visibility(int p_peer, const ObjectID &p_oid);
+ void _free_remotes(int p_peer);
+
+ Ref<SceneReplicationState> rep_state;
+ SceneMultiplayer *multiplayer = nullptr;
+ PackedByteArray packet_cache;
+ int sync_mtu = 1350; // Highly dependent on underlying protocol.
+
+ // An hack to apply the initial state before ready.
+ ObjectID pending_spawn;
+ const uint8_t *pending_buffer = nullptr;
+ int pending_buffer_size = 0;
+
+public:
+ static void make_default();
+
+ void on_reset();
+ void on_peer_change(int p_id, bool p_connected);
+
+ Error on_spawn(Object *p_obj, Variant p_config);
+ Error on_despawn(Object *p_obj, Variant p_config);
+ Error on_replication_start(Object *p_obj, Variant p_config);
+ Error on_replication_stop(Object *p_obj, Variant p_config);
+ void on_network_process();
+
+ Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
+ Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
+ Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
+
+ SceneReplicationInterface(SceneMultiplayer *p_multiplayer) {
+ rep_state.instantiate();
+ multiplayer = p_multiplayer;
+ }
+};
+
+#endif // SCENE_REPLICATION_INTERFACE_H
diff --git a/modules/multiplayer/scene_replication_state.cpp b/modules/multiplayer/scene_replication_state.cpp
new file mode 100644
index 0000000000..5ad27e9a97
--- /dev/null
+++ b/modules/multiplayer/scene_replication_state.cpp
@@ -0,0 +1,267 @@
+/*************************************************************************/
+/* scene_replication_state.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_replication_state.h"
+
+#include "scene/scene_string_names.h"
+
+#include "multiplayer_spawner.h"
+#include "multiplayer_synchronizer.h"
+
+SceneReplicationState::TrackedNode &SceneReplicationState::_track(const ObjectID &p_id) {
+ if (!tracked_nodes.has(p_id)) {
+ tracked_nodes[p_id] = TrackedNode(p_id);
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(p_id));
+ node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack), varray(p_id), Node::CONNECT_ONESHOT);
+ }
+ return tracked_nodes[p_id];
+}
+
+void SceneReplicationState::_untrack(const ObjectID &p_id) {
+ if (tracked_nodes.has(p_id)) {
+ uint32_t net_id = tracked_nodes[p_id].net_id;
+ uint32_t peer = tracked_nodes[p_id].remote_peer;
+ tracked_nodes.erase(p_id);
+ // If it was spawned by a remote, remove it from the received nodes.
+ if (peer && peers_info.has(peer)) {
+ peers_info[peer].recv_nodes.erase(net_id);
+ }
+ // If we spawned or synced it, we need to remove it from any peer it was sent to.
+ if (net_id || peer == 0) {
+ for (KeyValue<int, PeerInfo> &E : peers_info) {
+ E.value.sync_nodes.erase(p_id);
+ E.value.spawn_nodes.erase(p_id);
+ }
+ }
+ }
+}
+
+const HashMap<uint32_t, ObjectID> SceneReplicationState::peer_get_remotes(int p_peer) const {
+ return peers_info.has(p_peer) ? peers_info[p_peer].recv_nodes : HashMap<uint32_t, ObjectID>();
+}
+
+bool SceneReplicationState::update_last_node_sync(const ObjectID &p_id, uint16_t p_time) {
+ TrackedNode *tnode = tracked_nodes.getptr(p_id);
+ ERR_FAIL_COND_V(!tnode, false);
+ if (p_time <= tnode->last_sync && tnode->last_sync - p_time < 32767) {
+ return false;
+ }
+ tnode->last_sync = p_time;
+ return true;
+}
+
+bool SceneReplicationState::update_sync_time(const ObjectID &p_id, uint64_t p_msec) {
+ TrackedNode *tnode = tracked_nodes.getptr(p_id);
+ ERR_FAIL_COND_V(!tnode, false);
+ MultiplayerSynchronizer *sync = get_synchronizer(p_id);
+ if (!sync) {
+ return false;
+ }
+ if (tnode->last_sync_msec == p_msec) {
+ return true;
+ }
+ if (p_msec >= tnode->last_sync_msec + sync->get_replication_interval_msec()) {
+ tnode->last_sync_msec = p_msec;
+ return true;
+ }
+ return false;
+}
+
+uint32_t SceneReplicationState::get_net_id(const ObjectID &p_id) const {
+ const TrackedNode *tnode = tracked_nodes.getptr(p_id);
+ ERR_FAIL_COND_V(!tnode, 0);
+ return tnode->net_id;
+}
+
+void SceneReplicationState::set_net_id(const ObjectID &p_id, uint32_t p_net_id) {
+ TrackedNode *tnode = tracked_nodes.getptr(p_id);
+ ERR_FAIL_COND(!tnode);
+ tnode->net_id = p_net_id;
+}
+
+uint32_t SceneReplicationState::ensure_net_id(const ObjectID &p_id) {
+ TrackedNode *tnode = tracked_nodes.getptr(p_id);
+ ERR_FAIL_COND_V(!tnode, 0);
+ if (tnode->net_id == 0) {
+ tnode->net_id = ++last_net_id;
+ }
+ return tnode->net_id;
+}
+
+void SceneReplicationState::on_peer_change(int p_peer, bool p_connected) {
+ if (p_connected) {
+ peers_info[p_peer] = PeerInfo();
+ known_peers.insert(p_peer);
+ } else {
+ peers_info.erase(p_peer);
+ known_peers.erase(p_peer);
+ }
+}
+
+void SceneReplicationState::reset() {
+ peers_info.clear();
+ known_peers.clear();
+ // Tracked nodes are cleared on deletion, here we only reset the ids so they can be later re-assigned.
+ for (KeyValue<ObjectID, TrackedNode> &E : tracked_nodes) {
+ TrackedNode &tobj = E.value;
+ tobj.net_id = 0;
+ tobj.remote_peer = 0;
+ tobj.last_sync = 0;
+ }
+}
+
+Error SceneReplicationState::config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner) {
+ const ObjectID oid = p_node->get_instance_id();
+ TrackedNode &tobj = _track(oid);
+ ERR_FAIL_COND_V(tobj.spawner != ObjectID(), ERR_ALREADY_IN_USE);
+ tobj.spawner = p_spawner->get_instance_id();
+ spawned_nodes.insert(oid);
+ return OK;
+}
+
+Error SceneReplicationState::config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner) {
+ const ObjectID oid = p_node->get_instance_id();
+ ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER);
+ TrackedNode &tobj = _track(oid);
+ ERR_FAIL_COND_V(tobj.spawner != p_spawner->get_instance_id(), ERR_INVALID_PARAMETER);
+ tobj.spawner = ObjectID();
+ spawned_nodes.erase(oid);
+ for (KeyValue<int, PeerInfo> &E : peers_info) {
+ E.value.spawn_nodes.erase(oid);
+ }
+ return OK;
+}
+
+Error SceneReplicationState::config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync) {
+ const ObjectID oid = p_node->get_instance_id();
+ TrackedNode &tobj = _track(oid);
+ ERR_FAIL_COND_V(tobj.synchronizer != ObjectID(), ERR_ALREADY_IN_USE);
+ tobj.synchronizer = p_sync->get_instance_id();
+ synced_nodes.insert(oid);
+ return OK;
+}
+
+Error SceneReplicationState::config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync) {
+ const ObjectID oid = p_node->get_instance_id();
+ ERR_FAIL_COND_V(!is_tracked(oid), ERR_INVALID_PARAMETER);
+ TrackedNode &tobj = _track(oid);
+ ERR_FAIL_COND_V(tobj.synchronizer != p_sync->get_instance_id(), ERR_INVALID_PARAMETER);
+ tobj.synchronizer = ObjectID();
+ synced_nodes.erase(oid);
+ for (KeyValue<int, PeerInfo> &E : peers_info) {
+ E.value.sync_nodes.erase(oid);
+ }
+ return OK;
+}
+
+Error SceneReplicationState::peer_add_sync(int p_peer, const ObjectID &p_id) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
+ peers_info[p_peer].sync_nodes.insert(p_id);
+ return OK;
+}
+
+Error SceneReplicationState::peer_del_sync(int p_peer, const ObjectID &p_id) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
+ peers_info[p_peer].sync_nodes.erase(p_id);
+ return OK;
+}
+
+const HashSet<ObjectID> SceneReplicationState::get_peer_sync_nodes(int p_peer) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), HashSet<ObjectID>());
+ return peers_info[p_peer].sync_nodes;
+}
+
+bool SceneReplicationState::is_peer_sync(int p_peer, const ObjectID &p_id) const {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), false);
+ return peers_info[p_peer].sync_nodes.has(p_id);
+}
+
+Error SceneReplicationState::peer_add_spawn(int p_peer, const ObjectID &p_id) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
+ peers_info[p_peer].spawn_nodes.insert(p_id);
+ return OK;
+}
+
+Error SceneReplicationState::peer_del_spawn(int p_peer, const ObjectID &p_id) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
+ peers_info[p_peer].spawn_nodes.erase(p_id);
+ return OK;
+}
+
+const HashSet<ObjectID> SceneReplicationState::get_peer_spawn_nodes(int p_peer) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), HashSet<ObjectID>());
+ return peers_info[p_peer].spawn_nodes;
+}
+
+bool SceneReplicationState::is_peer_spawn(int p_peer, const ObjectID &p_id) const {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), false);
+ return peers_info[p_peer].spawn_nodes.has(p_id);
+}
+
+Node *SceneReplicationState::peer_get_remote(int p_peer, uint32_t p_net_id) {
+ PeerInfo *info = peers_info.getptr(p_peer);
+ return info && info->recv_nodes.has(p_net_id) ? Object::cast_to<Node>(ObjectDB::get_instance(info->recv_nodes[p_net_id])) : nullptr;
+}
+
+Error SceneReplicationState::peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner) {
+ ERR_FAIL_COND_V(!p_node || !p_spawner, ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAVAILABLE);
+ PeerInfo &pinfo = peers_info[p_peer];
+ ObjectID oid = p_node->get_instance_id();
+ TrackedNode &tobj = _track(oid);
+ tobj.spawner = p_spawner->get_instance_id();
+ tobj.net_id = p_net_id;
+ tobj.remote_peer = p_peer;
+ tobj.last_sync = pinfo.last_recv_sync;
+ // Also track as a remote.
+ ERR_FAIL_COND_V(pinfo.recv_nodes.has(p_net_id), ERR_ALREADY_IN_USE);
+ pinfo.recv_nodes[p_net_id] = oid;
+ return OK;
+}
+
+Error SceneReplicationState::peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_UNAUTHORIZED);
+ PeerInfo &info = peers_info[p_peer];
+ ERR_FAIL_COND_V(!info.recv_nodes.has(p_net_id), ERR_UNAUTHORIZED);
+ *r_node = Object::cast_to<Node>(ObjectDB::get_instance(info.recv_nodes[p_net_id]));
+ info.recv_nodes.erase(p_net_id);
+ return OK;
+}
+
+uint16_t SceneReplicationState::peer_sync_next(int p_peer) {
+ ERR_FAIL_COND_V(!peers_info.has(p_peer), 0);
+ PeerInfo &info = peers_info[p_peer];
+ return ++info.last_sent_sync;
+}
+
+void SceneReplicationState::peer_sync_recv(int p_peer, uint16_t p_time) {
+ ERR_FAIL_COND(!peers_info.has(p_peer));
+ peers_info[p_peer].last_recv_sync = p_time;
+}
diff --git a/modules/multiplayer/scene_replication_state.h b/modules/multiplayer/scene_replication_state.h
new file mode 100644
index 0000000000..bdff6ae3b7
--- /dev/null
+++ b/modules/multiplayer/scene_replication_state.h
@@ -0,0 +1,135 @@
+/*************************************************************************/
+/* scene_replication_state.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_REPLICATION_STATE_H
+#define SCENE_REPLICATION_STATE_H
+
+#include "core/object/ref_counted.h"
+
+#include "multiplayer_spawner.h"
+#include "multiplayer_synchronizer.h"
+
+class MultiplayerSpawner;
+class MultiplayerSynchronizer;
+class Node;
+
+class SceneReplicationState : public RefCounted {
+private:
+ struct TrackedNode {
+ ObjectID id;
+ uint32_t net_id = 0;
+ uint32_t remote_peer = 0;
+ ObjectID spawner;
+ ObjectID synchronizer;
+ uint16_t last_sync = 0;
+ uint64_t last_sync_msec = 0;
+
+ bool operator==(const ObjectID &p_other) { return id == p_other; }
+
+ Node *get_node() const { return id.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(id)) : nullptr; }
+ MultiplayerSpawner *get_spawner() const { return spawner.is_valid() ? Object::cast_to<MultiplayerSpawner>(ObjectDB::get_instance(spawner)) : nullptr; }
+ MultiplayerSynchronizer *get_synchronizer() const { return synchronizer.is_valid() ? Object::cast_to<MultiplayerSynchronizer>(ObjectDB::get_instance(synchronizer)) : nullptr; }
+ TrackedNode() {}
+ TrackedNode(const ObjectID &p_id) { id = p_id; }
+ TrackedNode(const ObjectID &p_id, uint32_t p_net_id) {
+ id = p_id;
+ net_id = p_net_id;
+ }
+ };
+
+ struct PeerInfo {
+ HashSet<ObjectID> sync_nodes;
+ HashSet<ObjectID> spawn_nodes;
+ HashMap<uint32_t, ObjectID> recv_nodes;
+ uint16_t last_sent_sync = 0;
+ uint16_t last_recv_sync = 0;
+ };
+
+ HashSet<int> known_peers;
+ uint32_t last_net_id = 0;
+ HashMap<ObjectID, TrackedNode> tracked_nodes;
+ HashMap<int, PeerInfo> peers_info;
+ HashSet<ObjectID> spawned_nodes;
+ HashSet<ObjectID> synced_nodes;
+
+ TrackedNode &_track(const ObjectID &p_id);
+ void _untrack(const ObjectID &p_id);
+ bool is_tracked(const ObjectID &p_id) const { return tracked_nodes.has(p_id); }
+
+public:
+ const HashSet<int> get_peers() const { return known_peers; }
+ const HashSet<ObjectID> &get_spawned_nodes() const { return spawned_nodes; }
+ bool is_spawned_node(const ObjectID &p_id) const { return spawned_nodes.has(p_id); }
+ const HashSet<ObjectID> &get_synced_nodes() const { return synced_nodes; }
+ bool is_synced_node(const ObjectID &p_id) const { return synced_nodes.has(p_id); }
+
+ MultiplayerSynchronizer *get_synchronizer(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_synchronizer() : nullptr; }
+ MultiplayerSpawner *get_spawner(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_spawner() : nullptr; }
+ Node *get_node(const ObjectID &p_id) { return tracked_nodes.has(p_id) ? tracked_nodes[p_id].get_node() : nullptr; }
+ bool update_last_node_sync(const ObjectID &p_id, uint16_t p_time);
+ bool update_sync_time(const ObjectID &p_id, uint64_t p_msec);
+
+ uint32_t get_net_id(const ObjectID &p_id) const;
+ void set_net_id(const ObjectID &p_id, uint32_t p_net_id);
+ uint32_t ensure_net_id(const ObjectID &p_id);
+
+ void reset();
+ void on_peer_change(int p_peer, bool p_connected);
+
+ Error config_add_spawn(Node *p_node, MultiplayerSpawner *p_spawner);
+ Error config_del_spawn(Node *p_node, MultiplayerSpawner *p_spawner);
+
+ Error config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync);
+ Error config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync);
+
+ Error peer_add_sync(int p_peer, const ObjectID &p_id);
+ Error peer_del_sync(int p_peer, const ObjectID &p_id);
+
+ const HashSet<ObjectID> get_peer_sync_nodes(int p_peer);
+ bool is_peer_sync(int p_peer, const ObjectID &p_id) const;
+
+ Error peer_add_spawn(int p_peer, const ObjectID &p_id);
+ Error peer_del_spawn(int p_peer, const ObjectID &p_id);
+
+ const HashSet<ObjectID> get_peer_spawn_nodes(int p_peer);
+ bool is_peer_spawn(int p_peer, const ObjectID &p_id) const;
+
+ const HashMap<uint32_t, ObjectID> peer_get_remotes(int p_peer) const;
+ Node *peer_get_remote(int p_peer, uint32_t p_net_id);
+ Error peer_add_remote(int p_peer, uint32_t p_net_id, Node *p_node, MultiplayerSpawner *p_spawner);
+ Error peer_del_remote(int p_peer, uint32_t p_net_id, Node **r_node);
+
+ uint16_t peer_sync_next(int p_peer);
+ void peer_sync_recv(int p_peer, uint16_t p_time);
+
+ SceneReplicationState() {}
+};
+
+#endif // SCENE_REPLICATION_STATE_H
diff --git a/modules/multiplayer/scene_rpc_interface.cpp b/modules/multiplayer/scene_rpc_interface.cpp
new file mode 100644
index 0000000000..65090b9316
--- /dev/null
+++ b/modules/multiplayer/scene_rpc_interface.cpp
@@ -0,0 +1,521 @@
+/*************************************************************************/
+/* scene_rpc_interface.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "scene_rpc_interface.h"
+
+#include "core/debugger/engine_debugger.h"
+#include "core/io/marshalls.h"
+#include "scene/main/multiplayer_api.h"
+#include "scene/main/node.h"
+#include "scene/main/window.h"
+
+#include "scene_multiplayer.h"
+
+// The RPC meta is composed by a single byte that contains (starting from the least significant bit):
+// - `NetworkCommands` in the first four bits.
+// - `NetworkNodeIdCompression` in the next 2 bits.
+// - `NetworkNameIdCompression` in the next 1 bit.
+// - `byte_only_or_no_args` in the next 1 bit.
+#define NODE_ID_COMPRESSION_SHIFT SceneMultiplayer::CMD_FLAG_0_SHIFT
+#define NAME_ID_COMPRESSION_SHIFT SceneMultiplayer::CMD_FLAG_2_SHIFT
+#define BYTE_ONLY_OR_NO_ARGS_SHIFT SceneMultiplayer::CMD_FLAG_3_SHIFT
+
+#define NODE_ID_COMPRESSION_FLAG ((1 << NODE_ID_COMPRESSION_SHIFT) | (1 << (NODE_ID_COMPRESSION_SHIFT + 1)))
+#define NAME_ID_COMPRESSION_FLAG (1 << NAME_ID_COMPRESSION_SHIFT)
+#define BYTE_ONLY_OR_NO_ARGS_FLAG (1 << BYTE_ONLY_OR_NO_ARGS_SHIFT)
+
+#ifdef DEBUG_ENABLED
+_FORCE_INLINE_ void SceneRPCInterface::_profile_node_data(const String &p_what, ObjectID p_id) {
+ if (EngineDebugger::is_profiling("rpc")) {
+ Array values;
+ values.push_back(p_id);
+ values.push_back(p_what);
+ EngineDebugger::profiler_add_frame_data("rpc", values);
+ }
+}
+#else
+_FORCE_INLINE_ void SceneRPCInterface::_profile_node_data(const String &p_what, ObjectID p_id) {}
+#endif
+
+// Returns the packet size stripping the node path added when the node is not yet cached.
+int get_packet_len(uint32_t p_node_target, int p_packet_len) {
+ if (p_node_target & 0x80000000) {
+ int ofs = p_node_target & 0x7FFFFFFF;
+ return p_packet_len - (p_packet_len - ofs);
+ } else {
+ return p_packet_len;
+ }
+}
+
+void SceneRPCInterface::_parse_rpc_config(const Variant &p_config, bool p_for_node, RPCConfigCache &r_cache) {
+ if (p_config.get_type() == Variant::NIL) {
+ return;
+ }
+ ERR_FAIL_COND(p_config.get_type() != Variant::DICTIONARY);
+ const Dictionary config = p_config;
+ Array names = config.keys();
+ names.sort(); // Ensure ID order
+ for (int i = 0; i < names.size(); i++) {
+ ERR_CONTINUE(names[i].get_type() != Variant::STRING);
+ String name = names[i].operator String();
+ ERR_CONTINUE(config[name].get_type() != Variant::DICTIONARY);
+ ERR_CONTINUE(!config[name].operator Dictionary().has("rpc_mode"));
+ Dictionary dict = config[name];
+ RPCConfig cfg;
+ cfg.name = name;
+ cfg.rpc_mode = ((MultiplayerAPI::RPCMode)dict.get("rpc_mode", MultiplayerAPI::RPC_MODE_AUTHORITY).operator int());
+ cfg.transfer_mode = ((MultiplayerPeer::TransferMode)dict.get("transfer_mode", MultiplayerPeer::TRANSFER_MODE_RELIABLE).operator int());
+ cfg.call_local = dict.get("call_local", false).operator bool();
+ cfg.channel = dict.get("channel", 0).operator int();
+ uint16_t id = ((uint16_t)i);
+ if (p_for_node) {
+ id |= (1 << 15);
+ }
+ r_cache.configs[id] = cfg;
+ r_cache.ids[name] = id;
+ }
+}
+
+const SceneRPCInterface::RPCConfigCache &SceneRPCInterface::_get_node_config(const Node *p_node) {
+ const ObjectID oid = p_node->get_instance_id();
+ if (rpc_cache.has(oid)) {
+ return rpc_cache[oid];
+ }
+ RPCConfigCache cache;
+ _parse_rpc_config(p_node->get_node_rpc_config(), true, cache);
+ if (p_node->get_script_instance()) {
+ _parse_rpc_config(p_node->get_script_instance()->get_rpc_config(), false, cache);
+ }
+ rpc_cache[oid] = cache;
+ return rpc_cache[oid];
+}
+
+_FORCE_INLINE_ bool _can_call_mode(Node *p_node, MultiplayerAPI::RPCMode mode, int p_remote_id) {
+ switch (mode) {
+ case MultiplayerAPI::RPC_MODE_DISABLED: {
+ return false;
+ } break;
+ case MultiplayerAPI::RPC_MODE_ANY_PEER: {
+ return true;
+ } break;
+ case MultiplayerAPI::RPC_MODE_AUTHORITY: {
+ return !p_node->is_multiplayer_authority() && p_remote_id == p_node->get_multiplayer_authority();
+ } break;
+ }
+
+ return false;
+}
+
+String SceneRPCInterface::get_rpc_md5(const Object *p_obj) {
+ const Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V(!node, "");
+ const RPCConfigCache cache = _get_node_config(node);
+ String rpc_list;
+ for (const KeyValue<uint16_t, RPCConfig> &config : cache.configs) {
+ rpc_list += String(config.value.name);
+ }
+ return rpc_list.md5_text();
+}
+
+Node *SceneRPCInterface::_process_get_node(int p_from, const uint8_t *p_packet, uint32_t p_node_target, int p_packet_len) {
+ Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
+ ERR_FAIL_COND_V(!root_node, nullptr);
+ Node *node = nullptr;
+
+ if (p_node_target & 0x80000000) {
+ // Use full path (not cached yet).
+ int ofs = p_node_target & 0x7FFFFFFF;
+
+ ERR_FAIL_COND_V_MSG(ofs >= p_packet_len, nullptr, "Invalid packet received. Size smaller than declared.");
+
+ String paths;
+ paths.parse_utf8((const char *)&p_packet[ofs], p_packet_len - ofs);
+
+ NodePath np = paths;
+
+ node = root_node->get_node(np);
+
+ if (!node) {
+ ERR_PRINT("Failed to get path from RPC: " + String(np) + ".");
+ }
+ return node;
+ } else {
+ // Use cached path.
+ return Object::cast_to<Node>(multiplayer->get_path_cache()->get_cached_object(p_from, p_node_target));
+ }
+}
+
+void SceneRPCInterface::process_rpc(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ // Extract packet meta
+ int packet_min_size = 1;
+ int name_id_offset = 1;
+ ERR_FAIL_COND_MSG(p_packet_len < packet_min_size, "Invalid packet received. Size too small.");
+ // Compute the meta size, which depends on the compression level.
+ int node_id_compression = (p_packet[0] & NODE_ID_COMPRESSION_FLAG) >> NODE_ID_COMPRESSION_SHIFT;
+ int name_id_compression = (p_packet[0] & NAME_ID_COMPRESSION_FLAG) >> NAME_ID_COMPRESSION_SHIFT;
+
+ switch (node_id_compression) {
+ case NETWORK_NODE_ID_COMPRESSION_8:
+ packet_min_size += 1;
+ name_id_offset += 1;
+ break;
+ case NETWORK_NODE_ID_COMPRESSION_16:
+ packet_min_size += 2;
+ name_id_offset += 2;
+ break;
+ case NETWORK_NODE_ID_COMPRESSION_32:
+ packet_min_size += 4;
+ name_id_offset += 4;
+ break;
+ default:
+ ERR_FAIL_MSG("Was not possible to extract the node id compression mode.");
+ }
+ switch (name_id_compression) {
+ case NETWORK_NAME_ID_COMPRESSION_8:
+ packet_min_size += 1;
+ break;
+ case NETWORK_NAME_ID_COMPRESSION_16:
+ packet_min_size += 2;
+ break;
+ default:
+ ERR_FAIL_MSG("Was not possible to extract the name id compression mode.");
+ }
+ ERR_FAIL_COND_MSG(p_packet_len < packet_min_size, "Invalid packet received. Size too small.");
+
+ uint32_t node_target = 0;
+ switch (node_id_compression) {
+ case NETWORK_NODE_ID_COMPRESSION_8:
+ node_target = p_packet[1];
+ break;
+ case NETWORK_NODE_ID_COMPRESSION_16:
+ node_target = decode_uint16(p_packet + 1);
+ break;
+ case NETWORK_NODE_ID_COMPRESSION_32:
+ node_target = decode_uint32(p_packet + 1);
+ break;
+ default:
+ // Unreachable, checked before.
+ CRASH_NOW();
+ }
+
+ Node *node = _process_get_node(p_from, p_packet, node_target, p_packet_len);
+ ERR_FAIL_COND_MSG(node == nullptr, "Invalid packet received. Requested node was not found.");
+
+ uint16_t name_id = 0;
+ switch (name_id_compression) {
+ case NETWORK_NAME_ID_COMPRESSION_8:
+ name_id = p_packet[name_id_offset];
+ break;
+ case NETWORK_NAME_ID_COMPRESSION_16:
+ name_id = decode_uint16(p_packet + name_id_offset);
+ break;
+ default:
+ // Unreachable, checked before.
+ CRASH_NOW();
+ }
+
+ const int packet_len = get_packet_len(node_target, p_packet_len);
+ _process_rpc(node, name_id, p_from, p_packet, packet_len, packet_min_size);
+}
+
+void SceneRPCInterface::_process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset) {
+ ERR_FAIL_COND_MSG(p_offset > p_packet_len, "Invalid packet received. Size too small.");
+
+ // Check that remote can call the RPC on this node.
+ const RPCConfigCache &cache_config = _get_node_config(p_node);
+ ERR_FAIL_COND(!cache_config.configs.has(p_rpc_method_id));
+ const RPCConfig &config = cache_config.configs[p_rpc_method_id];
+
+ bool can_call = _can_call_mode(p_node, config.rpc_mode, p_from);
+ ERR_FAIL_COND_MSG(!can_call, "RPC '" + String(config.name) + "' is not allowed on node " + p_node->get_path() + " from: " + itos(p_from) + ". Mode is " + itos((int)config.rpc_mode) + ", authority is " + itos(p_node->get_multiplayer_authority()) + ".");
+
+ int argc = 0;
+
+ const bool byte_only_or_no_args = p_packet[0] & BYTE_ONLY_OR_NO_ARGS_FLAG;
+ if (byte_only_or_no_args) {
+ if (p_offset < p_packet_len) {
+ // This packet contains only bytes.
+ argc = 1;
+ }
+ } else {
+ // Normal variant, takes the argument count from the packet.
+ ERR_FAIL_COND_MSG(p_offset >= p_packet_len, "Invalid packet received. Size too small.");
+ argc = p_packet[p_offset];
+ p_offset += 1;
+ }
+
+ Vector<Variant> args;
+ Vector<const Variant *> argp;
+ args.resize(argc);
+ argp.resize(argc);
+
+#ifdef DEBUG_ENABLED
+ _profile_node_data("rpc_in", p_node->get_instance_id());
+#endif
+
+ int out;
+ MultiplayerAPI::decode_and_decompress_variants(args, &p_packet[p_offset], p_packet_len - p_offset, out, byte_only_or_no_args, multiplayer->is_object_decoding_allowed());
+ for (int i = 0; i < argc; i++) {
+ argp.write[i] = &args[i];
+ }
+
+ Callable::CallError ce;
+
+ p_node->callp(config.name, (const Variant **)argp.ptr(), argc, ce);
+ if (ce.error != Callable::CallError::CALL_OK) {
+ String error = Variant::get_call_error_text(p_node, config.name, (const Variant **)argp.ptr(), argc, ce);
+ error = "RPC - " + error;
+ ERR_PRINT(error);
+ }
+}
+
+void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount) {
+ Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
+ ERR_FAIL_COND_MSG(peer.is_null(), "Attempt to call RPC without active multiplayer peer.");
+
+ ERR_FAIL_COND_MSG(peer->get_connection_status() == MultiplayerPeer::CONNECTION_CONNECTING, "Attempt to call RPC while multiplayer peer is not connected yet.");
+
+ ERR_FAIL_COND_MSG(peer->get_connection_status() == MultiplayerPeer::CONNECTION_DISCONNECTED, "Attempt to call RPC while multiplayer peer is disconnected.");
+
+ ERR_FAIL_COND_MSG(p_argcount > 255, "Too many arguments (>255).");
+
+ if (p_to != 0 && !multiplayer->get_connected_peers().has(ABS(p_to))) {
+ ERR_FAIL_COND_MSG(p_to == multiplayer->get_unique_id(), "Attempt to call RPC on yourself! Peer unique ID: " + itos(multiplayer->get_unique_id()) + ".");
+
+ ERR_FAIL_MSG("Attempt to call RPC with unknown peer ID: " + itos(p_to) + ".");
+ }
+
+ // See if all peers have cached path (if so, call can be fast).
+ int psc_id;
+ const bool has_all_peers = multiplayer->get_path_cache()->send_object_cache(p_from, p_to, psc_id);
+
+ // Create base packet, lots of hardcode because it must be tight.
+
+ int ofs = 0;
+
+#define MAKE_ROOM(m_amount) \
+ if (packet_cache.size() < m_amount) \
+ packet_cache.resize(m_amount);
+
+ // Encode meta.
+ uint8_t command_type = SceneMultiplayer::NETWORK_COMMAND_REMOTE_CALL;
+ uint8_t node_id_compression = UINT8_MAX;
+ uint8_t name_id_compression = UINT8_MAX;
+ bool byte_only_or_no_args = false;
+
+ MAKE_ROOM(1);
+ // The meta is composed along the way, so just set 0 for now.
+ packet_cache.write[0] = 0;
+ ofs += 1;
+
+ // Encode Node ID.
+ if (has_all_peers) {
+ // Compress the node ID only if all the target peers already know it.
+ if (psc_id >= 0 && psc_id <= 255) {
+ // We can encode the id in 1 byte
+ node_id_compression = NETWORK_NODE_ID_COMPRESSION_8;
+ MAKE_ROOM(ofs + 1);
+ packet_cache.write[ofs] = static_cast<uint8_t>(psc_id);
+ ofs += 1;
+ } else if (psc_id >= 0 && psc_id <= 65535) {
+ // We can encode the id in 2 bytes
+ node_id_compression = NETWORK_NODE_ID_COMPRESSION_16;
+ MAKE_ROOM(ofs + 2);
+ encode_uint16(static_cast<uint16_t>(psc_id), &(packet_cache.write[ofs]));
+ ofs += 2;
+ } else {
+ // Too big, let's use 4 bytes.
+ node_id_compression = NETWORK_NODE_ID_COMPRESSION_32;
+ MAKE_ROOM(ofs + 4);
+ encode_uint32(psc_id, &(packet_cache.write[ofs]));
+ ofs += 4;
+ }
+ } else {
+ // The targets don't know the node yet, so we need to use 32 bits int.
+ node_id_compression = NETWORK_NODE_ID_COMPRESSION_32;
+ MAKE_ROOM(ofs + 4);
+ encode_uint32(psc_id, &(packet_cache.write[ofs]));
+ ofs += 4;
+ }
+
+ // Encode method ID
+ if (p_rpc_id <= UINT8_MAX) {
+ // The ID fits in 1 byte
+ name_id_compression = NETWORK_NAME_ID_COMPRESSION_8;
+ MAKE_ROOM(ofs + 1);
+ packet_cache.write[ofs] = static_cast<uint8_t>(p_rpc_id);
+ ofs += 1;
+ } else {
+ // The ID is larger, let's use 2 bytes
+ name_id_compression = NETWORK_NAME_ID_COMPRESSION_16;
+ MAKE_ROOM(ofs + 2);
+ encode_uint16(p_rpc_id, &(packet_cache.write[ofs]));
+ ofs += 2;
+ }
+
+ int len;
+ Error err = MultiplayerAPI::encode_and_compress_variants(p_arg, p_argcount, nullptr, len, &byte_only_or_no_args, multiplayer->is_object_decoding_allowed());
+ ERR_FAIL_COND_MSG(err != OK, "Unable to encode RPC arguments. THIS IS LIKELY A BUG IN THE ENGINE!");
+ if (byte_only_or_no_args) {
+ MAKE_ROOM(ofs + len);
+ } else {
+ MAKE_ROOM(ofs + 1 + len);
+ packet_cache.write[ofs] = p_argcount;
+ ofs += 1;
+ }
+ if (len) {
+ MultiplayerAPI::encode_and_compress_variants(p_arg, p_argcount, &packet_cache.write[ofs], len, &byte_only_or_no_args, multiplayer->is_object_decoding_allowed());
+ ofs += len;
+ }
+
+ ERR_FAIL_COND(command_type > 7);
+ ERR_FAIL_COND(node_id_compression > 3);
+ ERR_FAIL_COND(name_id_compression > 1);
+
+ // We can now set the meta
+ packet_cache.write[0] = command_type + (node_id_compression << NODE_ID_COMPRESSION_SHIFT) + (name_id_compression << NAME_ID_COMPRESSION_SHIFT) + (byte_only_or_no_args ? BYTE_ONLY_OR_NO_ARGS_FLAG : 0);
+
+#ifdef DEBUG_ENABLED
+ multiplayer->profile_bandwidth("out", ofs);
+#endif
+
+ // Take chance and set transfer mode, since all send methods will use it.
+ peer->set_transfer_channel(p_config.channel);
+ peer->set_transfer_mode(p_config.transfer_mode);
+
+ if (has_all_peers) {
+ // They all have verified paths, so send fast.
+ peer->set_target_peer(p_to); // To all of you.
+ peer->put_packet(packet_cache.ptr(), ofs); // A message with love.
+ } else {
+ // Unreachable because the node ID is never compressed if the peers doesn't know it.
+ CRASH_COND(node_id_compression != NETWORK_NODE_ID_COMPRESSION_32);
+
+ // Not all verified path, so send one by one.
+
+ // Append path at the end, since we will need it for some packets.
+ NodePath from_path = multiplayer->get_root_path().rel_path_to(p_from->get_path());
+ CharString pname = String(from_path).utf8();
+ int path_len = encode_cstring(pname.get_data(), nullptr);
+ MAKE_ROOM(ofs + path_len);
+ encode_cstring(pname.get_data(), &(packet_cache.write[ofs]));
+
+ for (const int &P : multiplayer->get_connected_peers()) {
+ if (p_to < 0 && P == -p_to) {
+ continue; // Continue, excluded.
+ }
+
+ if (p_to > 0 && P != p_to) {
+ continue; // Continue, not for this peer.
+ }
+
+ bool confirmed = multiplayer->get_path_cache()->is_cache_confirmed(from_path, P);
+
+ peer->set_target_peer(P); // To this one specifically.
+
+ if (confirmed) {
+ // This one confirmed path, so use id.
+ encode_uint32(psc_id, &(packet_cache.write[1]));
+ peer->put_packet(packet_cache.ptr(), ofs);
+ } else {
+ // This one did not confirm path yet, so use entire path (sorry!).
+ encode_uint32(0x80000000 | ofs, &(packet_cache.write[1])); // Offset to path and flag.
+ peer->put_packet(packet_cache.ptr(), ofs + path_len);
+ }
+ }
+ }
+}
+
+Error SceneRPCInterface::rpcp(Object *p_obj, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount) {
+ Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
+ ERR_FAIL_COND_V_MSG(!peer.is_valid(), ERR_UNCONFIGURED, "Trying to call an RPC while no multiplayer peer is active.");
+ Node *node = Object::cast_to<Node>(p_obj);
+ ERR_FAIL_COND_V_MSG(!node || !node->is_inside_tree(), ERR_INVALID_PARAMETER, "The object must be a valid Node inside the SceneTree");
+ ERR_FAIL_COND_V_MSG(peer->get_connection_status() != MultiplayerPeer::CONNECTION_CONNECTED, ERR_CONNECTION_ERROR, "Trying to call an RPC via a multiplayer peer which is not connected.");
+
+ int caller_id = multiplayer->get_unique_id();
+ bool call_local_native = false;
+ bool call_local_script = false;
+ const RPCConfigCache &config_cache = _get_node_config(node);
+ uint16_t rpc_id = config_cache.ids.has(p_method) ? config_cache.ids[p_method] : UINT16_MAX;
+ ERR_FAIL_COND_V_MSG(rpc_id == UINT16_MAX, ERR_INVALID_PARAMETER,
+ vformat("Unable to get the RPC configuration for the function \"%s\" at path: \"%s\". This happens when the method is missing or not marked for RPCs in the local script.", p_method, node->get_path()));
+ const RPCConfig &config = config_cache.configs[rpc_id];
+
+ ERR_FAIL_COND_V_MSG(p_peer_id == caller_id && !config.call_local, ERR_INVALID_PARAMETER, "RPC '" + p_method + "' on yourself is not allowed by selected mode.");
+
+ if (p_peer_id == 0 || p_peer_id == caller_id || (p_peer_id < 0 && p_peer_id != -caller_id)) {
+ if (rpc_id & (1 << 15)) {
+ call_local_native = config.call_local;
+ } else {
+ call_local_script = config.call_local;
+ }
+ }
+
+ if (p_peer_id != caller_id) {
+#ifdef DEBUG_ENABLED
+ _profile_node_data("rpc_out", node->get_instance_id());
+#endif
+
+ _send_rpc(node, p_peer_id, rpc_id, config, p_method, p_arg, p_argcount);
+ }
+
+ if (call_local_native) {
+ Callable::CallError ce;
+
+ multiplayer->set_remote_sender_override(multiplayer->get_unique_id());
+ node->callp(p_method, p_arg, p_argcount, ce);
+ multiplayer->set_remote_sender_override(0);
+
+ if (ce.error != Callable::CallError::CALL_OK) {
+ String error = Variant::get_call_error_text(node, p_method, p_arg, p_argcount, ce);
+ error = "rpc() aborted in local call: - " + error + ".";
+ ERR_PRINT(error);
+ return FAILED;
+ }
+ }
+
+ if (call_local_script) {
+ Callable::CallError ce;
+ ce.error = Callable::CallError::CALL_OK;
+
+ multiplayer->set_remote_sender_override(multiplayer->get_unique_id());
+ node->get_script_instance()->callp(p_method, p_arg, p_argcount, ce);
+ multiplayer->set_remote_sender_override(0);
+
+ if (ce.error != Callable::CallError::CALL_OK) {
+ String error = Variant::get_call_error_text(node, p_method, p_arg, p_argcount, ce);
+ error = "rpc() aborted in script local call: - " + error + ".";
+ ERR_PRINT(error);
+ return FAILED;
+ }
+ }
+ return OK;
+}
diff --git a/modules/multiplayer/scene_rpc_interface.h b/modules/multiplayer/scene_rpc_interface.h
new file mode 100644
index 0000000000..aa9be525a2
--- /dev/null
+++ b/modules/multiplayer/scene_rpc_interface.h
@@ -0,0 +1,102 @@
+/*************************************************************************/
+/* scene_rpc_interface.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef SCENE_RPC_INTERFACE_H
+#define SCENE_RPC_INTERFACE_H
+
+#include "core/object/ref_counted.h"
+#include "scene/main/multiplayer_api.h"
+
+class SceneMultiplayer;
+class Node;
+
+class SceneRPCInterface : public RefCounted {
+ GDCLASS(SceneRPCInterface, RefCounted);
+
+private:
+ struct RPCConfig {
+ StringName name;
+ MultiplayerAPI::RPCMode rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED;
+ bool call_local = false;
+ MultiplayerPeer::TransferMode transfer_mode = MultiplayerPeer::TRANSFER_MODE_RELIABLE;
+ int channel = 0;
+
+ bool operator==(RPCConfig const &p_other) const {
+ return name == p_other.name;
+ }
+ };
+
+ struct RPCConfigCache {
+ HashMap<uint16_t, RPCConfig> configs;
+ HashMap<StringName, uint16_t> ids;
+ };
+
+ struct SortRPCConfig {
+ StringName::AlphCompare compare;
+ bool operator()(const RPCConfig &p_a, const RPCConfig &p_b) const {
+ return compare(p_a.name, p_b.name);
+ }
+ };
+
+ enum NetworkNodeIdCompression {
+ NETWORK_NODE_ID_COMPRESSION_8 = 0,
+ NETWORK_NODE_ID_COMPRESSION_16,
+ NETWORK_NODE_ID_COMPRESSION_32,
+ };
+
+ enum NetworkNameIdCompression {
+ NETWORK_NAME_ID_COMPRESSION_8 = 0,
+ NETWORK_NAME_ID_COMPRESSION_16,
+ };
+
+ SceneMultiplayer *multiplayer = nullptr;
+ Vector<uint8_t> packet_cache;
+
+ HashMap<ObjectID, RPCConfigCache> rpc_cache;
+
+protected:
+ _FORCE_INLINE_ void _profile_node_data(const String &p_what, ObjectID p_id);
+ void _process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset);
+
+ void _send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount);
+ Node *_process_get_node(int p_from, const uint8_t *p_packet, uint32_t p_node_target, int p_packet_len);
+
+ void _parse_rpc_config(const Variant &p_config, bool p_for_node, RPCConfigCache &r_cache);
+ const RPCConfigCache &_get_node_config(const Node *p_node);
+
+public:
+ Error rpcp(Object *p_obj, int p_peer_id, const StringName &p_method, const Variant **p_arg, int p_argcount);
+ void process_rpc(int p_from, const uint8_t *p_packet, int p_packet_len);
+ String get_rpc_md5(const Object *p_obj);
+
+ SceneRPCInterface(SceneMultiplayer *p_multiplayer) { multiplayer = p_multiplayer; }
+};
+
+#endif // SCENE_RPC_INTERFACE_H