5b25457794
Removes _networking_ prefix from some methods and members, now that multiplayer has been largely moved out of Node and SceneTree and is seperated into its own set of classes.
788 lines
34 KiB
C++
788 lines
34 KiB
C++
/*************************************************************************/
|
|
/* 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/multiplayer/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::_sync_all_default(const ResourceUID::ID &p_scene_id, int p_peer) {
|
|
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
|
|
SceneConfig &cfg = replications[p_scene_id];
|
|
int full_size = 0;
|
|
bool same_size = true;
|
|
int last_size = 0;
|
|
bool all_raw = true;
|
|
struct EncodeInfo {
|
|
int size = 0;
|
|
bool raw = false;
|
|
List<Variant> state;
|
|
};
|
|
Map<ObjectID, struct EncodeInfo> state;
|
|
if (tracked_objects.has(p_scene_id)) {
|
|
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
|
|
Object *obj = ObjectDB::get_instance(obj_id);
|
|
if (obj) {
|
|
struct EncodeInfo info;
|
|
Error err = _get_state(cfg.sync_properties, obj, info.state);
|
|
ERR_CONTINUE(err);
|
|
err = _encode_state(info.state, nullptr, info.size, &info.raw);
|
|
ERR_CONTINUE(err);
|
|
state[obj_id] = info;
|
|
full_size += info.size;
|
|
if (last_size && info.size != last_size) {
|
|
same_size = false;
|
|
}
|
|
all_raw = all_raw && info.raw;
|
|
last_size = info.size;
|
|
}
|
|
}
|
|
}
|
|
// Default implementation do not send empty updates.
|
|
if (!full_size) {
|
|
return OK;
|
|
}
|
|
#ifdef DEBUG_ENABLED
|
|
if (full_size > 4096 && cfg.sync_interval) {
|
|
WARN_PRINT_ONCE(vformat("The timed state update for scene %d is big (%d bytes) consider optimizing it", p_scene_id));
|
|
}
|
|
#endif
|
|
if (same_size) {
|
|
// This is fast and small. Should we allow more than 256 objects per type?
|
|
// This costs us 1 byte.
|
|
MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + 2 + full_size);
|
|
} else {
|
|
MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + state.size() * 2 + full_size);
|
|
}
|
|
int ofs = 0;
|
|
uint8_t *ptr = packet_cache.ptrw();
|
|
ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC | (same_size ? BYTE_OR_ZERO_FLAG : 0);
|
|
ofs = 1;
|
|
ofs += encode_uint64(p_scene_id, &ptr[ofs]);
|
|
ptr[ofs] = cfg.sync_recv++;
|
|
ofs += 1;
|
|
ofs += encode_uint16(state.size(), &ptr[ofs]);
|
|
if (same_size) {
|
|
ofs += encode_uint16(last_size + (all_raw ? 1 << 15 : 0), &ptr[ofs]);
|
|
}
|
|
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
|
|
if (!state.has(obj_id)) {
|
|
continue;
|
|
}
|
|
struct EncodeInfo &info = state[obj_id];
|
|
Object *obj = ObjectDB::get_instance(obj_id);
|
|
ERR_CONTINUE(!obj);
|
|
int size = 0;
|
|
if (!same_size) {
|
|
// We need to encode the size of every object.
|
|
ofs += encode_uint16(info.size + (info.raw ? 1 << 15 : 0), &ptr[ofs]);
|
|
}
|
|
Error err = _encode_state(info.state, &ptr[ofs], size, &info.raw);
|
|
ERR_CONTINUE(err);
|
|
ofs += size;
|
|
}
|
|
Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
|
|
peer->set_target_peer(p_peer);
|
|
peer->set_transfer_channel(0);
|
|
peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_UNRELIABLE);
|
|
return peer->put_packet(ptr, ofs);
|
|
}
|
|
|
|
void MultiplayerReplicator::_process_default_sync(const ResourceUID::ID &p_id, const uint8_t *p_packet, int p_packet_len) {
|
|
ERR_FAIL_COND_MSG(p_packet_len < SYNC_CMD_OFFSET + 5, "Invalid spawn packet received");
|
|
ERR_FAIL_COND_MSG(!replications.has(p_id), "Invalid spawn ID received " + itos(p_id));
|
|
SceneConfig &cfg = replications[p_id];
|
|
ERR_FAIL_COND_MSG(cfg.mode != REPLICATION_MODE_SERVER || multiplayer->is_server(), "The defualt implementation only allows sync packets from the server");
|
|
const bool same_size = p_packet[0] & BYTE_OR_ZERO_FLAG;
|
|
int ofs = SYNC_CMD_OFFSET;
|
|
int time = p_packet[ofs];
|
|
// Skip old update.
|
|
if (time < cfg.sync_recv && cfg.sync_recv - time < 127) {
|
|
return;
|
|
}
|
|
cfg.sync_recv = time;
|
|
ofs += 1;
|
|
int count = decode_uint16(&p_packet[ofs]);
|
|
ofs += 2;
|
|
#ifdef DEBUG_ENABLED
|
|
ERR_FAIL_COND(!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count);
|
|
#else
|
|
if (!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count) {
|
|
return;
|
|
}
|
|
#endif
|
|
int data_size = 0;
|
|
bool raw = false;
|
|
if (same_size) {
|
|
// This is fast and optimized.
|
|
data_size = decode_uint16(&p_packet[ofs]);
|
|
raw = (data_size & (1 << 15)) != 0;
|
|
data_size = data_size & ~(1 << 15);
|
|
ofs += 2;
|
|
ERR_FAIL_COND(p_packet_len - ofs < data_size * count);
|
|
}
|
|
for (const ObjectID &obj_id : tracked_objects[p_id]) {
|
|
Object *obj = ObjectDB::get_instance(obj_id);
|
|
ERR_CONTINUE(!obj);
|
|
if (!same_size) {
|
|
// This is slow and wasteful.
|
|
data_size = decode_uint16(&p_packet[ofs]);
|
|
raw = (data_size & (1 << 15)) != 0;
|
|
data_size = data_size & ~(1 << 15);
|
|
ofs += 2;
|
|
ERR_FAIL_COND(p_packet_len - ofs < data_size);
|
|
}
|
|
int size = 0;
|
|
Error err = _decode_state(cfg.sync_properties, obj, &p_packet[ofs], data_size, size, raw);
|
|
ofs += data_size;
|
|
ERR_CONTINUE(err);
|
|
ERR_CONTINUE(size != data_size);
|
|
}
|
|
}
|
|
|
|
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;
|
|
const PackedByteArray pba = state_variants[0];
|
|
state_len = pba.size();
|
|
} 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 ? BYTE_OR_ZERO_FLAG : 0);
|
|
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> peer = multiplayer->get_multiplayer_peer();
|
|
peer->set_target_peer(p_peer_id);
|
|
peer->set_transfer_channel(0);
|
|
peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_RELIABLE);
|
|
return 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] & BYTE_OR_ZERO_FLAG) >> BYTE_OR_ZERO_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;
|
|
_track(p_scene_id, node);
|
|
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);
|
|
_untrack(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] & BYTE_OR_ZERO_FLAG) >> BYTE_OR_ZERO_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);
|
|
}
|
|
}
|
|
|
|
void MultiplayerReplicator::process_sync(int p_from, const uint8_t *p_packet, int p_packet_len) {
|
|
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_sync_receive.is_valid()) {
|
|
Array objs;
|
|
if (tracked_objects.has(id)) {
|
|
objs.resize(tracked_objects[id].size());
|
|
int idx = 0;
|
|
for (const ObjectID &obj_id : tracked_objects[id]) {
|
|
objs[idx++] = ObjectDB::get_instance(obj_id);
|
|
}
|
|
}
|
|
PackedByteArray pba;
|
|
pba.resize(p_packet_len - SPAWN_CMD_OFFSET);
|
|
if (pba.size()) {
|
|
memcpy(pba.ptrw(), p_packet, p_packet_len - SPAWN_CMD_OFFSET);
|
|
}
|
|
Variant args[4] = { p_from, id, objs, pba };
|
|
Variant *argp[4] = { args, &args[1], &args[2], &args[3] };
|
|
Callable::CallError ce;
|
|
Variant ret;
|
|
cfg.on_sync_receive.call((const Variant **)argp, 4, ret, ce);
|
|
ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, "Custom sync function failed");
|
|
} else {
|
|
ERR_FAIL_COND_MSG(p_from != 1, "Default sync implementation only allow syncing from server to client");
|
|
_process_default_sync(id, p_packet, p_packet_len);
|
|
}
|
|
}
|
|
|
|
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(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::sync_config(const ResourceUID::ID &p_id, uint64_t p_interval, const TypedArray<StringName> &p_props, const Callable &p_on_send, const Callable &p_on_recv) {
|
|
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");
|
|
ERR_FAIL_COND_V(!replications.has(p_id), ERR_UNCONFIGURED);
|
|
SceneConfig &cfg = replications[p_id];
|
|
ERR_FAIL_COND_V_MSG(p_interval && cfg.mode != REPLICATION_MODE_SERVER && !p_on_send.is_valid(), ERR_INVALID_PARAMETER, "Timed updates in custom mode are only allowed if custom callbacks are also specified");
|
|
for (int i = 0; i < p_props.size(); i++) {
|
|
cfg.sync_properties.push_back(p_props[i]);
|
|
}
|
|
cfg.on_sync_send = p_on_send;
|
|
cfg.on_sync_receive = p_on_recv;
|
|
cfg.sync_interval = p_interval * 1000;
|
|
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) << BYTE_OR_ZERO_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> peer = multiplayer->get_multiplayer_peer();
|
|
peer->set_target_peer(p_peer_id);
|
|
peer->set_transfer_channel(0);
|
|
peer->set_transfer_mode(Multiplayer::TRANSFER_MODE_RELIABLE);
|
|
return 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(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED);
|
|
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_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(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED);
|
|
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_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, bool p_initial) {
|
|
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;
|
|
const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties;
|
|
Error err = _get_state(props, 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, bool p_initial) {
|
|
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];
|
|
const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties;
|
|
int size;
|
|
return _decode_state(props, 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_multiplayer_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_server()) {
|
|
replicated_nodes[p_node->get_instance_id()] = id;
|
|
_track(id, p_node);
|
|
spawn(id, p_node, 0);
|
|
}
|
|
emit_signal(SNAME("replicated_instance_added"), id, p_node);
|
|
} else {
|
|
if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_server() && replicated_nodes.has(p_node->get_instance_id())) {
|
|
replicated_nodes.erase(p_node->get_instance_id());
|
|
_untrack(id, p_node);
|
|
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::poll() {
|
|
for (KeyValue<ResourceUID::ID, SceneConfig> &E : replications) {
|
|
if (!E.value.sync_interval) {
|
|
continue;
|
|
}
|
|
if (E.value.mode == REPLICATION_MODE_SERVER && !multiplayer->is_server()) {
|
|
continue;
|
|
}
|
|
uint64_t time = OS::get_singleton()->get_ticks_usec();
|
|
if (E.value.sync_last + E.value.sync_interval <= time) {
|
|
sync_all(E.key, 0);
|
|
E.value.sync_last = time;
|
|
}
|
|
// Handle wrapping.
|
|
if (E.value.sync_last > time) {
|
|
E.value.sync_last = time;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MultiplayerReplicator::track(const ResourceUID::ID &p_scene_id, Object *p_obj) {
|
|
ERR_FAIL_COND(!replications.has(p_scene_id));
|
|
const SceneConfig &cfg = replications[p_scene_id];
|
|
ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode.");
|
|
_track(p_scene_id, p_obj);
|
|
}
|
|
|
|
void MultiplayerReplicator::_track(const ResourceUID::ID &p_scene_id, Object *p_obj) {
|
|
ERR_FAIL_COND(!p_obj);
|
|
ERR_FAIL_COND(!replications.has(p_scene_id));
|
|
if (!tracked_objects.has(p_scene_id)) {
|
|
tracked_objects[p_scene_id] = List<ObjectID>();
|
|
}
|
|
tracked_objects[p_scene_id].push_back(p_obj->get_instance_id());
|
|
}
|
|
|
|
void MultiplayerReplicator::untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) {
|
|
ERR_FAIL_COND(!replications.has(p_scene_id));
|
|
const SceneConfig &cfg = replications[p_scene_id];
|
|
ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode.");
|
|
_untrack(p_scene_id, p_obj);
|
|
}
|
|
|
|
void MultiplayerReplicator::_untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) {
|
|
ERR_FAIL_COND(!p_obj);
|
|
ERR_FAIL_COND(!replications.has(p_scene_id));
|
|
if (tracked_objects.has(p_scene_id)) {
|
|
tracked_objects[p_scene_id].erase(p_obj->get_instance_id());
|
|
}
|
|
}
|
|
|
|
Error MultiplayerReplicator::sync_all(const ResourceUID::ID &p_scene_id, int p_peer) {
|
|
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
|
|
if (!tracked_objects.has(p_scene_id)) {
|
|
return OK;
|
|
}
|
|
const SceneConfig &cfg = replications[p_scene_id];
|
|
if (cfg.on_sync_send.is_valid()) {
|
|
Array objs;
|
|
if (tracked_objects.has(p_scene_id)) {
|
|
objs.resize(tracked_objects[p_scene_id].size());
|
|
int idx = 0;
|
|
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
|
|
objs[idx++] = ObjectDB::get_instance(obj_id);
|
|
}
|
|
}
|
|
Variant args[3] = { p_scene_id, objs, p_peer };
|
|
Variant *argp[3] = { args, &args[1], &args[2] };
|
|
Callable::CallError ce;
|
|
Variant ret;
|
|
cfg.on_sync_send.call((const Variant **)argp, 3, ret, ce);
|
|
ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, "Custom sync function failed");
|
|
return OK;
|
|
} else if (cfg.sync_properties.size()) {
|
|
return _sync_all_default(p_scene_id, p_peer);
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
Error MultiplayerReplicator::send_sync(int p_peer_id, const ResourceUID::ID &p_scene_id, PackedByteArray p_data, Multiplayer::TransferMode p_transfer_mode, int p_channel) {
|
|
ERR_FAIL_COND_V(!multiplayer->has_multiplayer_peer(), ERR_UNCONFIGURED);
|
|
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
|
|
const SceneConfig &cfg = replications[p_scene_id];
|
|
ERR_FAIL_COND_V_MSG(!cfg.on_sync_send.is_valid(), ERR_UNCONFIGURED, "Sending raw sync messages is only available with custom functions");
|
|
MAKE_ROOM(SYNC_CMD_OFFSET + p_data.size());
|
|
uint8_t *ptr = packet_cache.ptrw();
|
|
ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC;
|
|
encode_uint64(p_scene_id, &ptr[1]);
|
|
Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
|
|
peer->set_target_peer(p_peer_id);
|
|
peer->set_transfer_channel(p_channel);
|
|
peer->set_transfer_mode(p_transfer_mode);
|
|
return peer->put_packet(ptr, SYNC_CMD_OFFSET + p_data.size());
|
|
}
|
|
|
|
void MultiplayerReplicator::clear() {
|
|
tracked_objects.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("sync_config", "scene_id", "interval", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::sync_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("send_sync", "peer_id", "scene_id", "data", "transfer_mode", "channel"), &MultiplayerReplicator::send_sync, DEFVAL(Multiplayer::TRANSFER_MODE_RELIABLE), DEFVAL(0));
|
|
ClassDB::bind_method(D_METHOD("sync_all", "scene_id", "peer_id"), &MultiplayerReplicator::sync_all, DEFVAL(0));
|
|
ClassDB::bind_method(D_METHOD("track", "scene_id", "object"), &MultiplayerReplicator::track);
|
|
ClassDB::bind_method(D_METHOD("untrack", "scene_id", "object"), &MultiplayerReplicator::untrack);
|
|
ClassDB::bind_method(D_METHOD("encode_state", "scene_id", "object", "initial"), &MultiplayerReplicator::encode_state, DEFVAL(true));
|
|
ClassDB::bind_method(D_METHOD("decode_state", "scene_id", "object", "data", "initial"), &MultiplayerReplicator::decode_state, DEFVAL(true));
|
|
|
|
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);
|
|
}
|