[Net] MultiplayerReplicator with initial state.

Move the former "spawnables" functions to a dedicated
MultiplayerReplicator class.
Support custom overrides in replicator.
Spawn/despawn messages can now contain a state.
The state can be automatically encoded/decoded by passing the desired
object properties to `spawnable_config`.
You can use script properties to optimize the state representation.
2 Callables can be also specified to completely override the default
implementation for sending and receiving the spawn/despawn event.
(9 bytes overhead, and there's room for improvement here).
When using a custom implementation `spawn` and `despawn` can be called
with any Object, `send_spawn`/`send_despawn` can receive any Variant as
a state, and the path is not required.

Two new functions, `spawn` and `despawn`, convey the implementation
independent method for requesting a spawn/despawn of an Object, while
`send_spawn` and `send_despawn` represent the more low-level send event
for a Variant to be used by the custom implementations.
This commit is contained in:
Fabio Alessandrelli 2021-08-09 22:46:23 +02:00
parent a031579286
commit d4dd859991
8 changed files with 849 additions and 351 deletions

View file

@ -32,15 +32,11 @@
#include "core/debugger/engine_debugger.h"
#include "core/io/marshalls.h"
#include "core/io/multiplayer_replicator.h"
#include "scene/main/node.h"
#include "scene/resources/packed_scene.h"
#include <stdint.h>
#define NODE_ID_COMPRESSION_SHIFT 3
#define NAME_ID_COMPRESSION_SHIFT 5
#define BYTE_ONLY_OR_NO_ARGS_SHIFT 6
#ifdef DEBUG_ENABLED
#include "core/os/os.h"
#endif
@ -147,7 +143,7 @@ void MultiplayerAPI::poll() {
}
void MultiplayerAPI::clear() {
replicated_nodes.clear();
replicator->clear();
connected_peers.clear();
path_get_cache.clear();
path_send_cache.clear();
@ -325,10 +321,10 @@ void MultiplayerAPI::_process_packet(int p_from, const uint8_t *p_packet, int p_
_process_raw(p_from, p_packet, p_packet_len);
} break;
case NETWORK_COMMAND_SPAWN: {
_process_spawn_despawn(p_from, p_packet, p_packet_len, true);
replicator->process_spawn_despawn(p_from, p_packet, p_packet_len, true);
} break;
case NETWORK_COMMAND_DESPAWN: {
_process_spawn_despawn(p_from, p_packet, p_packet_len, false);
replicator->process_spawn_despawn(p_from, p_packet, p_packet_len, false);
} break;
}
}
@ -338,7 +334,6 @@ Node *MultiplayerAPI::_process_get_node(int p_from, const uint8_t *p_packet, uin
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.");
@ -353,25 +348,11 @@ Node *MultiplayerAPI::_process_get_node(int p_from, const uint8_t *p_packet, uin
if (!node) {
ERR_PRINT("Failed to get path from RPC: " + String(np) + ".");
}
return node;
} else {
// Use cached path.
int id = p_node_target;
Map<int, PathGetCache>::Element *E = path_get_cache.find(p_from);
ERR_FAIL_COND_V_MSG(!E, nullptr, "Invalid packet received. Requests invalid peer cache.");
Map<int, PathGetCache::NodeInfo>::Element *F = E->get().nodes.find(id);
ERR_FAIL_COND_V_MSG(!F, nullptr, "Invalid packet received. Unabled to find requested cached node.");
PathGetCache::NodeInfo *ni = &F->get();
// Do proper caching later.
node = root_node->get_node(ni->path);
if (!node) {
ERR_PRINT("Failed to get cached path from RPC: " + String(ni->path) + ".");
}
return get_cached_node(p_from, p_node_target);
}
return node;
}
void MultiplayerAPI::_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) {
@ -425,7 +406,7 @@ void MultiplayerAPI::_process_rpc(Node *p_node, const uint16_t p_rpc_method_id,
ERR_FAIL_COND_MSG(p_offset >= p_packet_len, "Invalid packet received. Size too small.");
int vlen;
Error err = _decode_and_decompress_variant(args.write[i], &p_packet[p_offset], p_packet_len - p_offset, &vlen);
Error err = decode_and_decompress_variant(args.write[i], &p_packet[p_offset], p_packet_len - p_offset, &vlen);
ERR_FAIL_COND_MSG(err != OK, "Invalid packet received. Unable to decode RPC argument.");
argp.write[i] = &args[i];
@ -514,64 +495,6 @@ void MultiplayerAPI::_process_confirm_path(int p_from, const uint8_t *p_packet,
E->get() = true;
}
void MultiplayerAPI::_process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn) {
ERR_FAIL_COND_MSG(p_packet_len < 18, "Invalid spawn packet received");
int ofs = 1;
ResourceUID::ID id = decode_uint64(&p_packet[ofs]);
ofs += 8;
ERR_FAIL_COND_MSG(!spawnables.has(id), "Invalid spawn ID received " + itos(id));
uint32_t node_target = decode_uint32(&p_packet[ofs]);
Node *parent = _process_get_node(p_from, nullptr, node_target, 0);
ofs += 4;
ERR_FAIL_COND_MSG(parent == nullptr, "Invalid packet received. Requested node was not found.");
uint32_t name_len = decode_uint32(&p_packet[ofs]);
ofs += 4;
ERR_FAIL_COND_MSG(name_len > uint32_t(p_packet_len - ofs), vformat("Invalid spawn packet size: %d, wants: %d", p_packet_len, ofs + name_len));
ERR_FAIL_COND_MSG(name_len < 1, "Zero spawn name size.");
const String name = String::utf8((const char *)&p_packet[ofs], name_len);
// We need to make sure no trickery happens here (e.g. despawning a subpath), but we want to allow autogenerated ("@") node names.
ERR_FAIL_COND_MSG(name.validate_node_name() != name.replace("@", ""), vformat("Invalid node name received: '%s'", name));
ofs += name_len;
PackedByteArray data;
if (p_packet_len > ofs) {
data.resize(p_packet_len - ofs);
memcpy(data.ptrw(), &p_packet[ofs], data.size());
}
SpawnMode mode = spawnables[id];
if (mode == SPAWN_MODE_SERVER && p_from == 1) {
String scene_path = ResourceUID::get_singleton()->get_id_path(id);
if (p_spawn) {
ERR_FAIL_COND_MSG(parent->has_node(name), vformat("Unable to spawn node. Node already exists: %s/%s", parent->get_path(), name));
RES res = ResourceLoader::load(scene_path);
ERR_FAIL_COND_MSG(!res.is_valid(), "Unable to load scene to spawn at path: " + scene_path);
PackedScene *scene = Object::cast_to<PackedScene>(res.ptr());
ERR_FAIL_COND(!scene);
Node *node = scene->instantiate();
ERR_FAIL_COND(!node);
replicated_nodes[node->get_instance_id()] = id;
parent->_add_child_nocheck(node, name);
emit_signal(SNAME("network_spawn"), p_from, id, node, data);
} else {
ERR_FAIL_COND_MSG(!parent->has_node(name), vformat("Path not found: %s/%s", parent->get_path(), name));
Node *node = parent->get_node(name);
ERR_FAIL_COND_MSG(!replicated_nodes.has(node->get_instance_id()), vformat("Trying to despawn a Node that was not replicated: %s/%s", parent->get_path(), name));
emit_signal(SNAME("network_despawn"), p_from, id, node, data);
replicated_nodes.erase(node->get_instance_id());
node->queue_delete();
}
} else {
if (p_spawn) {
emit_signal(SNAME("network_spawn_request"), p_from, id, parent, name, data);
} else {
emit_signal(SNAME("network_despawn_request"), p_from, id, parent, name, data);
}
}
}
bool MultiplayerAPI::_send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, int p_target) {
bool has_all_peers = true;
List<int> peers_to_add; // If one is missing, take note to add it.
@ -647,7 +570,7 @@ bool MultiplayerAPI::_send_confirm_path(Node *p_node, NodePath p_path, PathSentC
#define ENCODE_16 1 << 5
#define ENCODE_32 2 << 5
#define ENCODE_64 3 << 5
Error MultiplayerAPI::_encode_and_compress_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len) {
Error MultiplayerAPI::encode_and_compress_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len) {
// Unreachable because `VARIANT_MAX` == 27 and `ENCODE_VARIANT_MASK` == 31
CRASH_COND(p_variant.get_type() > VARIANT_META_TYPE_MASK);
@ -722,7 +645,7 @@ Error MultiplayerAPI::_encode_and_compress_variant(const Variant &p_variant, uin
return OK;
}
Error MultiplayerAPI::_decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len) {
Error MultiplayerAPI::decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len) {
const uint8_t *buf = p_buffer;
int len = p_len;
@ -906,10 +829,10 @@ void MultiplayerAPI::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const
ofs += 1;
for (int i = 0; i < p_argcount; i++) {
int len(0);
Error err = _encode_and_compress_variant(*p_arg[i], nullptr, len);
Error err = encode_and_compress_variant(*p_arg[i], nullptr, len);
ERR_FAIL_COND_MSG(err != OK, "Unable to encode RPC argument. THIS IS LIKELY A BUG IN THE ENGINE!");
MAKE_ROOM(ofs + len);
_encode_and_compress_variant(*p_arg[i], &(packet_cache.write[ofs]), len);
encode_and_compress_variant(*p_arg[i], &(packet_cache.write[ofs]), len);
ofs += len;
}
}
@ -976,14 +899,7 @@ void MultiplayerAPI::_add_peer(int p_id) {
connected_peers.insert(p_id);
path_get_cache.insert(p_id, PathGetCache());
if (is_network_server()) {
for (const KeyValue<ObjectID, ResourceUID::ID> &E : replicated_nodes) {
// Only server mode adds to replicated_nodes, no need to check it.
Object *obj = ObjectDB::get_instance(E.key);
ERR_CONTINUE(!obj);
Node *node = Object::cast_to<Node>(obj);
ERR_CONTINUE(!node);
_send_spawn_despawn(p_id, E.value, node->get_path(), nullptr, 0, true);
}
replicator->spawn_all(p_id);
}
emit_signal(SNAME("network_peer_connected"), p_id);
}
@ -1000,10 +916,6 @@ void MultiplayerAPI::_del_peer(int p_id) {
PathSentCache *psc = path_send_cache.getptr(E);
psc->confirmed_peers.erase(p_id);
}
if (p_id == 1) {
// Erase server replicated nodes, but do not queue them for deletion.
replicated_nodes.clear();
}
emit_signal(SNAME("network_peer_disconnected"), p_id);
}
@ -1109,6 +1021,36 @@ void MultiplayerAPI::_process_raw(int p_from, const uint8_t *p_packet, int p_pac
emit_signal(SNAME("network_peer_packet"), p_from, out);
}
bool MultiplayerAPI::send_confirm_path(Node *p_node, NodePath p_path, int p_peer_id, int &r_id) {
// See if the path is cached.
PathSentCache *psc = path_send_cache.getptr(p_path);
if (!psc) {
// Path is not cached, create.
path_send_cache[p_path] = PathSentCache();
psc = path_send_cache.getptr(p_path);
psc->id = last_send_cache_id++;
}
r_id = psc->id;
// See if all peers have cached path (if so, call can be fast).
return _send_confirm_path(p_node, p_path, psc, p_peer_id);
}
Node *MultiplayerAPI::get_cached_node(int p_from, uint32_t p_node_id) {
Map<int, PathGetCache>::Element *E = path_get_cache.find(p_from);
ERR_FAIL_COND_V_MSG(!E, nullptr, vformat("No cache found for peer %d.", p_from));
Map<int, PathGetCache::NodeInfo>::Element *F = E->get().nodes.find(p_node_id);
ERR_FAIL_COND_V_MSG(!F, nullptr, vformat("ID %d not found in cache of peer %d.", p_node_id, p_from));
PathGetCache::NodeInfo *ni = &F->get();
Node *node = root_node->get_node(ni->path);
if (!node) {
ERR_PRINT("Failed to get cached path: " + String(ni->path) + ".");
}
return node;
}
int MultiplayerAPI::get_network_unique_id() const {
ERR_FAIL_COND_V_MSG(!network_peer.is_valid(), 0, "No network peer is assigned. Unable to get unique network ID.");
return network_peer->get_unique_id();
@ -1147,97 +1089,12 @@ bool MultiplayerAPI::is_object_decoding_allowed() const {
return allow_object_decoding;
}
Error MultiplayerAPI::spawnable_config(const ResourceUID::ID &p_id, SpawnMode p_mode) {
ERR_FAIL_COND_V(p_mode < SPAWN_MODE_NONE || p_mode > SPAWN_MODE_CUSTOM, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER);
#ifdef TOOLS_ENABLED
String path = ResourceUID::get_singleton()->get_id_path(p_id);
RES res = ResourceLoader::load(path);
ERR_FAIL_COND_V(!res->is_class("PackedScene"), ERR_INVALID_PARAMETER);
#endif
if (p_mode == SPAWN_MODE_NONE) {
if (spawnables.has(p_id)) {
spawnables.erase(p_id);
}
} else {
spawnables[p_id] = p_mode;
}
return OK;
MultiplayerReplicator *MultiplayerAPI::get_replicator() const {
return replicator;
}
Error MultiplayerAPI::send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data) {
return _send_spawn_despawn(p_peer_id, p_scene_id, p_path, p_data.ptr(), p_data.size(), false);
}
Error MultiplayerAPI::send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data) {
return _send_spawn_despawn(p_peer_id, p_scene_id, p_path, p_data.ptr(), p_data.size(), true);
}
Error MultiplayerAPI::_send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const uint8_t *p_data, int p_data_len, bool p_spawn) {
ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED);
ERR_FAIL_COND_V_MSG(!spawnables.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
NodePath rel_path = (root_node->get_path()).rel_path_to(p_path);
const Vector<StringName> names = rel_path.get_names();
ERR_FAIL_COND_V(names.size() < 2, ERR_INVALID_PARAMETER);
NodePath parent = NodePath(names.subarray(0, names.size() - 2), false);
ERR_FAIL_COND_V_MSG(!root_node->has_node(parent), ERR_INVALID_PARAMETER, "Path not found: " + parent);
// See if the path is cached.
PathSentCache *psc = path_send_cache.getptr(parent);
if (!psc) {
// Path is not cached, create.
path_send_cache[parent] = PathSentCache();
psc = path_send_cache.getptr(parent);
psc->id = last_send_cache_id++;
}
_send_confirm_path(root_node->get_node(parent), parent, psc, p_peer_id);
const CharString cname = String(names[names.size() - 1]).utf8();
int nlen = encode_cstring(cname.get_data(), nullptr);
MAKE_ROOM(1 + 8 + 4 + 4 + nlen + p_data_len);
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = p_spawn ? NETWORK_COMMAND_SPAWN : NETWORK_COMMAND_DESPAWN;
int ofs = 1;
ofs += encode_uint64(p_scene_id, &ptr[ofs]);
ofs += encode_uint32(psc->id, &ptr[ofs]);
ofs += encode_uint32(nlen, &ptr[ofs]);
ofs += encode_cstring(cname.get_data(), &ptr[ofs]);
memcpy(&ptr[ofs], p_data, p_data_len);
network_peer->set_target_peer(p_peer_id);
network_peer->set_transfer_channel(0);
network_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
return network_peer->put_packet(ptr, ofs + p_data_len);
}
void MultiplayerAPI::scene_enter_exit_notify(const String &p_scene, const Node *p_node, bool p_enter) {
if (!has_network_peer()) {
return;
}
ERR_FAIL_COND(!p_node || !p_node->get_parent() || !root_node);
NodePath path = (root_node->get_path()).rel_path_to(p_node->get_parent()->get_path());
if (path.is_empty()) {
return;
}
ResourceUID::ID id = ResourceLoader::get_resource_uid(p_scene);
if (!spawnables.has(id)) {
return;
}
SpawnMode mode = spawnables[id];
if (p_enter) {
if (mode == SPAWN_MODE_SERVER && is_network_server()) {
replicated_nodes[p_node->get_instance_id()] = id;
_send_spawn_despawn(0, id, p_node->get_path(), nullptr, 0, true);
}
emit_signal(SNAME("network_spawnable_added"), id, p_node);
} else {
if (mode == SPAWN_MODE_SERVER && is_network_server() && replicated_nodes.has(p_node->get_instance_id())) {
replicated_nodes.erase(p_node->get_instance_id());
_send_spawn_despawn(0, id, p_node->get_path(), nullptr, 0, false);
}
emit_signal(SNAME("network_spawnable_removed"), id, p_node);
}
void MultiplayerAPI::scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter) {
replicator->scene_enter_exit_notify(p_scene, p_node, p_enter);
}
void MultiplayerAPI::_bind_methods() {
@ -1258,15 +1115,14 @@ void MultiplayerAPI::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_refusing_new_network_connections"), &MultiplayerAPI::is_refusing_new_network_connections);
ClassDB::bind_method(D_METHOD("set_allow_object_decoding", "enable"), &MultiplayerAPI::set_allow_object_decoding);
ClassDB::bind_method(D_METHOD("is_object_decoding_allowed"), &MultiplayerAPI::is_object_decoding_allowed);
ClassDB::bind_method(D_METHOD("spawnable_config", "scene_id", "spawn_mode"), &MultiplayerAPI::spawnable_config);
ClassDB::bind_method(D_METHOD("send_despawn", "peer_id", "scene_id", "path", "data"), &MultiplayerAPI::send_despawn, DEFVAL(PackedByteArray()));
ClassDB::bind_method(D_METHOD("send_spawn", "peer_id", "scene_id", "path", "data"), &MultiplayerAPI::send_spawn, DEFVAL(PackedByteArray()));
ClassDB::bind_method(D_METHOD("get_replicator"), &MultiplayerAPI::get_replicator);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_network_connections"), "set_refuse_new_network_connections", "is_refusing_new_network_connections");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "network_peer", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerPeer", PROPERTY_USAGE_NONE), "set_network_peer", "get_network_peer");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root_node", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_root_node", "get_root_node");
ADD_PROPERTY_DEFAULT("refuse_new_network_connections", false);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replicator", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerReplicator", PROPERTY_USAGE_NONE), "", "get_replicator");
ADD_SIGNAL(MethodInfo("network_peer_connected", PropertyInfo(Variant::INT, "id")));
ADD_SIGNAL(MethodInfo("network_peer_disconnected", PropertyInfo(Variant::INT, "id")));
@ -1274,27 +1130,19 @@ void MultiplayerAPI::_bind_methods() {
ADD_SIGNAL(MethodInfo("connected_to_server"));
ADD_SIGNAL(MethodInfo("connection_failed"));
ADD_SIGNAL(MethodInfo("server_disconnected"));
ADD_SIGNAL(MethodInfo("network_despawn", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("network_spawn", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("network_despawn_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("network_spawn_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("network_spawnable_added", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
ADD_SIGNAL(MethodInfo("network_spawnable_removed", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
BIND_ENUM_CONSTANT(RPC_MODE_DISABLED);
BIND_ENUM_CONSTANT(RPC_MODE_REMOTE);
BIND_ENUM_CONSTANT(RPC_MODE_MASTER);
BIND_ENUM_CONSTANT(RPC_MODE_PUPPET);
BIND_ENUM_CONSTANT(SPAWN_MODE_NONE);
BIND_ENUM_CONSTANT(SPAWN_MODE_SERVER);
BIND_ENUM_CONSTANT(SPAWN_MODE_CUSTOM);
}
MultiplayerAPI::MultiplayerAPI() {
replicator = memnew(MultiplayerReplicator(this));
clear();
}
MultiplayerAPI::~MultiplayerAPI() {
clear();
memdelete(replicator);
}

View file

@ -35,6 +35,8 @@
#include "core/io/resource_uid.h"
#include "core/object/ref_counted.h"
class MultiplayerReplicator;
class MultiplayerAPI : public RefCounted {
GDCLASS(MultiplayerAPI, RefCounted);
@ -46,12 +48,6 @@ public:
RPC_MODE_PUPPET, // Using rpc() on it will call method for all puppets
};
enum SpawnMode {
SPAWN_MODE_NONE,
SPAWN_MODE_SERVER,
SPAWN_MODE_CUSTOM,
};
struct RPCConfig {
StringName name;
RPCMode rpc_mode = RPC_MODE_DISABLED;
@ -71,53 +67,6 @@ public:
}
};
private:
//path sent caches
struct PathSentCache {
Map<int, bool> confirmed_peers;
int id;
};
//path get caches
struct PathGetCache {
struct NodeInfo {
NodePath path;
ObjectID instance;
};
Map<int, NodeInfo> nodes;
};
Ref<MultiplayerPeer> network_peer;
Map<ResourceUID::ID, SpawnMode> spawnables;
int rpc_sender_id = 0;
Set<int> connected_peers;
HashMap<NodePath, PathSentCache> path_send_cache;
Map<int, PathGetCache> path_get_cache;
Map<ObjectID, ResourceUID::ID> replicated_nodes;
int last_send_cache_id;
Vector<uint8_t> packet_cache;
Node *root_node = nullptr;
bool allow_object_decoding = false;
protected:
static void _bind_methods();
void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len);
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);
void _process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
Node *_process_get_node(int p_from, const uint8_t *p_packet, uint32_t p_node_target, int p_packet_len);
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 _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len);
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);
bool _send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, int p_target);
Error _encode_and_compress_variant(const Variant &p_variant, uint8_t *p_buffer, int &r_len);
Error _decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len);
public:
enum NetworkCommands {
NETWORK_COMMAND_REMOTE_CALL = 0,
NETWORK_COMMAND_SIMPLIFY_PATH,
@ -138,6 +87,54 @@ public:
NETWORK_NAME_ID_COMPRESSION_16,
};
enum {
NODE_ID_COMPRESSION_SHIFT = 3,
NAME_ID_COMPRESSION_SHIFT = 5,
BYTE_ONLY_OR_NO_ARGS_SHIFT = 6,
};
private:
//path sent caches
struct PathSentCache {
Map<int, bool> confirmed_peers;
int id;
};
//path get caches
struct PathGetCache {
struct NodeInfo {
NodePath path;
ObjectID instance;
};
Map<int, NodeInfo> nodes;
};
Ref<MultiplayerPeer> network_peer;
int rpc_sender_id = 0;
Set<int> connected_peers;
HashMap<NodePath, PathSentCache> path_send_cache;
Map<int, PathGetCache> path_get_cache;
int last_send_cache_id;
Vector<uint8_t> packet_cache;
Node *root_node = nullptr;
bool allow_object_decoding = false;
MultiplayerReplicator *replicator = nullptr;
protected:
static void _bind_methods();
void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len);
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);
Node *_process_get_node(int p_from, const uint8_t *p_packet, uint32_t p_node_target, int p_packet_len);
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 _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len);
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);
bool _send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, int p_target);
public:
void poll();
void clear();
void set_root_node(Node *p_node);
@ -146,8 +143,16 @@ public:
Ref<MultiplayerPeer> get_network_peer() 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);
Error encode_and_compress_variant(const Variant &p_variant, uint8_t *p_buffer, int &r_len);
Error decode_and_decompress_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int *r_len);
// Called by Node.rpc
void rpcp(Node *p_node, int p_peer_id, bool p_unreliable, const StringName &p_method, const Variant **p_arg, int p_argcount);
// Called by Node._notification
void scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter);
// Called by replicator
bool send_confirm_path(Node *p_node, NodePath p_path, int p_target, int &p_id);
Node *get_cached_node(int p_from, uint32_t p_node_id);
void _add_peer(int p_id);
void _del_peer(int p_id);
@ -166,17 +171,12 @@ public:
void set_allow_object_decoding(bool p_enable);
bool is_object_decoding_allowed() const;
Error spawnable_config(const ResourceUID::ID &p_id, SpawnMode p_mode);
Error send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data = PackedByteArray());
Error send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data = PackedByteArray());
Error _send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const uint8_t *p_data, int p_data_len, bool p_spawn);
void scene_enter_exit_notify(const String &p_scene, const Node *p_node, bool p_enter);
MultiplayerReplicator *get_replicator() const;
MultiplayerAPI();
~MultiplayerAPI();
};
VARIANT_ENUM_CAST(MultiplayerAPI::RPCMode);
VARIANT_ENUM_CAST(MultiplayerAPI::SpawnMode);
#endif // MULTIPLAYER_API_H

