Changelog¶
All notable changes to MafiaNet are documented here.
Version 0.14.0¶
Voice
Bounded relay decode.
SetMaxDecodedSpeakers(n)caps how many relay speakers may hold a decoder at once. Relay frames are decoded inOnReceive, insideRakPeer::Receive, so by the time an application sees a frame the codec work is already done – a speaker cap applied above this layer bounds mixing rather than CPU, and there was previously no way to bound decode at all.At the cap, a newly-heard speaker takes the decoder of whichever current speaker has been silent longest, provided it has been idle for at least
RAKVOICE_RELAY_EVICT_IDLE_MS; otherwise its frame is dropped undecoded. Preferring eviction over outright refusal keeps decoders following whoever is actually talking, and the idle requirement is what stops a steady stream of talkers past the cap from creating and destroying a decoder every frame.The count excludes the self-keyed channel, which in relay mode carries outgoing encoder state rather than a remote talker, so a caller asking for
ndecoders getsn.Additive and off by default:
0leaves only the existingRAKVOICE_MAX_RELAY_SPEAKERSmemory backstop. Only the relay path is affected – direct peer-to-peer voice does not go throughGetOrCreateChannel. No message ids move, so peers built against 0.13.0 stay wire-compatible.
Version 0.13.0¶
Voice
Relay mode for RakVoice.
SendFramehas always transmitted peer-to-peer, which a dedicated-server game cannot use: clients are connected only to the server, never to each other. In relay mode 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 is not allowed to, because it never receives their bytes.The relay frame carries 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 being misparsed. Every offset derives from the one before it, so the writer and both readers cannot drift apart.
Origin-keyed channels. In relay mode a frame is looked up by its origin GUID rather than
packet->guid; otherwise every speaker arrives under the relay’s GUID and collapses into a single decoder.Per-speaker output.
SetPerSpeakerOutputstopsReceiveFramemixing all speakers into one buffer, andReceiveFrameFrompulls one speaker’s decoded PCM. The pre-mix leaves no way to position speakers individually, so this is what makes 3D voice possible above this layer.Bounded relay state. Concurrent relay speakers are capped (
RAKVOICE_MAX_RELAY_SPEAKERS) because origins are attacker-influenced and each costs a decoder plus two ring buffers. Idle relay channels are reaped:OnClosedConnectionnever fires for them, since relay speakers are 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 each host to remember. A host that forgets the impersonation check is otherwise silently exploitable.Peer-to-peer behaviour is unchanged when relay mode is off.
Security / robustness
Five pre-existing remote-input bugs, all reachable by any connected peer and none requiring relay mode, found while auditing RakVoice’s packet entry points:
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 a single 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 fixed: 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 received one on disconnect.
Testing
Tests/Unit/RakVoiceRelayTests.cpp(23 cases) covers the wire layout, hostile relay input (truncation, unknown format versions, the sentinel origin, the origin/sender mismatch a host must reject), the concurrent-speaker cap, and the channel-open parsing paths.
Warning
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.
Version 0.12.0¶
Networking
Batched datagram I/O (
recvmmsg/sendmmsg). On Linux the reliability layer now coalesces a tick’s outgoing datagrams into a singlesendmmsgand drains the socket with a singlerecvmmsgper burst, instead of onesendto/recvfromper packet. Measured at ~31x fewer system calls on a 2560-message reliable-ordered burst (5525 to 178, median of 3 runs understrace -c); at low packet rates it changes nothing measurable.There is nothing to configure: batching is a platform capability, guarded by a plain
#if defined(__linux__), and is always on where the syscalls exist. Every other platform (macOS, Windows, the BSDs) compiles the portable per-datagram paths. Delivery semantics are identical either way – the same datagrams arrive, in the same order, with the same reliability.Runtime fallback when the syscalls are unavailable. If
recvmmsgorsendmmsgreportENOSYS– 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. OnlyENOSYSlatches:EPERMis excluded because a firewall rejecting a single destination reports it too. Verified end to end under a seccomp profile forcingerrno38.RakNetSocket2::SendBatchis a new virtual on the socket interface, with a portableSend()-loop default and asendmmsgoverride on Linux. It returns a datagram count (not a byte total), or a negative error only when nothing at all went out, mirroringsendmmsg(2). A datagram that fails on its own is dropped and the rest of the batch is still sent.
Testing / CI
Tests/Unit/MmsgBatchTests.cpp(58 cases) covers 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 that no slot is leaked or double-freed.Tests/Integration/MmsgBatchLiveTests.cppdrives 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 builds and tests Debug and Release natively. The hermetic unit suite now runs exactly once;--repeat until-pass:3is reserved for the integration suite, where it absorbs loopback timing misses rather than masking nondeterminism.
Version 0.11.0¶
Core / API
Range-based receive
Peer::incoming(). Wraps the drain loop in a single-pass input range: each iteration yields a freshPacketPtrthat is deallocated when the loop body scope ends, andpkt.id()returns theID_TIMESTAMP-aware message identifier. The ChatExample client is updated to use it.Startup builders
Peer::server()/Peer::client(). Fluent builders that fold the multi-call startup dance (SocketDescriptor,Startup, result check,SetMaximumIncomingConnections/Connect) into a single chain.start()returns a move-onlyResult<Peer>carrying the livePeeron success, or aPeerErrorpreserving the underlyingStartupResult/ConnectionAttemptResult(tagged byPeerStage) on failure — never collapsed to a bool. Security stays opt-in (ServerBuilder::secure(),ClientBuilder::public_key()).Serialization archives in
mafianet/Archive.h. A singleserialize()convention overBitStream: a user type describes its wire format once (template <class Ar> void serialize(Ar& ar) { ar & a & b; }) andWriteArchive/ReadArchiverun it in either direction. Fields with their ownserialize()recurse; everything else falls through toBitStream’soperator<</operator>>and its per-type specializations. Exported from the umbrella header.Typed message dispatcher in
mafianet/Dispatcher.h.MafiaNet::Dispatcherreplaces the giant switch-on-first-byte receive loop:on<T>(handler)registers a typed handler (auto-assigning identifiers fromID_USER_PACKET_ENUMin registration order — a documented wire contract, withon<T>(id, handler)to pin explicit ids),on(id, handler)handles system identifiers, anddispatch()skips anyID_TIMESTAMPprefix, deserializes via the archives, and invokes the handler with aSender(guid()/peer_guid()/address()/guid_string()).encode()is the symmetric write path. Opt-in sugar — the rawswitchpath stays fully usable. Exported from the umbrella header.Typed send / broadcast
Peer::send<T>()/Peer::broadcast<T>(). Encode a registered message (id + archived body) via the dispatcher’s registry and forward toSend(const BitStream*, ...), with overridable defaults (Priority::High,Reliability::ReliableOrdered, channel 0). The destination accepts anAddressOrGUID(implicit fromSystemAddressorRakNetGUID); rawSend()is untouched.
Build
RakVoice is built into the core library.
RakVoice.hmoved tomafianet/RakVoice.hand its codec dependencies (Opus, RNNoise) are fetched and linked into the core library automatically — no separate extension build required.
Testing / CI
Full GoogleTest migration. All 29 legacy tests are ported and the homegrown
TestInterfaceharness (Samples/Tests) is deleted. Tests now live underTests/— a hermeticUnitTestsbinary (Tests/Unit, labelunit) and a loopbackIntegrationTestsbinary (Tests/Integration, labelintegration), with shared helpers inTests/Support. Each test runs in its own process under CTest (RUN_SERIAL+ timeout for integration);MAFIANET_BUILD_TESTSnow builds everything test-related (and requiresMAFIANET_BUILD_STATIC). CI runs ctest with JUnit artifacts on all platforms and retries transient integration misses. Two latent uninitialized-variable bugs in ported tests were fixed along the way.
Version 0.10.0¶
Core / API
Real umbrella header
mafianet/mafianet.h. Aggregates the core public headers (RakPeerInterface, types, message IDs,PacketPriority,BitStream,GetTime,Statistics) behind a single include, so the common client/server path only needs#include "mafianet/mafianet.h". Purely additive — the granular headers remain for advanced users. Encryption headers are intentionally omitted; connection security stays opt-in viaRakPeerInterface::InitializeSecurity().Canonical type aliases in
mafianet/aliases.hover the legacy RakNet-named public types:PeerInterface(RakPeerInterface),Guid(RakNetGUID),Statistics(RakNetStatistics) andUnassignedGuid(UNASSIGNED_RAKNET_GUID). These areusingaliases denoting the exact same types/objects, so old and new names interoperate freely. The legacy declarations are left untouched and un-deprecated. Pulled into the umbrella header.RAII handles
PeerandPacketPtrinmafianet/PeerHandle.h(exported from the umbrella header).Peerowns aRakPeerInterfaceinstance and destroys it on scope exit;PacketPtrowns a receivedPacketand deallocates it automatically — removing manualDestroyInstance/DeallocatePacketbookkeeping. The ChatExample client is rewritten to demonstrate them.Thread-safe value-type GUID accessors in
mafianet/guid_util.h:std::string MafiaNet::to_string(const RakNetGUID&)owns its buffer and is thread-safe, andstd::optional<SystemAddress> connected_address(...)maps theUNASSIGNED_SYSTEM_ADDRESSsentinel tostd::nullopt.
Spatial
PointGridSectorizer — a uniform grid over point entries that keeps a per-entry record (cell + slot) in a pointer-keyed hash, giving O(1)
RemoveEntryandMoveEntry(swap-remove within a cell, with a cheap early-out when a move stays in its current cell).AddEntry/MoveEntryshare upsert semantics (one entry per pointer), andGetEntriesnever returns duplicates. Out-of-bounds positions and query rectangles clamp to the edge cells.GridSectorizeris left untouched.
Breaking changes
Scoped enum classes for priority and reliability. The unscoped global C enums
PacketPriority/PacketReliability(which leaked their enumerators into the global namespace) are removed and replaced with scopedMafiaNet::Priority/MafiaNet::Reliabilityenum classes. Enumerator order is preserved, so underlying integer values — and the 3-bit reliability wire field — are unchanged and remain wire-compatible. PublicSend()/CloseConnection()/ etc. now take the new types; update call sites (e.g.HIGH_PRIORITY→MafiaNet::Priority::High,RELIABLE_ORDERED→MafiaNet::Reliability::ReliableOrdered). TheNUMBER_OF_PRIORITIES/NUMBER_OF_RELIABILITIESsentinels are nowconstexpr unsigned intcounts (4 and 8) in namespaceMafiaNet.Removed the non-thread-safe
RakNetGUID::ToString(void)member (which returned a shared static buffer). UseMafiaNet::to_string(g).c_str()instead.AddressOrGUID::ToString(bool)now self-contains its rotating buffer;SystemAddress/AddressOrGUIDToStringcalls are otherwise unchanged.
Bug fix
PeerHandleno longer dereferences a moved-fromPeerinreceive(); corrected the header copyright.
Testing
Added
PointGridSectorizerTest,PeerHandleTestandGuidUtilTest; hardenedDisconnectReasonTestagainst CI scheduler starvation with a bounded retry.
Version 0.9.0¶
Core
Strong-typed
PeerGuid. 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 withToPeerGuid()/ToGuid(), and compare against theUNASSIGNED_PEER_GUIDsentinel. Being a trivially-copyable 8-byte scoped enum, it serializes byte-identically throughBitStream(and thereforeVariableDeltaSerializer) to the rawuint64_tit replaces, so it is fully wire-compatible and requires no netcode/protocol bump. Purely additive — no behavioural change.
Version 0.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 exactly 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. Appending bytes after the 1-byte ID is 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.
Documentation
Added a “Disconnect with a reason” section to the connecting guide and a cross-reference from the disconnect-debugging guide.
Testing
Added
DisconnectReasonTestcovering reason round-trip, thenullptrdefault, and the empty-but-non-nullBitStreamguard.
Version 0.7.0¶
Plugins
Virtual worlds (dimensions) for ReplicaManager3. A new lightweight per-entity / per-observer
VirtualWorldIdtag scopes visibility at runtime — the SA-MPSetPlayerVirtualWorld/ routing-bucket model for instanced interiors such as apartments. Players only see entities sharing their virtual world (or theVIRTUAL_WORLD_GLOBALsentinel), switchable on the fly with no reconnect, while staying on the same connection and the same RM3WorldId. Derive entities from the newVirtualWorldReplica3base (mafianet/VirtualWorldReplica3.h);Connection_RM3gainsGet/SetVirtualWorld;ReplicaManager3gainsGetConnectionsInVirtualWorld/GetGuidsInVirtualWorld(recipient-filter helpers for scoping non-replica traffic like chat and RPC) andSetPlayerVirtualWorld. The filter is applied only by the authority for an (entity, connection) pair, so a downloaded copy never despawns the entity at its owner. Seemafianet/VirtualWorld.hand theSamples/VirtualWorlddemo.
Documentation
Added a “Virtual Worlds (Dimensions)” plugin guide and expanded the contributing guide with how to test networked features (unit + end-to-end), the ReplicaManager3 authority model, and multi-peer-in-one-process gotchas.
Testing
Added
VirtualWorldTest(deterministic unit coverage, including the non-authority case) and a self-containedSamples/VirtualWorldsmoke test.Fixed the test harness so a subset run (
Tests <name>) callsDestroyPeers()on the test that actually ran.
Version 0.6.1¶
Plugins
ReplicaManager3::GetReplicaAtIndex is now const. It was the only one of the four read accessors (
GetReplicaCount,GetReplicaAtIndex,GetConnectionCount,GetConnectionAtIndex) that was non-const, which forcedconstmethods on derived managers toconst_castaway constness just to iterate replicas. The accessor only reads the world’s replica list and returns an existing pointer, so the qualifier is accurate; the returnedReplica3*stays non-const, matchingGetConnectionAtIndex. Source-compatible — addingconstto a read accessor doesn’t break existing non-const call sites.
Version 0.6.0¶
Plugins
RPC4 handlers now carry user context.
RegisterFunction,RegisterSlot,RegisterBlockingFunctionand theRPC4GlobalRegistrationhandler constructors take an opaquevoid *contextthat 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 may serve multiple object instances under one identifier. Thevoid*approach preserves RPC4’s zero-external-dependency design.
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, matching the existing buffered-close path.
Testing
Added
RPC4ContextTestcovering slot, nonblocking, and blocking handler context.Quarantined the flaky
ManyClientsOneServerDeallocateBlockingTestunder CI while a pre-existing multithreaded teardown race is investigated.
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 handlers and registration calls (passnullptrwhen no context is needed).
Version 0.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.
Version 0.5.0¶
API & Namespace Cleanup
Standardized on the
MafiaNetnamespace throughout the libraryRemoved the legacy
SLNetcompatibility macro and replaced it with anMNetshort-hand alias that expands toMafiaNetDropped stale
RakNetnamespace-alias references (the alias no longer existed in code) and migrated the remaining sample code toMafiaNet::Collapsed the three header layers inherited from the RakNet/SLikeNet lineage down to the single canonical
Source/include/mafianet/set; the redirect-onlySource/*.handSource/mafianet/*.hstubs were removed and all includes now use themafianet/...form
Bug Fixes
Guarded
BitStream’s catch-allWrite/Readtemplates with astd::is_trivially_copyablestatic_assert, preventing silent pointer-aliasing and double-frees when serializing types that own heap memory (e.g.std::string)Added binary-safe, length-prefixed
std::stringBitStreamspecializations (wire-compatible withRakString)Fixed
Ranking_GetMatchesserializingSubmittedMatchthrough the unsafe catch-all instead of its ownSerialize()
Testing
Added the
BitStreamStringTestregression test
Breaking Changes
Legacy include spellings are gone — include public headers via the
mafianet/...path (e.g.mafianet/string.hinstead ofRakString.h)The
SLNetnamespace macro has been removed — useMafiaNet(or the newMNetshorthand)Serializing a non-trivially-copyable type through the generic
BitStream::Write/Readnow fails to compile by design; provide an explicitSerialize()or a type specialization
Version 0.4.0¶
Cross-Platform Support
Full macOS and Linux compatibility, including merged
Socket2definitionsRemoved deprecated platform back-ends to simplify the socket layer
Fixed IPv6 connectivity and initialization issues
Guarded the
<sys/io.h>include to x86/x86_64 only (fixes ARM builds)
Dependencies
Build pipeline upgraded to OpenSSL 3.6.0 (3.0+ still required)
Updated miniupnpc 2.2.8 → 2.3.3
Updated Opus 1.5.2 → 1.6.1
Dependencies are now fetched on demand via CMake
FetchContentinstead of being bundled in-tree
Bug Fixes
Fixed undefined behaviour from a negative
double→unsignedcast in congestion controlGuard against a null socket in
BCS_CLOSE_CONNECTIONhandlingFixed DLL exports on Windows
Fixed sample compilation across all platforms
Testing & CI
CI now builds and runs the full test suite on Linux, macOS and Windows (stress tests included)
Added a Dockerfile for running the test suite in a container
Numerous test-stability improvements: mesh-convergence waits, race-condition fixes, and a thread-safe plugin lifecycle for
PacketChangerPlugin
Version 0.3.0¶
RakVoice: Speex to Opus Migration
Replaced deprecated Speex codec with Opus 1.5.2
Added RNNoise for neural network-based noise suppression
Supported sample rates changed to native Opus rates: 8000, 16000, 24000, 48000 Hz
VAD now uses Opus DTX (Discontinuous Transmission)
Added
SetSignalType()for voice/music optimization hintsRemoved
SetEncoderComplexity()/GetEncoderComplexity()(Opus handles internally)Bundled Opus and RNNoise sources (no external dependencies)
Removed Speex and SpeexDSP from DependentExtensions
Breaking Changes:
Sample rate 32000 Hz is no longer supported (use 24000 or 48000)
SendFrame()andRequestVoiceChannel()now useRakNetGUIDinstead ofSystemAddress
Version 0.2.0¶
Rebranded from SLikeNet to MafiaNet
Updated to C++17 standard
Modernized CMake build system
Added Sphinx documentation with Breathe integration
Removed pre-generated Visual Studio solution files
Updated dependencies (miniupnpc, OpenSSL)
Version 0.1.0¶
Initial fork from SLikeNet
Basic project structure established