Ver código fonte

Merge pull request #62961 from Faless/mp/4.x_interest

Add peer visibility to MultiplayerSynchronizer.
Fabio Alessandrelli 3 anos atrás
pai
commit
1cf7ebda50

+ 6 - 2
core/multiplayer/multiplayer_api.cpp

@@ -463,8 +463,12 @@ bool MultiplayerAPI::is_cache_confirmed(NodePath p_path, int p_peer) {
 	return cache->is_cache_confirmed(p_path, p_peer);
 }
 
-bool MultiplayerAPI::send_object_cache(Object *p_obj, NodePath p_path, int p_peer_id, int &r_id) {
-	return cache->send_object_cache(p_obj, p_path, p_peer_id, r_id);
+bool MultiplayerAPI::send_object_cache(Object *p_obj, int p_peer_id, int &r_id) {
+	return cache->send_object_cache(p_obj, p_peer_id, r_id);
+}
+
+int MultiplayerAPI::make_object_cache(Object *p_obj) {
+	return cache->make_object_cache(p_obj);
 }
 
 Object *MultiplayerAPI::get_cached_object(int p_from, uint32_t p_cache_id) {

+ 4 - 2
core/multiplayer/multiplayer_api.h

@@ -77,7 +77,8 @@ public:
 	virtual void process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len) {}
 
 	// Returns true if all peers have cached path.
-	virtual bool send_object_cache(Object *p_obj, NodePath p_path, int p_target, int &p_id) { return false; }
+	virtual bool send_object_cache(Object *p_obj, int p_target, int &r_id) { return false; }
+	virtual int make_object_cache(Object *p_obj) { return false; }
 	virtual Object *get_cached_object(int p_from, uint32_t p_cache_id) { return nullptr; }
 	virtual bool is_cache_confirmed(NodePath p_path, int p_peer) { return false; }
 
@@ -160,7 +161,8 @@ public:
 	Error replication_start(Object *p_object, Variant p_config);
 	Error replication_stop(Object *p_object, Variant p_config);
 	// Cache API
-	bool send_object_cache(Object *p_obj, NodePath p_path, int p_target, int &p_id);
+	bool send_object_cache(Object *p_obj, int p_target, int &r_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);
 

+ 0 - 2
doc/classes/MultiplayerSpawner.xml

@@ -43,8 +43,6 @@
 		</method>
 	</methods>
 	<members>
-		<member name="auto_spawn" type="bool" setter="set_auto_spawning" getter="is_auto_spawning" default="false">
-		</member>
 		<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;)">

+ 52 - 0
doc/classes/MultiplayerSynchronizer.xml

@@ -6,12 +6,64 @@
 	</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>

+ 2 - 15
scene/multiplayer/multiplayer_spawner.cpp

@@ -71,7 +71,7 @@ bool MultiplayerSpawner::_get(const StringName &p_name, Variant &r_ret) const {
 }
 
 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, "Scenes,scenes/"));
+	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;
@@ -144,10 +144,6 @@ void MultiplayerSpawner::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_spawn_limit", "limit"), &MultiplayerSpawner::set_spawn_limit);
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "spawn_limit", PROPERTY_HINT_RANGE, "0,1024,1,or_greater"), "set_spawn_limit", "get_spawn_limit");
 
-	ClassDB::bind_method(D_METHOD("set_auto_spawning", "enabled"), &MultiplayerSpawner::set_auto_spawning);
-	ClassDB::bind_method(D_METHOD("is_auto_spawning"), &MultiplayerSpawner::is_auto_spawning);
-	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_spawn"), "set_auto_spawning", "is_auto_spawning");
-
 	GDVIRTUAL_BIND(_spawn_custom, "data");
 
 	ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