View file

@ -0,0 +1,497 @@
/*************************************************************************/
/* multiplayer_replicator.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "core/io/multiplayer_replicator.h"
#include "core/io/marshalls.h"
#include "scene/main/node.h"
#include "scene/resources/packed_scene.h"
#define MAKE_ROOM(m_amount) \
if (packet_cache.size() < m_amount) \
packet_cache.resize(m_amount);
Error MultiplayerReplicator::_send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn) {
ERR_FAIL_COND_V(p_spawn && !p_obj, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
Error err;
// Prepare state
List<Variant> state_variants;
int state_len = 0;
const SceneConfig &cfg = replications[p_scene_id];
if (p_spawn) {
if ((err = _get_state(cfg.properties, p_obj, state_variants)) != OK) {
return err;
}
}
bool is_raw = false;
if (state_variants.size() == 1 && state_variants[0].get_type() == Variant::PACKED_BYTE_ARRAY) {
is_raw = true;
} else if (state_variants.size()) {
err = _encode_state(state_variants, nullptr, state_len);
ERR_FAIL_COND_V(err, err);
} else {
is_raw = true;
}
int ofs = 0;
// Prepare simplified path
const Node *root_node = multiplayer->get_root_node();
ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED);
NodePath rel_path = (root_node->get_path()).rel_path_to(p_path);
const Vector<StringName> names = rel_path.get_names();
ERR_FAIL_COND_V(names.size() < 2, ERR_INVALID_PARAMETER);
NodePath parent = NodePath(names.subarray(0, names.size() - 2), false);
ERR_FAIL_COND_V_MSG(!root_node->has_node(parent), ERR_INVALID_PARAMETER, "Path not found: " + parent);
int path_id = 0;
multiplayer->send_confirm_path(root_node->get_node(parent), parent, p_peer_id, path_id);
// Encode name and parent ID.
CharString cname = String(names[names.size() - 1]).utf8();
int nlen = encode_cstring(cname.get_data(), nullptr);
MAKE_ROOM(SPAWN_CMD_OFFSET + 4 + 4 + nlen + state_len);
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = (p_spawn ? MultiplayerAPI::NETWORK_COMMAND_SPAWN : MultiplayerAPI::NETWORK_COMMAND_DESPAWN) + ((is_raw ? 1 : 0) << MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT);
ofs = 1;
ofs += encode_uint64(p_scene_id, &ptr[ofs]);
ofs += encode_uint32(path_id, &ptr[ofs]);
ofs += encode_uint32(nlen, &ptr[ofs]);
ofs += encode_cstring(cname.get_data(), &ptr[ofs]);
// Encode state.
if (!is_raw) {
_encode_state(state_variants, &ptr[ofs], state_len);
} else if (state_len) {
PackedByteArray pba = state_variants[0];
memcpy(&ptr[ofs], pba.ptr(), state_len);
}
Ref<MultiplayerPeer> network_peer = multiplayer->get_network_peer();
network_peer->set_target_peer(p_peer_id);
network_peer->set_transfer_channel(0);
network_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
return network_peer->put_packet(ptr, ofs + state_len);
}
void MultiplayerReplicator::_process_default_spawn_despawn(int p_from, const ResourceUID::ID &p_scene_id, const uint8_t *p_packet, int p_packet_len, bool p_spawn) {
ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET + 9, "Invalid spawn packet received");
int ofs = SPAWN_CMD_OFFSET;
uint32_t node_target = decode_uint32(&p_packet[ofs]);
Node *parent = multiplayer->get_cached_node(p_from, node_target);
ofs += 4;
ERR_FAIL_COND_MSG(parent == nullptr, "Invalid packet received. Requested node was not found.");
uint32_t name_len = decode_uint32(&p_packet[ofs]);
ofs += 4;
ERR_FAIL_COND_MSG(name_len > uint32_t(p_packet_len - ofs), vformat("Invalid spawn packet size: %d, wants: %d", p_packet_len, ofs + name_len));
ERR_FAIL_COND_MSG(name_len < 1, "Zero spawn name size.");
const String name = String::utf8((const char *)&p_packet[ofs], name_len);
// We need to make sure no trickery happens here (e.g. despawning a subpath), but we want to allow autogenerated ("@") node names.
ERR_FAIL_COND_MSG(name.validate_node_name() != name.replace("@", ""), vformat("Invalid node name received: '%s'", name));
ofs += name_len;
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.mode == REPLICATION_MODE_SERVER && p_from == 1) {
String scene_path = ResourceUID::get_singleton()->get_id_path(p_scene_id);
if (p_spawn) {
const bool is_raw = ((p_packet[0] & 64) >> MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT) == 1;
ERR_FAIL_COND_MSG(parent->has_node(name), vformat("Unable to spawn node. Node already exists: %s/%s", parent->get_path(), name));
RES res = ResourceLoader::load(scene_path);
ERR_FAIL_COND_MSG(!res.is_valid(), "Unable to load scene to spawn at path: " + scene_path);
PackedScene *scene = Object::cast_to<PackedScene>(res.ptr());
ERR_FAIL_COND(!scene);
Node *node = scene->instantiate();
ERR_FAIL_COND(!node);
replicated_nodes[node->get_instance_id()] = p_scene_id;
int size;
_decode_state(cfg.properties, node, &p_packet[ofs], p_packet_len - ofs, size, is_raw);
parent->_add_child_nocheck(node, name);
emit_signal(SNAME("spawned"), p_scene_id, node);
} else {
ERR_FAIL_COND_MSG(!parent->has_node(name), vformat("Path not found: %s/%s", parent->get_path(), name));
Node *node = parent->get_node(name);
ERR_FAIL_COND_MSG(!replicated_nodes.has(node->get_instance_id()), vformat("Trying to despawn a Node that was not replicated: %s/%s", parent->get_path(), name));
emit_signal(SNAME("despawned"), p_scene_id, node);
replicated_nodes.erase(node->get_instance_id());
node->queue_delete();
}
} else {
PackedByteArray data;
if (p_packet_len > ofs) {
data.resize(p_packet_len - ofs);
memcpy(data.ptrw(), &p_packet[ofs], data.size());
}
if (p_spawn) {
emit_signal(SNAME("spawn_requested"), p_from, p_scene_id, parent, name, data);
} else {
emit_signal(SNAME("despawn_requested"), p_from, p_scene_id, parent, name, data);
}
}
}
void MultiplayerReplicator::process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn) {
ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET, "Invalid spawn packet received");
ResourceUID::ID id = decode_uint64(&p_packet[1]);
ERR_FAIL_COND_MSG(!replications.has(id), "Invalid spawn ID received " + itos(id));
const SceneConfig &cfg = replications[id];
if (cfg.on_spawn_despawn_receive.is_valid()) {
int ofs = SPAWN_CMD_OFFSET;
bool is_raw = ((p_packet[0] & 64) >> MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT) == 1;
Variant data;
int left = p_packet_len - ofs;
if (is_raw && left) {
PackedByteArray pba;
pba.resize(left);
memcpy(pba.ptrw(), &p_packet[ofs], pba.size());
data = pba;
} else if (left) {
ERR_FAIL_COND(decode_variant(data, &p_packet[ofs], left) != OK);
}
Variant args[4];
args[0] = p_from;
args[1] = id;
args[2] = data;
args[3] = p_spawn;
const Variant *argp[] = { &args[0], &args[1], &args[2], &args[3] };
Callable::CallError ce;
Variant ret;
cfg.on_spawn_despawn_receive.call(argp, 4, ret, ce);
ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, "Custom receive function failed");
} else {
_process_default_spawn_despawn(p_from, id, p_packet, p_packet_len, p_spawn);
}
}
Error MultiplayerReplicator::_get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant) {
ERR_FAIL_COND_V_MSG(!p_obj, ERR_INVALID_PARAMETER, "Cannot encode null object");
for (const StringName &prop : p_properties) {
bool valid = false;
const Variant v = p_obj->get(prop, &valid);
ERR_FAIL_COND_V_MSG(!valid, ERR_INVALID_DATA, vformat("Property '%s' not found.", prop));
r_variant.push_back(v);
}
return OK;
}
Error MultiplayerReplicator::_encode_state(const List<Variant> &p_variants, uint8_t *p_buffer, int &r_len, bool *r_raw) {
r_len = 0;
int size = 0;
// Try raw encoding optimization.
if (r_raw && p_variants.size() == 1) {
*r_raw = false;
const Variant v = p_variants[0];
if (v.get_type() == Variant::PACKED_BYTE_ARRAY) {
*r_raw = true;
const PackedByteArray pba = v;
if (p_buffer) {
memcpy(p_buffer, pba.ptr(), pba.size());
}
r_len += pba.size();
} else {
multiplayer->encode_and_compress_variant(v, p_buffer, size);
r_len += size;
}
return OK;
}
// Regular encoding.
for (const Variant &v : p_variants) {
multiplayer->encode_and_compress_variant(v, p_buffer ? p_buffer + r_len : nullptr, size);
r_len += size;
}
return OK;
}
Error MultiplayerReplicator::_decode_state(const List<StringName> &p_properties, Object *p_obj, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw) {
r_len = 0;
int argc = p_properties.size();
if (argc == 0 && p_raw) {
ERR_FAIL_COND_V_MSG(p_len != 0, ERR_INVALID_DATA, "Buffer has trailing bytes.");
return OK;
}
ERR_FAIL_COND_V(p_raw && argc != 1, ERR_INVALID_DATA);
if (p_raw) {
r_len = p_len;
PackedByteArray pba;
pba.resize(p_len);
memcpy(pba.ptrw(), p_buffer, p_len);
p_obj->set(p_properties[0], pba);
return OK;
}
Vector<Variant> args;
Vector<const Variant *> argp;
args.resize(argc);
for (int i = 0; i < argc; i++) {
ERR_FAIL_COND_V_MSG(r_len >= p_len, ERR_INVALID_DATA, "Invalid packet received. Size too small.");
int vlen;
Error err = multiplayer->decode_and_decompress_variant(args.write[i], &p_buffer[r_len], p_len - r_len, &vlen);
ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid packet received. Unable to decode state variable.");
r_len += vlen;
}
ERR_FAIL_COND_V_MSG(p_len - r_len != 0, ERR_INVALID_DATA, "Buffer has trailing bytes.");
int i = 0;
for (const StringName &prop : p_properties) {
p_obj->set(prop, args[i]);
i += 1;
}
return OK;
}
Error MultiplayerReplicator::spawn_config(const ResourceUID::ID &p_id, ReplicationMode p_mode, const TypedArray<StringName> &p_props, const Callable &p_on_send, const Callable &p_on_recv) {
ERR_FAIL_COND_V(p_mode < REPLICATION_MODE_NONE || p_mode > REPLICATION_MODE_CUSTOM, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V_MSG(p_on_send.is_valid() != p_on_recv.is_valid(), ERR_INVALID_PARAMETER, "Send and receive custom callables must be both valid or both empty");
#ifdef TOOLS_ENABLED
if (!p_on_send.is_valid()) {
// We allow non scene spawning with custom callables.
String path = ResourceUID::get_singleton()->get_id_path(p_id);
RES res = ResourceLoader::load(path);
ERR_FAIL_COND_V(!res->is_class("PackedScene"), ERR_INVALID_PARAMETER);
}
#endif
if (p_mode == REPLICATION_MODE_NONE) {
if (replications.has(p_id)) {
replications.erase(p_id);
}
} else {
SceneConfig cfg;
cfg.mode = p_mode;
for (int i = 0; i < p_props.size(); i++) {
cfg.properties.push_back(StringName(p_props[i]));
}
cfg.on_spawn_despawn_send = p_on_send;
cfg.on_spawn_despawn_receive = p_on_recv;
replications[p_id] = cfg;
}
return OK;
}
Error MultiplayerReplicator::_send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn) {
int data_size = 0;
int is_raw = false;
if (p_data.get_type() == Variant::PACKED_BYTE_ARRAY) {
const PackedByteArray pba = p_data;
is_raw = true;
data_size = p_data.operator PackedByteArray().size();
} else if (p_data.get_type() == Variant::NIL) {
is_raw = true;
} else {
Error err = encode_variant(p_data, nullptr, data_size);
ERR_FAIL_COND_V(err, err);
}
MAKE_ROOM(SPAWN_CMD_OFFSET + data_size);
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = (p_spawn ? MultiplayerAPI::NETWORK_COMMAND_SPAWN : MultiplayerAPI::NETWORK_COMMAND_DESPAWN) + ((is_raw ? 1 : 0) << MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT);
encode_uint64(p_scene_id, &ptr[1]);
if (p_data.get_type() == Variant::PACKED_BYTE_ARRAY) {
const PackedByteArray pba = p_data;
memcpy(&ptr[SPAWN_CMD_OFFSET], pba.ptr(), pba.size());
} else if (data_size) {
encode_variant(p_data, &ptr[SPAWN_CMD_OFFSET], data_size);
}
Ref<MultiplayerPeer> network_peer = multiplayer->get_network_peer();
network_peer->set_target_peer(p_peer_id);
network_peer->set_transfer_channel(0);
network_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
return network_peer->put_packet(ptr, SPAWN_CMD_OFFSET + data_size);
}
Error MultiplayerReplicator::send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) {
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_spawn_despawn_send.is_valid()) {
return _send_spawn_despawn(p_peer_id, p_scene_id, p_data, true);
} else {
ERR_FAIL_COND_V_MSG(cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server(), ERR_UNAVAILABLE, "Manual despawn is restricted in default server mode implementation. Use custom mode if you desire control over server spawn requests.");
NodePath path = p_path;
Object *obj = p_data.get_type() == Variant::OBJECT ? p_data.get_validated_object() : nullptr;
if (path.is_empty() && obj) {
Node *node = Object::cast_to<Node>(obj);
if (node && node->is_inside_tree()) {
path = node->get_path();
}
}
ERR_FAIL_COND_V_MSG(path.is_empty(), ERR_INVALID_PARAMETER, "Despawn default implementation requires a despawn path, or the data to be a node inside the SceneTree");
return _send_default_spawn_despawn(p_peer_id, p_scene_id, obj, path, false);
}
}
Error MultiplayerReplicator::send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) {
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_spawn_despawn_send.is_valid()) {
return _send_spawn_despawn(p_peer_id, p_scene_id, p_data, false);
} else {
ERR_FAIL_COND_V_MSG(cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server(), ERR_UNAVAILABLE, "Manual spawn is restricted in default server mode implementation. Use custom mode if you desire control over server spawn requests.");
NodePath path = p_path;
Object *obj = p_data.get_type() == Variant::OBJECT ? p_data.get_validated_object() : nullptr;
ERR_FAIL_COND_V_MSG(!obj, ERR_INVALID_PARAMETER, "Spawn default implementation requires the data to be an object.");
if (path.is_empty()) {
Node *node = Object::cast_to<Node>(obj);
if (node && node->is_inside_tree()) {
path = node->get_path();
}
}
ERR_FAIL_COND_V_MSG(path.is_empty(), ERR_INVALID_PARAMETER, "Spawn default implementation requires a spawn path, or the data to be a node inside the SceneTree");
return _send_default_spawn_despawn(p_peer_id, p_scene_id, obj, path, true);
}
}
Error MultiplayerReplicator::_spawn_despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer, bool p_spawn) {
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_spawn_despawn_send.is_valid()) {
Variant args[4];
args[0] = p_peer;
args[1] = p_scene_id;
args[2] = p_obj;
args[3] = true;
const Variant *argp[] = { &args[0], &args[1], &args[2], &args[3] };
Callable::CallError ce;
Variant ret;
cfg.on_spawn_despawn_send.call(argp, 4, ret, ce);
ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, "Custom send function failed");
return OK;
} else {
Node *node = Object::cast_to<Node>(p_obj);
ERR_FAIL_COND_V_MSG(!p_obj, ERR_INVALID_PARAMETER, "Only nodes can be replicated by the default implementation");
return _send_default_spawn_despawn(p_peer, p_scene_id, node, node->get_path(), p_spawn);
}
}
Error MultiplayerReplicator::spawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer) {
return _spawn_despawn(p_scene_id, p_obj, p_peer, true);
}
Error MultiplayerReplicator::despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer) {
return _spawn_despawn(p_scene_id, p_obj, p_peer, false);
}
PackedByteArray MultiplayerReplicator::encode_state(const ResourceUID::ID &p_scene_id, const Object *p_obj) {
PackedByteArray state;
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), state, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
int len = 0;
List<Variant> state_vars;
Error err = _get_state(cfg.properties, p_obj, state_vars);
ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to retrieve object state.");
err = _encode_state(state_vars, nullptr, len);
ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to encode object state.");
state.resize(len);
_encode_state(state_vars, state.ptrw(), len);
return state;
}
Error MultiplayerReplicator::decode_state(const ResourceUID::ID &p_scene_id, Object *p_obj, const PackedByteArray p_data) {
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
int size;
return _decode_state(cfg.properties, p_obj, p_data.ptr(), p_data.size(), size);
}
void MultiplayerReplicator::scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter) {
if (!multiplayer->has_network_peer()) {
return;
}
Node *root_node = multiplayer->get_root_node();
ERR_FAIL_COND(!p_node || !p_node->get_parent() || !root_node);
NodePath path = (root_node->get_path()).rel_path_to(p_node->get_parent()->get_path());
if (path.is_empty()) {
return;
}
ResourceUID::ID id = ResourceLoader::get_resource_uid(p_scene);
if (!replications.has(id)) {
return;
}
const SceneConfig &cfg = replications[id];
if (p_enter) {
if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server()) {
replicated_nodes[p_node->get_instance_id()] = id;
spawn(id, p_node, 0);
}
emit_signal(SNAME("replicated_instance_added"), id, p_node);
} else {
if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server() && replicated_nodes.has(p_node->get_instance_id())) {
replicated_nodes.erase(p_node->get_instance_id());
despawn(id, p_node, 0);
}
emit_signal(SNAME("replicated_instance_removed"), id, p_node);
}
}
void MultiplayerReplicator::spawn_all(int p_peer) {
for (const KeyValue<ObjectID, ResourceUID::ID> &E : replicated_nodes) {
// Only server mode adds to replicated_nodes, no need to check it.
Object *obj = ObjectDB::get_instance(E.key);
ERR_CONTINUE(!obj);
Node *node = Object::cast_to<Node>(obj);
ERR_CONTINUE(!node);
spawn(E.value, node, p_peer);
}
}
void MultiplayerReplicator::clear() {
replicated_nodes.clear();
}
void MultiplayerReplicator::_bind_methods() {
ClassDB::bind_method(D_METHOD("spawn_config", "scene_id", "spawn_mode", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::spawn_config, DEFVAL(TypedArray<StringName>()), DEFVAL(Callable()), DEFVAL(Callable()));
ClassDB::bind_method(D_METHOD("despawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::despawn, DEFVAL(0));
ClassDB::bind_method(D_METHOD("spawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::spawn, DEFVAL(0));
ClassDB::bind_method(D_METHOD("send_despawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_despawn, DEFVAL(Variant()), DEFVAL(NodePath()));
ClassDB::bind_method(D_METHOD("send_spawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_spawn, DEFVAL(Variant()), DEFVAL(NodePath()));
ClassDB::bind_method(D_METHOD("encode_state", "scene_id", "object"), &MultiplayerReplicator::encode_state);
ClassDB::bind_method(D_METHOD("decode_state", "scene_id", "object", "data"), &MultiplayerReplicator::decode_state);
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")));
ADD_SIGNAL(MethodInfo("despawn_requested", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("spawn_requested", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
ADD_SIGNAL(MethodInfo("replicated_instance_added", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
ADD_SIGNAL(MethodInfo("replicated_instance_removed", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
BIND_ENUM_CONSTANT(REPLICATION_MODE_NONE);
BIND_ENUM_CONSTANT(REPLICATION_MODE_SERVER);
BIND_ENUM_CONSTANT(REPLICATION_MODE_CUSTOM);
}

View file

@ -0,0 +1,99 @@
/*************************************************************************/
/* multiplayer_replicator.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef MULTIPLAYER_REPLICATOR_H
#define MULTIPLAYER_REPLICATOR_H
#include "core/io/multiplayer_api.h"
#include "core/variant/typed_array.h"
class MultiplayerReplicator : public Object {
GDCLASS(MultiplayerReplicator, Object);
public:
enum {
SPAWN_CMD_OFFSET = 9,
};
enum ReplicationMode {
REPLICATION_MODE_NONE,
REPLICATION_MODE_SERVER,
REPLICATION_MODE_CUSTOM,
};
struct SceneConfig {
ReplicationMode mode;
List<StringName> properties;
Callable on_spawn_despawn_send;
Callable on_spawn_despawn_receive;
};
protected:
static void _bind_methods();
private:
MultiplayerAPI *multiplayer = nullptr;
Vector<uint8_t> packet_cache;
Map<ResourceUID::ID, SceneConfig> replications;
Map<ObjectID, ResourceUID::ID> replicated_nodes;
Error _encode_state(const List<Variant> &p_variants, uint8_t *p_buffer, int &r_len, bool *r_raw = nullptr);
Error _decode_state(const List<StringName> &p_cfg, Object *p_obj, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw = false);
Error _get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant);
Error _spawn_despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer, bool p_spawn);
Error _send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn);
void _process_default_spawn_despawn(int p_from, const ResourceUID::ID &p_scene_id, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
Error _send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn);
public:
void clear();
Error spawn_config(const ResourceUID::ID &p_id, ReplicationMode p_mode, const TypedArray<StringName> &p_props = TypedArray<StringName>(), const Callable &p_on_send = Callable(), const Callable &p_on_recv = Callable());
Error spawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0);
Error despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0);
Error send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath());
Error send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath());
PackedByteArray encode_state(const ResourceUID::ID &p_scene_id, const Object *p_node);
Error decode_state(const ResourceUID::ID &p_scene_id, Object *p_node, PackedByteArray p_data);
// Used by MultiplayerAPI
void spawn_all(int p_peer);
void process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
void scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter);
MultiplayerReplicator(MultiplayerAPI *p_multiplayer) {
multiplayer = p_multiplayer;
}
};
VARIANT_ENUM_CAST(MultiplayerReplicator::ReplicationMode);
#endif // MULTIPLAYER_REPLICATOR_H

View file

@ -49,6 +49,7 @@
#include "core/io/marshalls.h"
#include "core/io/multiplayer_api.h"
#include "core/io/multiplayer_peer.h"
#include "core/io/multiplayer_replicator.h"
#include "core/io/packed_data_container.h"
#include "core/io/packet_peer.h"
#include "core/io/packet_peer_dtls.h"
@ -193,6 +194,7 @@ void register_core_types() {
ResourceLoader::add_resource_format_loader(resource_format_loader_crypto);
GDREGISTER_VIRTUAL_CLASS(MultiplayerPeer);
GDREGISTER_VIRTUAL_CLASS(MultiplayerReplicator);
GDREGISTER_CLASS(MultiplayerAPI);
GDREGISTER_CLASS(MainLoop);
GDREGISTER_CLASS(Translation);

View file

@ -66,34 +66,6 @@
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>
<method name="send_despawn">
<return type="int" enum="Error" />
<argument index="0" name="peer_id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="path" type="NodePath" />
<argument index="3" name="data" type="PackedByteArray" default="PackedByteArray()" />
<description>
Sends a despawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant SPAWN_MODE_SERVER] (see [method spawnable_config]) and the request is sent by the server (see [method is_network_server]), the receiving peer(s) will automatically queue for deletion the node at [code]path[/code] and emit the signal [signal network_despawn]. In all other cases no deletion happens, and the signal [signal network_despawn_request] is emitted instead.
</description>
</method>
<method name="send_spawn">
<return type="int" enum="Error" />
<argument index="0" name="peer_id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="path" type="NodePath" />
<argument index="3" name="data" type="PackedByteArray" default="PackedByteArray()" />
<description>
Sends a spawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant SPAWN_MODE_SERVER] (see [method spawnable_config]) and the request is sent by the server (see [method is_network_server]), the receiving peer(s) will automatically instantiate that scene, add it to the [SceneTree] at the given [code]path[/code] and emit the signal [signal network_spawn]. In all other cases no instantiation happens, and the signal [signal network_spawn_request] is emitted instead.
</description>
</method>
<method name="spawnable_config">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="spawn_mode" type="int" enum="MultiplayerAPI.SpawnMode" />
<description>
Configures the MultiplayerAPI to track instances of the [PackedScene] idenfied by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. See [enum SpawnMode] for the possible configurations.
</description>
</method>
</methods>
<members>
<member name="allow_object_decoding" type="bool" setter="set_allow_object_decoding" getter="is_object_decoding_allowed" default="false">
@ -106,6 +78,8 @@
<member name="refuse_new_network_connections" type="bool" setter="set_refuse_new_network_connections" getter="is_refusing_new_network_connections" default="false">
If [code]true[/code], the MultiplayerAPI's [member network_peer] refuses new incoming connections.
</member>
<member name="replicator" type="MultiplayerReplicator" setter="" getter="get_replicator">
</member>
<member name="root_node" type="Node" setter="set_root_node" getter="get_root_node">
The root node to use for RPCs. 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.
@ -122,25 +96,6 @@
Emitted when this MultiplayerAPI's [member network_peer] fails to establish a connection to a server. Only emitted on clients.
</description>
</signal>
<signal name="network_despawn">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="node" type="Node" />
<argument index="3" name="data" type="PackedByteArray" />
<description>
Emitted on a client before deleting a local Node upon receiving a despawn request from the server.
</description>
</signal>
<signal name="network_despawn_request">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="parent" type="Node" />
<argument index="3" name="name" type="String" />
<argument index="4" name="data" type="PackedByteArray" />
<description>
Emitted when a network despawn request has been received from a client, or for a [PackedScene] that has been configured as [constant SPAWN_MODE_CUSTOM].
</description>
</signal>
<signal name="network_peer_connected">
<argument index="0" name="id" type="int" />
<description>
@ -160,39 +115,6 @@
Emitted when this MultiplayerAPI's [member network_peer] receive 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>
<signal name="network_spawn">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="node" type="Node" />
<argument index="3" name="data" type="PackedByteArray" />
<description>
Emitted on a client after a new Node is instantiated locally and added to the SceneTree upon receiving a spawn request from the server.
</description>
</signal>
<signal name="network_spawn_request">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="parent" type="Node" />
<argument index="3" name="name" type="String" />
<argument index="4" name="data" type="PackedByteArray" />
<description>
Emitted when a network spawn request has been received from a client, or for a [PackedScene] that has been configured as [constant SPAWN_MODE_CUSTOM].
</description>
</signal>
<signal name="network_spawnable_added">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted when an instance of a [PackedScene] that has been configured for networking enters the [SceneTree]. See [method spawnable_config].
</description>
</signal>
<signal name="network_spawnable_removed">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted when an instance of a [PackedScene] that has been configured for networking leaves the [SceneTree]. See [method spawnable_config].
</description>
</signal>
<signal name="server_disconnected">
<description>
Emitted when this MultiplayerAPI's [member network_peer] disconnects from server. Only emitted on clients.
@ -212,14 +134,5 @@
<constant name="RPC_MODE_PUPPET" value="3" enum="RPCMode">
Used with [method Node.rpc_config] to set a method to be called or a property to be changed only on puppets for this node. Analogous to the [code]puppet[/code] keyword. Only accepts calls or property changes from the node's network master, see [method Node.set_network_master].
</constant>
<constant name="SPAWN_MODE_NONE" value="0" enum="SpawnMode">
Used with [method spawnable_config] to identify a [PackedScene] that should not be replicated.
</constant>
<constant name="SPAWN_MODE_SERVER" value="1" enum="SpawnMode">
Used with [method spawnable_config] to identify a [PackedScene] that should be automatically replicated from server to clients.
</constant>
<constant name="SPAWN_MODE_CUSTOM" value="2" enum="SpawnMode">
Used with [method spawnable_config] to identify a [PackedScene] that can be manually replicated among peers.
</constant>
</constants>
</class>

View file

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="MultiplayerReplicator" inherits="Object" version="4.0">
<brief_description>
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
<methods>
<method name="decode_state">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="data" type="PackedByteArray" />
<description>
Decode the given [code]data[/code] representing a spawnable state into [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when a client receives a server spawn for a scene with [constant REPLICATION_MODE_SERVER]. See [method spawn_config].
Tip: You may find this function useful in servers when parsing spawn requests from clients, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM].
</description>
</method>
<method name="despawn">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="peer_id" type="int" default="0" />
<description>
</description>
</method>
<method name="encode_state">
<return type="PackedByteArray" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<description>
Encode the given [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when the server spawns scenes with [constant REPLICATION_MODE_SERVER]. See [method spawn_config].
Tip: You may find this function useful when requesting spawns from clients to server, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM].
</description>
</method>
<method name="send_despawn">
<return type="int" enum="Error" />
<argument index="0" name="peer_id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="data" type="Variant" default="null" />
<argument index="3" name="path" type="NodePath" default="NodePath(&quot;&quot;)" />
<description>
Sends a despawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant REPLICATION_MODE_SERVER] (see [method spawn_config]) and the request is sent by the server (see [method MultiplayerAPI.is_network_server]), the receiving peer(s) will automatically queue for deletion the node at [code]path[/code] and emit the signal [signal despawned]. In all other cases no deletion happens, and the signal [signal despawn_requested] is emitted instead.
</description>
</method>
<method name="send_spawn">
<return type="int" enum="Error" />
<argument index="0" name="peer_id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="data" type="Variant" default="null" />
<argument index="3" name="path" type="NodePath" default="NodePath(&quot;&quot;)" />
<description>
Sends a spawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant REPLICATION_MODE_SERVER] (see [method spawn_config]) and the request is sent by the server (see [method MultiplayerAPI.is_network_server]), the receiving peer(s) will automatically instantiate that scene, add it to the [SceneTree] at the given [code]path[/code] and emit the signal [signal spawned]. In all other cases no instantiation happens, and the signal [signal spawn_requested] is emitted instead.
</description>
</method>
<method name="spawn">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="peer_id" type="int" default="0" />
<description>
</description>
</method>
<method name="spawn_config">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="spawn_mode" type="int" enum="MultiplayerReplicator.ReplicationMode" />
<argument index="2" name="properties" type="StringName[]" default="[]" />
<argument index="3" name="custom_send" type="Callable" />
<argument index="4" name="custom_receive" type="Callable" />
<description>
Configures the MultiplayerAPI to track instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. When [code]mode[/code] is [constant REPLICATION_MODE_SERVER], the specified [code]properties[/code] will also be replicated to clients during the initial spawn.
Tip: You can use a custom property in the scene main script to return a customly optimized state representation.
</description>
</method>
</methods>
<signals>
<signal name="despawn_requested">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="parent" type="Node" />
<argument index="3" name="name" type="String" />
<argument index="4" name="data" type="PackedByteArray" />
<description>
Emitted when a network despawn request has been received from a client, or for a [PackedScene] that has been configured as [constant REPLICATION_MODE_CUSTOM].
</description>
</signal>
<signal name="despawned">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted on a client before deleting a local Node upon receiving a despawn request from the server.
</description>
</signal>
<signal name="replicated_instance_added">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted when an instance of a [PackedScene] that has been configured for networking enters the [SceneTree]. See [method spawn_config].
</description>
</signal>
<signal name="replicated_instance_removed">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted when an instance of a [PackedScene] that has been configured for networking leaves the [SceneTree]. See [method spawn_config].
</description>
</signal>
<signal name="spawn_requested">
<argument index="0" name="id" type="int" />
<argument index="1" name="scene_id" type="int" />
<argument index="2" name="parent" type="Node" />
<argument index="3" name="name" type="String" />
<argument index="4" name="data" type="PackedByteArray" />
<description>
Emitted when a network spawn request has been received from a client, or for a [PackedScene] that has been configured as [constant REPLICATION_MODE_CUSTOM].
</description>
</signal>
<signal name="spawned">
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="node" type="Node" />
<description>
Emitted on a client after a new Node is instantiated locally and added to the SceneTree upon receiving a spawn request from the server.
</description>
</signal>
</signals>
<constants>
<constant name="REPLICATION_MODE_NONE" value="0" enum="ReplicationMode">
Used with [method spawn_config] to identify a [PackedScene] that should not be replicated.
</constant>
<constant name="REPLICATION_MODE_SERVER" value="1" enum="ReplicationMode">
Used with [method spawn_config] to identify a [PackedScene] that should be automatically replicated from server to clients.
</constant>
<constant name="REPLICATION_MODE_CUSTOM" value="2" enum="ReplicationMode">
Used with [method spawn_config] to identify a [PackedScene] that can be manually replicated among peers.
</constant>
</constants>
</class>

View file

@ -202,7 +202,7 @@ protected:
static String _get_name_num_separator();
friend class SceneState;
friend class MultiplayerAPI;
friend class MultiplayerReplicator;
void _add_child_nocheck(Node *p_child, const StringName &p_name);
void _set_owner_nocheck(Node *p_owner);