Releases: MafiaHub/MafiaNet
Release list
v0.13.0
Voice: relay mode for RakVoice
SendFrame has always transmitted peer-to-peer, which a dedicated-server game cannot use — clients connect only to the server, never to each other. Relay mode lets clients send frames to a host that forwards them without decoding, so the server stays authoritative over who hears whom without paying for a codec: a hacked client cannot hear players it isn't allowed to, because it never receives their bytes.
Relay frames carry the talker's GUID, since the sender is now the relay rather than the speaker, plus a format-version byte:
[id][format version][origin guid][channel id][sequence][opus payload]
The version byte is a deliberate escape hatch — a future layout change is rejected by today's build instead of misparsed. Every offset derives from the one before it, so the writer and both readers cannot drift apart.
Alongside it:
- Origin-keyed channels — frames are looked up by origin rather than
packet->guid; otherwise every speaker arrives under the relay's GUID and collapses into one decoder. - Per-speaker output —
SetPerSpeakerOutput/ReceiveFrameFrompull one speaker's decoded PCM instead of the pre-mix, which is what makes 3D positioning possible above this layer. - Bounded relay state — concurrent speakers are capped (origins are attacker-influenced and each costs a decoder plus two rings), and idle relay channels are reaped, since
OnClosedConnectionnever fires for peers of the host rather than of us. RelayFramevalidates centrally — origin against the transport-authenticated sender, frame size, packet id, recipient list — rather than trusting every host to remember the impersonation check.
Peer-to-peer behaviour is unchanged when relay mode is off.
Security and robustness
Five pre-existing remote-input bugs, found while auditing RakVoice's packet entry points. All are reachable by any connected peer and none require relay mode — they affect anyone running an earlier release with RakVoice attached:
OnVoiceDataread out of bounds on a 1–2 byteID_RAKVOICE_DATApacket: the headermemcpyran past the buffer andpacket->length - headerSizeunderflowed to a huge unsigned value passed toopus_decodeas the payload length.OpenChannelcalledRakAsserton a remotely supplied sample rate.RakAssertis a realassert()in debug builds, so one malformed channel-open packet aborted a debug server.OpenChannelused that sample rate without checking the read succeeded; a packet too short to carry it left the value indeterminate.OnReceivedispatched ondata[0]with no length check.OnOpenChannelReplylacked the initialisation guardOnOpenChannelRequesthas, so an unsolicited reply on an uninitialised instance opened a channel withbufferSizeBytesof 0 and allocated empty rings.
Two further latent bugs: the constructor never initialised zeroBufferedOutput or bufferedOutputCount, so Update() read indeterminate values on any attached-but-uninitialised instance; and CloseVoiceChannel sent ID_RAKVOICE_CLOSE_CHANNEL unconditionally, so a peer that never opened a channel still got one on disconnect.
Testing
23 new unit cases (Tests/Unit/RakVoiceRelayTests.cpp) over the wire layout, hostile relay input, the speaker cap and the channel-open paths. 149/149 pass.
⚠️ Breaking change
ID_RAKVOICE_RELAY_DATA is inserted after ID_RAKVOICE_DATA and shifts every subsequent message id — including ID_READY_EVENT_SET, the RPC4 and two-way-authentication ids, and ID_USER_PACKET_ENUM. Peers must be rebuilt together; a peer built against the old header misparses everything past that point.
Full changelog: v0.12.0...v0.13.0
v0.12.0
Batched datagram I/O (recvmmsg / sendmmsg)
On Linux the reliability layer now coalesces a tick's outgoing datagrams into a single sendmmsg, and drains the receive socket with a single recvmmsg per burst, instead of one sendto/recvfrom per packet.
There is nothing to configure. Batching is a platform capability, guarded by a plain #if defined(__linux__), always on where the syscalls exist. macOS, Windows and the BSDs compile the portable per-datagram paths. Delivery semantics are identical either way — the same datagrams arrive, in the same order, with the same reliability.
Impact
Measured on the 2560-message reliable-ordered burst in Tests/Integration/MmsgBatchLiveTests.cpp (Linux, Release, strace -c, median of 3 runs):
| syscall | per-datagram | batched |
|---|---|---|
sendto |
2907 | 35 |
sendmmsg |
0 | 58 |
recvfrom |
2618 | 0 |
recvmmsg |
0 | 85 |
| total | 5525 | 178 |
~31x fewer system calls. Up to 64 datagrams (MMSG_BATCH_MAX) coalesce per call. Counts vary a percent or two between runs, so the ratio is the result rather than the exact figures. At low packet rates the change is not measurable.
Runtime fallback
If recvmmsg/sendmmsg report ENOSYS — a seccomp profile, gVisor, user-mode emulation, or a kernel older than the syscall — the process latches the condition once and both paths revert to the portable per-datagram code for the rest of its life. Only ENOSYS latches; EPERM is excluded because a firewall rejecting a single destination reports it too. Verified end to end under a seccomp profile forcing errno 38: recvmmsg is attempted exactly once, sendmmsg never, and all traffic falls back cleanly with the full suite passing.
New API
RakNetSocket2::SendBatch — a virtual on the socket interface with a portable Send()-loop default and a sendmmsg override on Linux. Returns a datagram count (not a byte total), or a negative error only when nothing at all went out, mirroring sendmmsg(2). A datagram that fails on its own is dropped and the rest of the batch is still sent.
Testing
Tests/Unit/MmsgBatchTests.cpp— 58 cases over the partial-send resume state machine, the transient-vs-permanenterrnosplit, missing-syscall detection,sockaddrdecoding, and the recv-slot carry-over including a 500-pass fixed-seed stress proving no slot is leaked or double-freed.Tests/Integration/MmsgBatchLiveTests.cpp— drives bursts far past the batch boundary; the rest of the suite sends about one datagram per tick and never fills a batch. Each message is its own checksum, so truncation, payload aliasing and reordering each fail a distinct assertion.- New
linux-nativeCI job covering Debug and Release. The hermetic unit suite now runs exactly once —--repeat until-pass:3is reserved for the integration suite, where it absorbs loopback timing misses instead of masking nondeterminism.
Verified on Linux Debug and Release (unit 94/94, integration 32/32), macOS and Windows CI green.
Known limitations
All testing is loopback inside a container — not a real NIC, MTU, loss or ICMP. ENOBUFS and partial-sendmmsg are exercised against fakes rather than provoked. No long-running soak. A staged rollout is recommended for the batched paths.
Full changelog: v0.11.0...v0.12.0
v0.11.0
Core / API
- Range-based receive
Peer::incoming()— drain the receive queue with a range-for; each iteration yields a freshPacketPtrfreed at end of scope, andpkt.id()returns theID_TIMESTAMP-aware message identifier. - Startup builders
Peer::server()/Peer::client()— a fluent chain foldingSocketDescriptor+Startup+ result check +SetMaximumIncomingConnections/Connectinto one call.start()returns a move-onlyResult<Peer>whose error preserves the underlyingStartupResult/ConnectionAttemptResult(tagged byPeerStage). Security stays opt-in (secure()/public_key()). - Serialization archives (
mafianet/Archive.h) — oneserialize(Ar&)member template describes a type's wire format for both directions;WriteArchive/ReadArchiveadapt aBitStream, recursing into nestedserialize()types and falling through tooperator<</operator>>for everything else. - Typed message dispatcher (
mafianet/Dispatcher.h) —on<T>(handler)auto-assigns identifiers fromID_USER_PACKET_ENUMin registration order (documented wire contract;on<T>(id, handler)pins explicit ids),on(id, handler)covers system messages, anddispatch()skipsID_TIMESTAMPprefixes and hands handlers a deserializedTplus aSender.encode()is the symmetric write path. Opt-in — the rawswitchstays fully usable. - Typed
Peer::send/Peer::broadcast— serialize-and-send a registered message in one call via the dispatcher registry, with overridable defaults (Priority::High,Reliability::ReliableOrdered, channel 0). Destination acceptsSystemAddressorRakNetGUID; rawSend()untouched.
Build
- RakVoice is built into the core library — header now
mafianet/RakVoice.h; Opus and RNNoise are fetched and linked into the core automatically, no separate extension build.
Testing / CI
- Full GoogleTest migration — all 29 legacy tests ported and the
Samples/TestsTestInterfaceharness deleted. HermeticUnitTestsand loopbackIntegrationTestsunderTests/, one process per test via CTest;MAFIANET_BUILD_TESTSbuilds everything test-related (requiresMAFIANET_BUILD_STATIC). CI runs ctest with JUnit artifacts on all platforms.
Full changelog: v0.10.0...v0.11.0
v0.10.0
MafiaNet v0.10.0
Core / API
- Umbrella header
mafianet/mafianet.h— single include aggregating the core public headers (RakPeerInterface, types, message IDs,PacketPriority,BitStream,GetTime,Statistics). Additive; encryption headers intentionally omitted (security stays opt-in). - Canonical type aliases (
mafianet/aliases.h) —PeerInterface,Guid,Statistics,UnassignedGuidover the legacy RakNet names.usingaliases, fully interoperable; legacy names untouched. - RAII handles
Peer&PacketPtr(mafianet/PeerHandle.h) — own aRakPeerInterface/ receivedPacketand clean up on scope exit. ChatExample client rewritten to use them. - Thread-safe GUID value accessors (
mafianet/guid_util.h) —MafiaNet::to_string(const RakNetGUID&)(owns its buffer) andconnected_address(...)returningstd::optional<SystemAddress>.
Spatial
PointGridSectorizer— uniform point grid with O(1)RemoveEntry/MoveEntry(per-entry hash + swap-remove, early-out on same-cell moves), upsert add/move, duplicate-freeGetEntries, edge-cell clamping.GridSectorizerleft untouched.
⚠️ Breaking changes
- Scoped enum classes — global
PacketPriority/PacketReliabilityC enums removed in favour of scopedMafiaNet::Priority/MafiaNet::Reliability. Enumerator order and wire field preserved; update call sites (HIGH_PRIORITY→MafiaNet::Priority::High,RELIABLE_ORDERED→MafiaNet::Reliability::ReliableOrdered).NUMBER_OF_*sentinels are nowconstexprcounts inMafiaNet. - Removed non-thread-safe
RakNetGUID::ToString(void)(shared static buffer) — useMafiaNet::to_string(g).c_str().
Bug fix
PeerHandleno longer dereferences a moved-fromPeerinreceive(); corrected header copyright.
Testing
- Added
PointGridSectorizerTest,PeerHandleTest,GuidUtilTest; hardenedDisconnectReasonTestagainst CI scheduler starvation.
Full changelog: v0.9.0...v0.10.0
v0.9.0
Core
- Strong-typed
PeerGuid(#25). A newenum class PeerGuid : uint64_tnames a peer'sRakNetGUIDvalue distinctly fromNetworkID(an object id), so the two can no longer be passed interchangeably in auint64_t-typed signature — removing a class of silent "passed the wrong id" bugs in ReplicaManager3 glue andvoid(uint64_t)callbacks.- Convert with
MafiaNet::ToPeerGuid()/MafiaNet::ToGuid(); compare against theUNASSIGNED_PEER_GUIDsentinel. - As a trivially-copyable 8-byte scoped enum it serializes byte-identically through
BitStream(and thereforeVariableDeltaSerializer) to the rawuint64_tit replaces — fully wire-compatible, no netcode/protocol bump. - Purely additive; no behavioural change.
- Convert with
Full Changelog: v0.8.0...v0.9.0
v0.8.0
Core
- Optional disconnect reason on graceful disconnects.
CloseConnectiongains a final optionalconst BitStream *reasonDataargument whose bytes are appended right after theID_DISCONNECTION_NOTIFICATIONmessage ID, so the remote peer can learn why it was dropped (e.g. a kick/ban enum plus a custom string). The receiver reads it like any other message body —packet->data + 1forpacket->length - 1bytes. Only graceful disconnects carry a reason; locally-synthesized notifications (ID_CONNECTION_LOSTand the timeout/dead-connection path) stay payload-less, so consumers must tolerate a zero-length body. Wire-backward-compatible: peers that only inspectdata[0]are unaffected.
Bug fix
RakPeer::CloseConnectionno longer coerces an unresolved target index (-1fromGetIndexFromSystemAddress) to0and then readsremoteSystemList[0]— which targeted an unrelated peer's slot or crashed when the list was unallocated. The close socket is now resolved without assuming a valid slot index.
Testing
- Added
DisconnectReasonTestcovering reason round-trip, thenullptrdefault, and the empty-but-non-nullBitStreamguard.
Full Changelog: v0.7.0...v0.8.0
v0.7.0
Virtual Worlds (Dimensions) for ReplicaManager3
This release adds runtime per-player dimension scoping on top of ReplicaManager3 — the SA-MP `SetPlayerVirtualWorld` / FiveM routing-bucket model for instanced interiors such as apartments.
Highlights
- New lightweight per-entity / per-observer `VirtualWorldId` tag (distinct from the heavyweight RM3 `WorldId`). Players only see entities sharing their virtual world (or the `VIRTUAL_WORLD_GLOBAL` sentinel), switchable on the fly with no reconnect, while staying on the same connection and RM3 world.
- Derive entities from the new `VirtualWorldReplica3` base (`mafianet/VirtualWorldReplica3.h`) — no per-object filtering code.
- `Connection_RM3`: `Get/SetVirtualWorld` (the observer's dimension).
- `ReplicaManager3`: `GetConnectionsInVirtualWorld` / `GetGuidsInVirtualWorld` (recipient-filter helpers for scoping chat/RPC/raw sends) and `SetPlayerVirtualWorld`.
- The filter is applied only by the authority for an (entity, connection) pair, so a downloaded copy never despawns the entity at its owner.
Docs & tests
- New "Virtual Worlds (Dimensions)" plugin guide; expanded contributing guide (how to test networked features end-to-end, the RM3 authority model, multi-peer gotchas).
- New `VirtualWorldTest` (deterministic) and a self-contained `Samples/VirtualWorld` demo/smoke test.
See `mafianet/VirtualWorld.h` and `Samples/VirtualWorld` to get started.
Full changelog: v0.6.1...v0.7.0
v0.6.1
ReplicaManager3: const-qualify GetReplicaAtIndex
GetReplicaAtIndex was the only one of the four ReplicaManager3 read accessors that was non-const, even though its sibling GetConnectionAtIndex (same pattern, returns an internal pointer by index) is const. This forced const methods on derived managers to const_cast away constness just to iterate replicas.
Replica3 *GetReplicaAtIndex(unsigned index, WorldId worldId=0) const;The method only reads the world's replica list and returns an existing pointer — no mutation — so the const qualifier is accurate. The returned Replica3* stays non-const, consistent with GetConnectionAtIndex returning a non-const Connection_RM3* from a const method.
Compatibility: Source-compatible — adding const to a read accessor doesn't break existing non-const call sites, and lets const iterators drop their const_cast. No ABI-sensitive layout change.
Full Changelog: v0.6.0...v0.6.1
v0.6.0
RPC4 handlers now carry user context
RegisterFunction, RegisterSlot, RegisterBlockingFunction and the RPC4GlobalRegistration handler constructors now take an opaque void *context that is passed back to the handler on every invocation. This removes the need for file-static global pointers to route an RPC back to an object instance — each registration carries its own context, so the same handler can serve multiple object instances under one identifier. The void* approach keeps RPC4 free of external dependencies. (#4, #5)
Bug fixes
RakPeer::CloseConnectionno longer dereferences a nullrakNetSocketduring connection teardown (a pre-existing crash in release builds, where the assertion is compiled out); it now falls back to the primary socket.
Testing
- Added
RPC4ContextTestcovering slot, nonblocking, and blocking handler context. - Quarantined the flaky
ManyClientsOneServerDeallocateBlockingTestunder CI pending a pre-existing teardown-race fix (#7).
⚠️ Breaking changes
- RPC4 handler signatures gained a trailing
void *contextparameter, and the registration / global-registration functions take a context argument. There are no compatibility overloads — update your handlers and registration calls (passnullptrwhen no context is needed).
Full changelog: v0.5.1...v0.6.0
v0.5.1
Plugins
- Added
DirectoryDeltaTransfer::AddFile(const char *filePath, const char *fileName)to queue a single file for upload, complementing the recursiveAddUploadsFromSubdirectory. It forwards to the existingFileList::AddFileoverload, making the fork self-sufficient for downstream consumers (MafiaHub Framework) that depend on this helper.
Full Changelog: v0.5.0...v0.5.1