@@ -169,7 +165,7 @@ void MultiplayerSpawner::_update_spawn_node() {
 	Node *node = spawn_path.is_empty() && is_inside_tree() ? nullptr : get_node_or_null(spawn_path);
 	if (node) {
 		spawn_node = node->get_instance_id();
-		if (auto_spawn) {
+		if (get_spawnable_scene_count() && !GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom)) {
 			node->connect("child_entered_tree", callable_mp(this, &MultiplayerSpawner::_node_added));
 		}
 	} else {
@@ -221,15 +217,6 @@ void MultiplayerSpawner::_node_added(Node *p_node) {
 	_track(p_node, Variant(), id);
 }
 
-void MultiplayerSpawner::set_auto_spawning(bool p_enabled) {
-	auto_spawn = p_enabled;
-	_update_spawn_node();
-}
-
-bool MultiplayerSpawner::is_auto_spawning() const {
-	return auto_spawn;
-}
-
 NodePath MultiplayerSpawner::get_spawn_path() const {
 	return spawn_path;
 }

+ 0 - 3
scene/multiplayer/multiplayer_spawner.h

@@ -69,7 +69,6 @@ private:
 
 	ObjectID spawn_node;
 	HashMap<ObjectID, SpawnInfo> tracked_nodes;
-	bool auto_spawn = false;
 	uint32_t spawn_limit = 0;
 
 	void _update_spawn_node();
@@ -102,8 +101,6 @@ public:
 	void set_spawn_path(const NodePath &p_path);
 	uint32_t get_spawn_limit() const { return spawn_limit; }
 	void set_spawn_limit(uint32_t p_limit) { spawn_limit = p_limit; }
-	bool is_auto_spawning() const;
-	void set_auto_spawning(bool p_enabled);
 
 	const Variant get_spawn_argument(const ObjectID &p_id) const;
 	int find_spawnable_scene_index_from_object(const ObjectID &p_id) const;

+ 141 - 1
scene/multiplayer/multiplayer_synchronizer.cpp

@@ -43,6 +43,11 @@ Object *MultiplayerSynchronizer::_get_prop_target(Object *p_obj, const NodePath
 }
 
 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()->replication_stop(node, this);
@@ -50,9 +55,42 @@ void MultiplayerSynchronizer::_stop() {
 }
 
 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()->replication_start(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;
 	}
 }
 
@@ -85,6 +123,66 @@ Error MultiplayerSynchronizer::set_state(const List<NodePath> &p_properties, Obj
 	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);
@@ -95,9 +193,29 @@ void MultiplayerSynchronizer::_bind_methods() {
 	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"), "set_replication_config", "get_replication_config");
+	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) {
@@ -118,6 +236,11 @@ void MultiplayerSynchronizer::_notification(int p_what) {
 		case NOTIFICATION_EXIT_TREE: {
 			_stop();
 		} break;
+
+		case NOTIFICATION_INTERNAL_PROCESS:
+		case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+			update_visibility(0);
+		} break;
 	}
 }
 
@@ -142,6 +265,18 @@ 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;
@@ -162,3 +297,8 @@ void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_re
 	Node::set_multiplayer_authority(p_peer_id, p_recursive);
 	get_multiplayer()->replication_start(node, this);
 }
+
+MultiplayerSynchronizer::MultiplayerSynchronizer() {
+	// Publicly visible by default.
+	peer_visibility.insert(0);
+}

+ 24 - 1
scene/multiplayer/multiplayer_synchronizer.h

@@ -38,14 +38,25 @@
 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();
@@ -66,7 +77,19 @@ public:
 	NodePath get_root_path() const;
 	virtual void set_multiplayer_authority(int p_peer_id, bool p_recursive = true) override;
 
-	MultiplayerSynchronizer() {}
+	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

+ 18 - 7
scene/multiplayer/scene_cache_interface.cpp

@@ -187,18 +187,29 @@ bool SceneCacheInterface::is_cache_confirmed(NodePath p_path, int p_peer) {
 	return F->value;
 }
 
