diff --git a/cmake/MafiaNetPin.cmake b/cmake/MafiaNetPin.cmake index f8906c91d..129485886 100644 --- a/cmake/MafiaNetPin.cmake +++ b/cmake/MafiaNetPin.cmake @@ -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") diff --git a/code/framework/src/networking/replication/delegation.h b/code/framework/src/networking/replication/delegation.h index 03528aa16..65066c55a 100644 --- a/code/framework/src/networking/replication/delegation.h +++ b/code/framework/src/networking/replication/delegation.h @@ -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; diff --git a/code/framework/src/networking/replication/network_entity.cpp b/code/framework/src/networking/replication/network_entity.cpp index cb53b4150..120f05436 100644 --- a/code/framework/src/networking/replication/network_entity.cpp +++ b/code/framework/src/networking/replication/network_entity.cpp @@ -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(); + } + + std::vector NetworkEntity::ForcedVariables(bool serverAuthored) { + std::vector spans; + FieldSerializer forced(&spans); + SerializeForcedState(forced); + std::vector variables; + FieldSerializer state(&spans, &variables, serverAuthored); + SerializeBaseFields(state); + SerializeFields(state); + return variables; + } + + void DeltaSerializer::MarkChanged(const std::vector &variables) { + // Nothing sent yet: the first send carries every variable anyway. + const unsigned int history = GetRemoteSystemHistoryListIndex(MafiaNet::UNASSIGNED_RAKNET_GUID); + if (history == static_cast(-1)) { + return; + } + std::vector bits((variables.size() + 7) / 8, 0); + for (std::size_t i = 0; i < variables.size(); ++i) { + if (variables[i]) { + bits[i >> 3] |= static_cast(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()) { @@ -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 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. diff --git a/code/framework/src/networking/replication/network_entity.h b/code/framework/src/networking/replication/network_entity.h index 63dda9195..7c215477b 100644 --- a/code/framework/src/networking/replication/network_entity.h +++ b/code/framework/src/networking/replication/network_entity.h @@ -17,7 +17,9 @@ #include #include +#include #include +#include namespace Framework::Networking::Replication { class ReplicationManager; @@ -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 *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 *spans) : _spans(spans) {} + FieldSerializer(const std::vector *within, std::vector *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. @@ -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))); } } @@ -139,6 +167,17 @@ namespace Framework::Networking::Replication { } private: + bool Overlaps(const void *data, std::size_t size) const { + const auto *begin = static_cast(data); + for (const Span &span : *_within) { + const auto *other = static_cast(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; @@ -146,6 +185,17 @@ namespace Framework::Networking::Replication { bool _writePlain = false; bool _serverAuthored = true; bool _good = true; + const std::vector *_keep = nullptr; + std::size_t _index = 0; + std::vector *_spans = nullptr; + const std::vector *_within = nullptr; + std::vector *_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 &variables); }; // A replicated game object. Game entities derive from this and override SerializeFields (per-tick @@ -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. @@ -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 &keys); @@ -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 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. @@ -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; diff --git a/code/framework/src/networking/replication/replication_manager.cpp b/code/framework/src/networking/replication/replication_manager.cpp index cf65b0456..39cca2c90 100644 --- a/code/framework/src/networking/replication/replication_manager.cpp +++ b/code/framework/src/networking/replication/replication_manager.cpp @@ -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([this](const SetOwnerRPC &payload, MafiaNet::Packet *) { @@ -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)); } diff --git a/code/framework/src/networking/replication/replication_manager.h b/code/framework/src/networking/replication/replication_manager.h index b89be774e..82b0b53ab 100644 --- a/code/framework/src/networking/replication/replication_manager.h +++ b/code/framework/src/networking/replication/replication_manager.h @@ -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 diff --git a/code/tests/modules/replication_authority_ut.h b/code/tests/modules/replication_authority_ut.h index 60ff9490d..f9633bfea 100644 --- a/code/tests/modules/replication_authority_ut.h +++ b/code/tests/modules/replication_authority_ut.h @@ -203,13 +203,48 @@ MODULE(replication_authority, { } }; - // One entity's state channel, serialized the way its peer writes it and delivered to another. - const auto deliverState = [](VerdictEntity &from, VerdictEntity &to, MafiaNet::Connection_RM3 *sourceConnection) { + // An owner-reported value the server can override and push back with ForceState (a game's effect + // mask), beside one only the owner measures. `trim` is forced over a coarse 2-bit wire encoding. + // `forcedReads` counts what a game hangs off reading a forced state, which only an owner may run; + // `forcedHooks` counts OnStateForced. + struct ForcedEntity final: NetworkEntity { + int measured = 0; + int forced = 0; + float trim = 0.0f; + int forcedReads = 0; + int forcedHooks = 0; + void OnStateForced() override { + ++forcedHooks; + } + void SerializeFields(Framework::Networking::Replication::FieldSerializer &fields) override { + fields.Field(measured); + fields.Field(forced); + fields.Field(trim); + } + void SerializeForcedState(Framework::Networking::Replication::FieldSerializer &fields) override { + fields.Field(forced); + fields.FloatBits(trim, 0.0f, 1.0f, 2); + if (!fields.Writing()) { + ++forcedReads; + } + } + }; + + // The ForceState RPC body, from the server's entity to its owner's, once the epoch is bumped. + const auto deliverForcedState = [](NetworkEntity &server, NetworkEntity &owner) { + MafiaNet::BitStream bs; + server.WriteForcedState(&bs); + owner.ReadForcedState(&bs); + }; + + // One entity's state channel, serialized the way its peer writes it and delivered to another. The + // first send to a system carries every field; later ones only what changed since the last. + const auto deliverState = [](auto &from, auto &to, MafiaNet::Connection_RM3 *sourceConnection, bool firstSend = true) { MafiaNet::SerializeParameters sp; for (int i = 0; i < MafiaNet::RM3_NUM_OUTPUT_BITSTREAM_CHANNELS; ++i) { sp.lastSentBitstream[i] = nullptr; } - sp.whenLastSerialized = 0; + sp.whenLastSerialized = firstSend ? 0 : 1; sp.curTime = 1; from.OnUserReplicaPreSerializeTick(); from.Serialize(&sp); @@ -256,6 +291,99 @@ MODULE(replication_authority, { EQUALS(observer.verdict, 2); }); + IT("applies a stale update's measured fields but keeps the ones the server forced", { + ForcedEntity simulator; + simulator.replicaManager = clientManager; + simulator.measured = 1; + simulator.forced = 1; + + ForcedEntity server; + server.replicaManager = serverManager; + server.ownerGUID = MafiaNet::ToPeerGuid(ownerGuid); + + deliverState(simulator, server, ownerConn); + EQUALS(server.measured, 1); + EQUALS(server.forced, 1); + + // The server overrides. Before the override reaches the owner, it sends a change of its own + // and its reading of the forced value, both under the old epoch. The owner sends neither + // again, so the measured one has to land now; `trim` must come back exact, not as 2 bits. + server.forced = 5; + server.trim = 0.5f; + ++server.stateEpoch; + simulator.measured = 2; + simulator.forced = 9; + deliverState(simulator, server, ownerConn, false); + EQUALS(server.measured, 2); + EQUALS(server.forced, 5); + EQUALS(server.trim, 0.5f); + EQUALS(server.forcedReads, 0); + + // Adopted: the owner's updates land whole again. + deliverForcedState(server, simulator); + EQUALS(simulator.stateEpoch, server.stateEpoch); + EQUALS(simulator.forced, 5); + simulator.measured = 3; + deliverState(simulator, server, ownerConn, false); + EQUALS(server.measured, 3); + EQUALS(server.forced, 5); + simulator.forced = 6; + deliverState(simulator, server, ownerConn, false); + EQUALS(server.forced, 6); + }); + + IT("resends a forced field the owner moves back to the value it sent before the override", { + ForcedEntity simulator; + simulator.replicaManager = clientManager; + simulator.forced = 1; + + ForcedEntity server; + server.replicaManager = serverManager; + server.ownerGUID = MafiaNet::ToPeerGuid(ownerGuid); + + deliverState(simulator, server, ownerConn); + + // The owner reports 9 under the old epoch while the server forces 5, so the server keeps 5. + server.forced = 5; + ++server.stateEpoch; + simulator.forced = 9; + deliverState(simulator, server, ownerConn, false); + EQUALS(server.forced, 5); + + // The owner adopts 5, then its game measures 9 again. It last sent 9, but the server never + // took it, so 9 has to go out again. + deliverForcedState(server, simulator); + simulator.forced = 9; + deliverState(simulator, server, ownerConn, false); + EQUALS(server.forced, 9); + }); + + IT("adopts nothing from a truncated forced state", { + ForcedEntity simulator; + simulator.replicaManager = clientManager; + simulator.stateEpoch = 3; + + MafiaNet::BitStream empty; + simulator.ReadForcedState(&empty); + EQUALS(simulator.stateEpoch, 3); + EQUALS(simulator.forcedHooks, 0); + + // A header with no fields after it: the epoch would acknowledge an override never applied. + MafiaNet::BitStream headerOnly; + headerOnly.Write(static_cast(4)); + headerOnly.Write(true); + simulator.ReadForcedState(&headerOnly); + EQUALS(simulator.stateEpoch, 3); + EQUALS(simulator.forcedHooks, 0); + + ForcedEntity server; + server.replicaManager = serverManager; + server.stateEpoch = 4; + deliverForcedState(server, simulator); + EQUALS(simulator.stateEpoch, 4); + EQUALS(simulator.forcedHooks, 1); + }); + serverManager->DeallocConnection(ownerConn); serverManager->DeallocConnection(attackerConn); });