Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions cmake/MafiaNetPin.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
# tree, so bumping the pin here and reconfiguring incrementally would silently
# keep the old release. This file is the single source of truth, and there is no
# reason to let -D override the wire format of the protocol.
set(MAFIANET_PIN_VERSION "0.23.0") # A Shutdown() from inside receive dispatch no longer frees the packet or transfer receiver in use; snake_case header names (RAKNET_PROTOCOL_VERSION still 7, wire-compatible)
set(MAFIANET_PIN_VERSION "0.23.1") # Resizing a delta-tracked variable no longer clears the next one's dirty flag or writes past the list (RAKNET_PROTOCOL_VERSION still 7, wire-compatible)

set(MAFIANET_PIN_SHA256_linux-x86_64 "b81fcd58707273b25e6e6ac3ce927f3b51ff6d53742bc042aac22e59a71fb953")
set(MAFIANET_PIN_SHA256_macos-arm64 "0723adfa047ffd8cf89ce5c0b1ddb1ffc088313bbbf8f25b95a321bbd0301a2b")
set(MAFIANET_PIN_SHA256_macos-x86_64 "cbb3ccc389a2a8444bad223db2bb0fe8d7c4818b48b3b40e56f744200c584ff5")
set(MAFIANET_PIN_SHA256_windows-x64 "baefa6694fdd2985940f307256b1410ccbd3a7dbd21f12ebc809c80b5d813e6c")
set(MAFIANET_PIN_SHA256_windows-x86 "75f70eafec2245994aa3130a89727f6241732831ce14f6b38dcd4edb33416c3e")
set(MAFIANET_PIN_SHA256_linux-x86_64 "c4035c5ea91d231b921b111b06e10e730c7d1b51ab633bcfb291fb2e0b037092")
set(MAFIANET_PIN_SHA256_macos-arm64 "666cffa58941299d90e66384d29934b1bff7b4063dde27a5c10049b6f5d4b9c7")
set(MAFIANET_PIN_SHA256_macos-x86_64 "5f518eaba0fd5f305b234798bbea9c5284b6670a2fce16d76dbc25ac7649b156")
set(MAFIANET_PIN_SHA256_windows-x64 "86026f72e81c6e257c096e03ade38fa8317375bf1d0be2aee448641808ebb605")
set(MAFIANET_PIN_SHA256_windows-x86 "db722fbabb0d7844433bcd31d25b22e4ab7c9b01b0509191b32273409335987b")
2 changes: 1 addition & 1 deletion code/framework/src/networking/replication/delegation.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ namespace Framework::Networking::Replication {
// It is deliberately built on the ownership the replication layer already has rather than beside
// it: the elected client becomes the entity's owner, so it inherits the whole authority model --
// its updates travel upstream and are relayed, the server's overrides reach it through
// ForceState, stale updates from a revoked simulator are dropped by the state epoch, and the
// ForceState, updates still in flight from a revoked simulator are refused, and the
// pose ordering is reset on the handover. A game's own code asks `IsOwner()` and nothing else.
//
// The entity opts in by overriding NetworkEntity::GetDelegationPolicy. Nothing else is required;
Expand Down
75 changes: 67 additions & 8 deletions code/framework/src/networking/replication/network_entity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,62 @@ namespace Framework::Networking::Replication {
fields.Field(rotation);
}

void NetworkEntity::WriteForcedState(MafiaNet::BitStream *bs) {
bs->Write(stateEpoch);
// Whether the pose below is a move or an echo, so the owner warps only for the first.
bs->Write(IsPoseServerAuthored());
FieldSerializer fields(bs, true);
SerializeForcedState(fields);
}

void NetworkEntity::ReadForcedState(MafiaNet::BitStream *bs) {
// Adopting the epoch is what acknowledges the override: updates we send from here on carry
// it, so the server stops refusing them.
// A truncated body adopts nothing: its epoch would acknowledge an override never applied.
uint8_t epoch = 0;
bool poseForced = true;
if (!bs->Read(epoch) || !bs->Read(poseForced)) {
return;
}
FieldSerializer fields(bs, false);
SerializeForcedState(fields);
if (!fields.Good()) {
return;
}
stateEpoch = epoch;
_poseForced = poseForced;
// The server read past what we sent of these under the old epoch, but our delta history
// records it as delivered: without this, moving back to that value would never be resent.
_vds.MarkChanged(ForcedVariables(IsServerPeer()));
OnStateForced();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

std::vector<bool> NetworkEntity::ForcedVariables(bool serverAuthored) {
std::vector<FieldSerializer::Span> spans;
FieldSerializer forced(&spans);
SerializeForcedState(forced);
std::vector<bool> variables;
FieldSerializer state(&spans, &variables, serverAuthored);
SerializeBaseFields(state);
SerializeFields(state);
return variables;
}

void DeltaSerializer::MarkChanged(const std::vector<bool> &variables) {
// Nothing sent yet: the first send carries every variable anyway.
const unsigned int history = GetRemoteSystemHistoryListIndex(MafiaNet::UNASSIGNED_RAKNET_GUID);
if (history == static_cast<unsigned int>(-1)) {
return;
}
std::vector<unsigned char> bits((variables.size() + 7) / 8, 0);
for (std::size_t i = 0; i < variables.size(); ++i) {
if (variables[i]) {
bits[i >> 3] |= static_cast<unsigned char>(0x80 >> (i & 7));
}
}
remoteSystemVariableHistoryList[history]->variableListDeltaTracker.FlagDirtyFromBitArray(bits.data());
}

void NetworkEntity::MarkStateDirty() {
// No manager before Reference(); a bag written that early is carried by the seed anyway.
if (auto *manager = Manager()) {
Expand Down Expand Up @@ -313,18 +369,21 @@ namespace Framework::Networking::Replication {
}
}

// Channel 1 — state.
// Channel 1 — state. A stale update is still applied: the owner sends each change only once, so
// dropping it would lose it for good. Only its forced fields are read past.
if (deserializeParameters->bitstreamWrittenTo[kStateChannel]) {
uint8_t incomingEpoch = stateEpoch;
deserializeParameters->serializationBitstream[kStateChannel].Read(incomingEpoch);
if (ApplyIncomingEpoch(incomingEpoch)) {
MafiaNet::VariableDeltaSerializer::DeserializationContext ctx;
_vds.BeginDeserialize(&ctx, &deserializeParameters->serializationBitstream[kStateChannel]);
FieldSerializer fields(&_vds, &ctx, !IsServerPeer());
SerializeBaseFields(fields);
SerializeFields(fields);
_vds.EndDeserialize(&ctx);
std::vector<bool> forced;
if (!ApplyIncomingEpoch(incomingEpoch)) {
forced = ForcedVariables(!IsServerPeer());
}
MafiaNet::VariableDeltaSerializer::DeserializationContext ctx;
_vds.BeginDeserialize(&ctx, &deserializeParameters->serializationBitstream[kStateChannel]);
FieldSerializer fields(&_vds, &ctx, !IsServerPeer(), &forced);
SerializeBaseFields(fields);
SerializeFields(fields);
_vds.EndDeserialize(&ctx);
}

// Already shifted to our local clock by RakPeer; do not subtract GetClockDifferential.
Expand Down
79 changes: 69 additions & 10 deletions code/framework/src/networking/replication/network_entity.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>

#include <cstddef>
#include <cstdint>
#include <vector>

namespace Framework::Networking::Replication {
class ReplicationManager;
Expand All @@ -32,11 +34,24 @@ namespace Framework::Networking::Replication {
class FieldSerializer final {
public:
FieldSerializer(MafiaNet::VariableDeltaSerializer *vds, MafiaNet::VariableDeltaSerializer::SerializationContext *ctx, bool serverAuthored) : _vds(vds), _serialize(ctx), _serverAuthored(serverAuthored) {}
FieldSerializer(MafiaNet::VariableDeltaSerializer *vds, MafiaNet::VariableDeltaSerializer::DeserializationContext *ctx, bool serverAuthored) : _vds(vds), _deserialize(ctx), _serverAuthored(serverAuthored) {}
// `keep` marks, by delta variable index, the fields to read past without assigning.
FieldSerializer(MafiaNet::VariableDeltaSerializer *vds, MafiaNet::VariableDeltaSerializer::DeserializationContext *ctx, bool serverAuthored, const std::vector<bool> *keep = nullptr) : _vds(vds), _deserialize(ctx), _serverAuthored(serverAuthored), _keep(keep) {}
FieldSerializer(MafiaNet::BitStream *bs, bool write, bool serverAuthored = true) : _plain(bs), _writePlain(write), _serverAuthored(serverAuthored) {}

// Where one field lives, so one field list can be found inside another without a wire.
struct Span {
const void *data;
std::size_t size;
};

// Walks without touching anything: lists every field's span, or, given `within`, records per
// field (in delta variable order) whether it overlaps one of them. Reads as writing so a
// body's read-side code does not run.
explicit FieldSerializer(std::vector<Span> *spans) : _spans(spans) {}
FieldSerializer(const std::vector<Span> *within, std::vector<bool> *overlaps, bool serverAuthored) : _serverAuthored(serverAuthored), _within(within), _overlaps(overlaps) {}

bool Writing() const {
return _serialize != nullptr || (_plain != nullptr && _writePlain);
return _serialize != nullptr || (_plain != nullptr && _writePlain) || _spans != nullptr || _overlaps != nullptr;
}

// Whether the server wrote (or is writing) this stream; see the class comment.
Expand All @@ -59,13 +74,26 @@ namespace Framework::Networking::Replication {
else if (_serialize) {
_vds->SerializeVariable(_serialize, value);
}
else {
else if (_deserialize) {
// DeserializeVariable returns "changed", not read success.
bool changed = false;
_good = _deserialize->bitStream->Read(changed);
if (_good && changed) {
_good = _deserialize->bitStream->Read(value);
if (_keep != nullptr && _index < _keep->size() && (*_keep)[_index]) {
T discarded {};
_good = _deserialize->bitStream->Read(discarded);
}
else {
_good = _deserialize->bitStream->Read(value);
}
}
++_index;
}
else if (_spans) {
_spans->push_back({&value, sizeof(T)});
}
else {
_overlaps->push_back(Overlaps(&value, sizeof(T)));
}
}

Expand Down Expand Up @@ -139,13 +167,35 @@ namespace Framework::Networking::Replication {
}

private:
bool Overlaps(const void *data, std::size_t size) const {
const auto *begin = static_cast<const std::byte *>(data);
for (const Span &span : *_within) {
const auto *other = static_cast<const std::byte *>(span.data);
if (begin < other + span.size && other < begin + size) {
return true;
}
}
return false;
}

MafiaNet::VariableDeltaSerializer *_vds = nullptr;
MafiaNet::VariableDeltaSerializer::SerializationContext *_serialize = nullptr;
MafiaNet::VariableDeltaSerializer::DeserializationContext *_deserialize = nullptr;
MafiaNet::BitStream *_plain = nullptr;
bool _writePlain = false;
bool _serverAuthored = true;
bool _good = true;
const std::vector<bool> *_keep = nullptr;
std::size_t _index = 0;
std::vector<Span> *_spans = nullptr;
const std::vector<Span> *_within = nullptr;
std::vector<bool> *_overlaps = nullptr;
};

// The delta serializer, plus a way to make chosen variables count as changed on the next send.
class DeltaSerializer final : public MafiaNet::VariableDeltaSerializer {
public:
void MarkChanged(const std::vector<bool> &variables);
};

// A replicated game object. Game entities derive from this and override SerializeFields (per-tick
Expand Down Expand Up @@ -187,9 +237,9 @@ namespace Framework::Networking::Replication {

// Fences server overrides against in-flight owner updates: bumped by the server on
// ForceState, adopted by the owner from the ForceState/SetOwner RPCs and echoed back in its
// updates; the server drops owner state carrying a stale value (sent before the owner saw the
// override) instead of letting it revert the forced state. Equality-checked, so uint8 wrap is
// harmless. Managed by ReplicationManager — game code never touches it.
// updates; an owner update carrying a stale value (sent before the owner saw the override)
// loses its pose and its forced fields, so it cannot revert the forced state; the owner resends
// those fields once it adopts the override. Equality-checked, so uint8 wrap is harmless. Managed by ReplicationManager — game code never touches it.
uint8_t stateEpoch = 0;

// Local-clock send time of the last applied update (MafiaNet shifts it on receipt). Not replicated.
Expand Down Expand Up @@ -276,6 +326,11 @@ namespace Framework::Networking::Replication {
return _poseForced;
}

// The ForceState RPC body: the server writes it for the owner, which adopts it. Sent by
// ReplicationManager::ForceState; game code calls ForceState instead.
void WriteForcedState(MafiaNet::BitStream *bs);
void ReadForcedState(MafiaNet::BitStream *bs);

// Raised for every key a construction seed delivered, after OnConstructed, so a listener
// finds the entity fully built.
void NotifySeededState(const std::vector<std::string> &keys);
Expand Down Expand Up @@ -385,10 +440,14 @@ namespace Framework::Networking::Replication {
void SerializeBaseFields(FieldSerializer &fields);

// Apply an epoch received over the wire: clients adopt it; the server compares it and
// returns false when the update is stale (sent before the last ForceState) and must not be
// applied.
// returns false when the update is stale (sent before the last ForceState), so its pose and
// forced fields must not be applied.
bool ApplyIncomingEpoch(uint8_t incomingEpoch);

// Which state-channel variables, in delta order, hold a field SerializeForcedState carries.
// `serverAuthored` names the stream being walked, as for FieldSerializer.
std::vector<bool> ForcedVariables(bool serverAuthored);

// --- State-bag plumbing, reached only by the bag itself ---
// Registers with the manager's dirty list, so a flush walks what changed rather than
// sweeping the world.
Expand All @@ -403,7 +462,7 @@ namespace Framework::Networking::Replication {

// Tracks the last value of each serialized variable per connection so updates carry only
// what changed (the documented ReplicaManager3 delta path).
MafiaNet::VariableDeltaSerializer _vds;
DeltaSerializer _vds;

// Sender: refresh schedule state, in serialize-tick time.
bool _transformMoved = false;
Expand Down
18 changes: 2 additions & 16 deletions code/framework/src/networking/replication/replication_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,8 @@ namespace Framework::Networking::Replication {
owner->RegisterRawRPC(kForceStateId, [this](MafiaNet::BitStream *bs, MafiaNet::Packet *) {
MafiaNet::NetworkID networkId;
bs->ReadCompressed(networkId);
uint8_t epoch = 0;
bs->Read(epoch);
bool poseForced = true;
bs->Read(poseForced);
if (auto *entity = GetEntityByNetworkID(networkId)) {
// Adopting the epoch is what acknowledges the override: updates we send from here
// on carry it, so the server stops dropping them.
entity->stateEpoch = epoch;
entity->_poseForced = poseForced;
FieldSerializer fields(bs, false);
entity->SerializeForcedState(fields);
entity->OnStateForced();
entity->ReadForcedState(bs);
}
});
owner->RegisterRPC<SetOwnerRPC>([this](const SetOwnerRPC &payload, MafiaNet::Packet *) {
Expand Down Expand Up @@ -172,11 +162,7 @@ namespace Framework::Networking::Replication {
MafiaNet::NetworkID networkId = entity->GetNetworkID();
// NetworkIDs are small and monotonic, so WriteCompressed strips the leading zero bytes.
bs.WriteCompressed(networkId);
bs.Write(entity->stateEpoch);
// Whether the pose below is a move or an echo, so the owner warps only for the first.
bs.Write(entity->IsPoseServerAuthored());
FieldSerializer fields(&bs, true);
entity->SerializeForcedState(fields);
entity->WriteForcedState(&bs);
_owner->SendRawRPC(kForceStateId, bs, MafiaNet::ToGuid(entity->ownerGUID));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,8 @@ namespace Framework::Networking::Replication {

// Server: push the entity's forced state to its owner — the server's authoritative override
// of an owned entity (see NetworkEntity::ForceState / OnStateForced). Bumps the entity's
// state epoch so owner updates sent before the override arrives are dropped instead of
// reverting it. No-op on clients and for unowned entities (which already replicate to
// everyone).
// state epoch so owner updates sent before the override arrives cannot revert it. No-op on
// clients and for unowned entities (which already replicate to everyone).
void ForceState(NetworkEntity *entity);

// Server: change an entity's owner and notify the new owner directly (see
Expand Down
Loading
Loading