-bool SceneCacheInterface::send_object_cache(Object *p_obj, NodePath p_path, int p_peer_id, int &r_id) {
+int SceneCacheInterface::make_object_cache(Object *p_obj) {
 	Node *node = Object::cast_to<Node>(p_obj);
-	ERR_FAIL_COND_V(!node, false);
+	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(p_path);
+	PathSentCache *psc = path_send_cache.getptr(for_path);
 	if (!psc) {
 		// Path is not cached, create.
-		path_send_cache[p_path] = PathSentCache();
-		psc = path_send_cache.getptr(p_path);
+		path_send_cache[for_path] = PathSentCache();
+		psc = path_send_cache.getptr(for_path);
 		psc->id = last_send_cache_id++;
 	}
-	r_id = psc->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.
@@ -233,7 +244,7 @@ bool SceneCacheInterface::send_object_cache(Object *p_obj, NodePath p_path, int
 	}
 
 	if (peers_to_add.size()) {
-		_send_confirm_path(node, p_path, psc, peers_to_add);
+		_send_confirm_path(node, for_path, psc, peers_to_add);
 	}
 
 	return has_all_peers;

+ 2 - 1
scene/multiplayer/scene_cache_interface.h

@@ -72,7 +72,8 @@ public:
 	virtual void process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len) override;
 
 	// Returns true if all peers have cached path.
-	virtual bool send_object_cache(Object *p_obj, NodePath p_path, int p_target, int &p_id) override;
+	virtual bool send_object_cache(Object *p_obj, int p_target, int &p_id) override;
+	virtual int make_object_cache(Object *p_obj) override;
 	virtual Object *get_cached_object(int p_from, uint32_t p_cache_id) override;
 	virtual bool is_cache_confirmed(NodePath p_path, int p_peer) override;
 

+ 162 - 48
scene/multiplayer/scene_replication_interface.cpp

@@ -60,14 +60,13 @@ void SceneReplicationInterface::on_peer_change(int p_id, bool p_connected) {
 	if (p_connected) {
 		rep_state->on_peer_change(p_id, p_connected);
 		for (const ObjectID &oid : rep_state->get_spawned_nodes()) {
-			_send_spawn(rep_state->get_node(oid), rep_state->get_spawner(oid), p_id);
+			_update_spawn_visibility(p_id, oid);
 		}
-		for (const ObjectID &oid : rep_state->get_path_only_nodes()) {
-			Node *node = rep_state->get_node(oid);
+		for (const ObjectID &oid : rep_state->get_synced_nodes()) {
 			MultiplayerSynchronizer *sync = rep_state->get_synchronizer(oid);
-			ERR_CONTINUE(!node || !sync);
+			ERR_CONTINUE(!sync); // ERR_BUG
 			if (sync->is_multiplayer_authority()) {
-				rep_state->peer_add_node(p_id, oid);
+				_update_sync_visibility(p_id, oid);
 			}
 		}
 	} else {
@@ -97,7 +96,13 @@ Error SceneReplicationInterface::on_spawn(Object *p_obj, Variant p_config) {
 	ERR_FAIL_COND_V(!spawner, ERR_INVALID_PARAMETER);
 	Error err = rep_state->config_add_spawn(node, spawner);
 	ERR_FAIL_COND_V(err != OK, err);
-	return _send_spawn(node, spawner, 0);
+	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) {
@@ -105,9 +110,19 @@ Error SceneReplicationInterface::on_despawn(Object *p_obj, Variant p_config) {
 	ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
 	MultiplayerSpawner *spawner = Object::cast_to<MultiplayerSpawner>(p_config.get_validated_object());
 	ERR_FAIL_COND_V(!p_obj || !spawner, ERR_INVALID_PARAMETER);
-	Error err = rep_state->config_del_spawn(node, spawner);
-	ERR_FAIL_COND_V(err != OK, err);
-	return _send_despawn(node, 0);
+	// 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) {
@@ -115,7 +130,15 @@ Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_c
 	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.
@@ -127,9 +150,6 @@ Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_c
 		ERR_FAIL_COND_V(err, err);
 		err = MultiplayerSynchronizer::set_state(props, node, vars);
 		ERR_FAIL_COND_V(err, err);
-	} else if (multiplayer->has_multiplayer_peer() && sync->is_multiplayer_authority()) {
-		// Either it's a spawn or a static sync, in any case add it to the list of known nodes.
-		rep_state->peer_add_node(0, p_obj->get_instance_id());
 	}
 	return OK;
 }
@@ -138,10 +158,103 @@ Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_co
 	Node *node = Object::cast_to<Node>(p_obj);
 	ERR_FAIL_COND_V(!node || p_config.get_type() != Variant::OBJECT, ERR_INVALID_PARAMETER);
 	MultiplayerSynchronizer *sync = Object::cast_to<MultiplayerSynchronizer>(p_config.get_validated_object());
-	ERR_FAIL_COND_V(!p_obj || !sync, ERR_INVALID_PARAMETER);
+	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->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);
@@ -158,18 +271,20 @@ Error SceneReplicationInterface::_send_raw(const uint8_t *p_buffer, int p_size,
 	return peer->put_packet(p_buffer, p_size);
 }
 
-Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer) {
-	ERR_FAIL_COND_V(p_peer < 0, ERR_BUG);
+Error SceneReplicationInterface::_make_spawn_packet(Node *p_node, int &r_len) {
 	ERR_FAIL_COND_V(!multiplayer, ERR_BUG);
-	ERR_FAIL_COND_V(!p_spawner || !p_node, ERR_BUG);
 
 	const ObjectID oid = p_node->get_instance_id();
-	uint32_t nid = rep_state->ensure_net_id(oid);
+	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 = p_spawner->find_spawnable_scene_index_from_object(oid);
+	uint8_t scene_id = spawner->find_spawnable_scene_index_from_object(oid);
 	bool is_custom = scene_id == MultiplayerSpawner::INVALID_ID;
-	Variant spawn_arg = p_spawner->get_spawn_argument(oid);
+	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);
@@ -181,7 +296,8 @@ Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p
 	Vector<Variant> state_vars;
 	Vector<const Variant *> state_varp;
 	MultiplayerSynchronizer *synchronizer = rep_state->get_synchronizer(oid);
-	if (synchronizer && synchronizer->get_replication_config().is_valid()) {
+	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.");
@@ -189,13 +305,8 @@ Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p
 		ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to encode spawn state.");
 	}
 
-	// Prepare simplified path.
-	NodePath rel_path = multiplayer->get_root_path().rel_path_to(p_spawner->get_path());
-
-	int path_id = 0;
-	multiplayer->send_object_cache(p_spawner, rel_path, p_peer, path_id);
-
-	// Encode name and parent ID.
+	// Encode scene ID, path ID, net ID, node name.
+	int path_id = multiplayer->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);
@@ -220,12 +331,11 @@ Error SceneReplicationInterface::_send_spawn(Node *p_node, MultiplayerSpawner *p
 		ERR_FAIL_COND_V(err, err);
 		ofs += state_size;
 	}
-	Error err = _send_raw(ptr, ofs, p_peer, true);
-	ERR_FAIL_COND_V(err, err);
-	return rep_state->peer_add_node(p_peer, oid);
+	r_len = ofs;
+	return OK;
 }
 
-Error SceneReplicationInterface::_send_despawn(Node *p_node, int p_peer) {
+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();
@@ -233,9 +343,8 @@ Error SceneReplicationInterface::_send_despawn(Node *p_node, int p_peer) {
 	int ofs = 1;
 	uint32_t nid = rep_state->get_net_id(oid);
 	ofs += encode_uint32(nid, &ptr[ofs]);
-	Error err = _send_raw(ptr, ofs, p_peer, true);
-	ERR_FAIL_COND_V(err, err);
-	return rep_state->peer_del_node(p_peer, oid);
+	r_len = ofs;
+	return OK;
 }
 
 Error SceneReplicationInterface::on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
@@ -316,8 +425,8 @@ Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p
 }
 
 void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) {
-	const HashSet<ObjectID> &known = rep_state->get_known_nodes(p_peer);
-	if (known.is_empty()) {
+	const HashSet<ObjectID> &to_sync = rep_state->get_peer_sync_nodes(p_peer);
+	if (to_sync.is_empty()) {
 		return;
 	}
 	MAKE_ROOM(sync_mtu);
@@ -327,14 +436,29 @@ void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) {
 	ofs += encode_uint16(rep_state->peer_sync_next(p_peer), &ptr[1]);
 	// Can only send updates for already notified nodes.
 	// This is a lazy implementation, we could optimize much more here with by grouping by replication config.
-	for (const ObjectID &oid : known) {
+	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);
+		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->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;
@@ -351,16 +475,6 @@ void SceneReplicationInterface::_send_sync(int p_peer, uint64_t p_msec) {
 			ofs = 3;
 		}
 		if (size) {
-			uint32_t net_id = rep_state->get_net_id(oid);
-			if (net_id == 0 || (net_id & 0x80000000)) {
-				// First time path based ID.
-				NodePath rel_path = multiplayer->get_root_path().rel_path_to(sync->get_path());
-				int path_id = 0;
-				multiplayer->send_object_cache(sync, rel_path, p_peer, path_id);
-				ERR_CONTINUE_MSG(net_id && net_id != (uint32_t(path_id) | 0x80000000), "This should never happen!");
-				net_id = path_id;
-				rep_state->set_net_id(oid, net_id | 0x80000000);
-			}
 			ofs += encode_uint32(rep_state->get_net_id(oid), &ptr[ofs]);
 			ofs += encode_uint32(size, &ptr[ofs]);
 			MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), &ptr[ofs], size);

+ 5 - 2
scene/multiplayer/scene_replication_interface.h

@@ -40,10 +40,13 @@ class SceneReplicationInterface : public MultiplayerReplicationInterface {
 
 private:
 	void _send_sync(int p_peer, uint64_t p_msec);
-	Error _send_spawn(Node *p_node, MultiplayerSpawner *p_spawner, int p_peer);
-	Error _send_despawn(Node *p_node, int p_peer);
+	Error _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;

+ 47 - 34
scene/multiplayer/scene_replication_state.cpp

@@ -56,7 +56,8 @@ void SceneReplicationState::_untrack(const ObjectID &p_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.known_nodes.erase(p_id);
+				E.value.sync_nodes.erase(p_id);
+				E.value.spawn_nodes.erase(p_id);
 			}
 		}
 	}
@@ -93,11 +94,6 @@ bool SceneReplicationState::update_sync_time(const ObjectID &p_id, uint64_t p_ms
 	return false;
 }
 
-const HashSet<ObjectID> SceneReplicationState::get_known_nodes(int p_peer) {
-	ERR_FAIL_COND_V(!peers_info.has(p_peer), HashSet<ObjectID>());
-	return peers_info[p_peer].known_nodes;
-}
-
 uint32_t SceneReplicationState::get_net_id(const ObjectID &p_id) const {
 	const TrackedNode *tnode = tracked_nodes.getptr(p_id);
 	ERR_FAIL_COND_V(!tnode, 0);
@@ -147,8 +143,6 @@ Error SceneReplicationState::config_add_spawn(Node *p_node, MultiplayerSpawner *
 	ERR_FAIL_COND_V(tobj.spawner != ObjectID(), ERR_ALREADY_IN_USE);
 	tobj.spawner = p_spawner->get_instance_id();
 	spawned_nodes.insert(oid);
-	// The spawner may be notified after the synchronizer.
-	path_only_nodes.erase(oid);
 	return OK;
 }
 
@@ -159,6 +153,9 @@ Error SceneReplicationState::config_del_spawn(Node *p_node, MultiplayerSpawner *
 	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;
 }
 
@@ -167,10 +164,7 @@ Error SceneReplicationState::config_add_sync(Node *p_node, MultiplayerSynchroniz
 	TrackedNode &tobj = _track(oid);
 	ERR_FAIL_COND_V(tobj.synchronizer != ObjectID(), ERR_ALREADY_IN_USE);
 	tobj.synchronizer = p_sync->get_instance_id();
-	// If it doesn't have a spawner, we might need to assign ID for this node using it's path.
-	if (tobj.spawner.is_null()) {
-		path_only_nodes.insert(oid);
-	}
+	synced_nodes.insert(oid);
 	return OK;
 }
 
@@ -180,38 +174,57 @@ Error SceneReplicationState::config_del_sync(Node *p_node, MultiplayerSynchroniz
 	TrackedNode &tobj = _track(oid);
 	ERR_FAIL_COND_V(tobj.synchronizer != p_sync->get_instance_id(), ERR_INVALID_PARAMETER);
 	tobj.synchronizer = ObjectID();
-	if (path_only_nodes.has(oid)) {
-		p_node->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneReplicationState::_untrack));
-		_untrack(oid);
-		path_only_nodes.erase(oid);
+	synced_nodes.erase(oid);
+	for (KeyValue<int, PeerInfo> &E : peers_info) {
+		E.value.sync_nodes.erase(oid);
 	}
 	return OK;
 }
 
-Error SceneReplicationState::peer_add_node(int p_peer, const ObjectID &p_id) {
-	if (p_peer) {
-		ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
-		peers_info[p_peer].known_nodes.insert(p_id);
-	} else {
-		for (KeyValue<int, PeerInfo> &E : peers_info) {
-			E.value.known_nodes.insert(p_id);
-		}
-	}
+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_node(int p_peer, const ObjectID &p_id) {
-	if (p_peer) {
-		ERR_FAIL_COND_V(!peers_info.has(p_peer), ERR_INVALID_PARAMETER);
-		peers_info[p_peer].known_nodes.erase(p_id);
-	} else {
-		for (KeyValue<int, PeerInfo> &E : peers_info) {
-			E.value.known_nodes.erase(p_id);
-		}
-	}
+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;

+ 17 - 6
scene/multiplayer/scene_replication_state.h

@@ -62,7 +62,8 @@ private:
 	};
 
 	struct PeerInfo {
-		HashSet<ObjectID> known_nodes;
+		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;
@@ -73,7 +74,7 @@ private:
 	HashMap<ObjectID, TrackedNode> tracked_nodes;
 	HashMap<int, PeerInfo> peers_info;
 	HashSet<ObjectID> spawned_nodes;
-	HashSet<ObjectID> path_only_nodes;
+	HashSet<ObjectID> synced_nodes;
 
 	TrackedNode &_track(const ObjectID &p_id);
 	void _untrack(const ObjectID &p_id);
@@ -82,7 +83,9 @@ private:
 public:
 	const HashSet<int> get_peers() const { return known_peers; }
 	const HashSet<ObjectID> &get_spawned_nodes() const { return spawned_nodes; }
-	const HashSet<ObjectID> &get_path_only_nodes() const { return path_only_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; }
@@ -90,7 +93,6 @@ public:
 	bool update_last_node_sync(const ObjectID &p_id, uint16_t p_time);
 	bool update_sync_time(const ObjectID &p_id, uint64_t p_msec);
 
-	const HashSet<ObjectID> get_known_nodes(int p_peer);
 	uint32_t get_net_id(const ObjectID &p_id) const;
 	void set_net_id(const ObjectID &p_id, uint32_t p_net_id);
 	uint32_t ensure_net_id(const ObjectID &p_id);
@@ -104,8 +106,17 @@ public:
 	Error config_add_sync(Node *p_node, MultiplayerSynchronizer *p_sync);
 	Error config_del_sync(Node *p_node, MultiplayerSynchronizer *p_sync);
 
-	Error peer_add_node(int p_peer, const ObjectID &p_id);
-	Error peer_del_node(int p_peer, const ObjectID &p_id);
+	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);

+ 2 - 4
scene/multiplayer/scene_rpc_interface.cpp

@@ -302,12 +302,9 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 		ERR_FAIL_MSG("Attempt to call RPC with unknown peer ID: " + itos(p_to) + ".");
 	}
 
-	NodePath from_path = multiplayer->get_root_path().rel_path_to(p_from->get_path());
-	ERR_FAIL_COND_MSG(from_path.is_empty(), "Unable to send RPC. Relative path is empty. THIS IS LIKELY A BUG IN THE ENGINE!");
-
 	// See if all peers have cached path (if so, call can be fast).
 	int psc_id;
-	const bool has_all_peers = multiplayer->send_object_cache(p_from, from_path, p_to, psc_id);
+	const bool has_all_peers = multiplayer->send_object_cache(p_from, p_to, psc_id);
 
 	// Create base packet, lots of hardcode because it must be tight.
 
@@ -414,6 +411,7 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 		// 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);