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 in OnReceive, inside RakPeer::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 n decoders gets n.

  • Additive and off by default: 0 leaves only the existing RAKVOICE_MAX_RELAY_SPEAKERS memory backstop. Only the relay path is affected – direct peer-to-peer voice does not go through GetOrCreateChannel. No message ids move, so peers built against 0.13.0 stay wire-compatible.

Version 0.13.0

Voice

  • Relay mode for RakVoice. SendFrame has 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. SetPerSpeakerOutput stops ReceiveFrame mixing all speakers into one buffer, and ReceiveFrameFrom pulls 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: OnClosedConnection never fires for them, since relay speakers are peers of the host rather than of us.

  • RelayFrame validates 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:

  • OnVoiceData read out of bounds on a 1-2 byte ID_RAKVOICE_DATA packet: the header memcpy ran past the buffer and packet->length - headerSize underflowed to a huge unsigned value passed to opus_decode as the payload length.

  • OpenChannel called RakAssert on a remotely supplied sample rate. RakAssert is a real assert() in debug builds, so a single malformed channel-open packet aborted a debug server.

  • OpenChannel used that sample rate without checking the read succeeded; a packet too short to carry it left the value indeterminate.

  • OnReceive dispatched on data[0] with no length check.

  • OnOpenChannelReply lacked the initialisation guard OnOpenChannelRequest has, so an unsolicited reply on an uninitialised instance opened a channel with bufferSizeBytes of 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 single sendmmsg and drains the socket with a single recvmmsg per burst, instead of one sendto/recvfrom per packet. Measured at ~31x fewer system calls on a 2560-message reliable-ordered burst (5525 to 178, median of 3 runs under strace -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 recvmmsg or 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.

  • RakNetSocket2::SendBatch is a new virtual on the socket interface, with a portable Send()-loop default and a sendmmsg override on Linux. It 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 / CI

  • Tests/Unit/MmsgBatchTests.cpp (58 cases) covers the partial-send resume state machine, the transient-vs-permanent errno split, missing-syscall detection, sockaddr decoding, and the recv-slot carry-over including a 500-pass fixed-seed stress that 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-native CI job builds and tests Debug and Release natively. The hermetic unit suite now runs exactly once; --repeat until-pass:3 is 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 fresh PacketPtr that is deallocated when the loop body scope ends, and pkt.id() returns the ID_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-only Result<Peer> carrying the live Peer on success, or a PeerError preserving the underlying StartupResult / ConnectionAttemptResult (tagged by PeerStage) on failure — never collapsed to a bool. Security stays opt-in (ServerBuilder::secure(), ClientBuilder::public_key()).

  • Serialization archives in mafianet/Archive.h. A single serialize() convention over BitStream: a user type describes its wire format once (template <class Ar> void serialize(Ar& ar) { ar & a & b; }) and WriteArchive / ReadArchive run it in either direction. Fields with their own serialize() recurse; everything else falls through to BitStream’s operator<< / operator>> and its per-type specializations. Exported from the umbrella header.

  • Typed message dispatcher in mafianet/Dispatcher.h. MafiaNet::Dispatcher replaces the giant switch-on-first-byte receive loop: on<T>(handler) registers a typed handler (auto-assigning identifiers from ID_USER_PACKET_ENUM in registration order — a documented wire contract, with on<T>(id, handler) to pin explicit ids), on(id, handler) handles system identifiers, and dispatch() skips any ID_TIMESTAMP prefix, deserializes via the archives, and invokes the handler with a Sender (guid() / peer_guid() / address() / guid_string()). encode() is the symmetric write path. Opt-in sugar — the raw switch path 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 to Send(const BitStream*, ...), with overridable defaults (Priority::High, Reliability::ReliableOrdered, channel 0). The destination accepts an AddressOrGUID (implicit from SystemAddress or RakNetGUID); raw Send() is untouched.

Build

  • RakVoice is built into the core library. RakVoice.h moved to mafianet/RakVoice.h and 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 TestInterface harness (Samples/Tests) is deleted. Tests now live under Tests/ — a hermetic UnitTests binary (Tests/Unit, label unit) and a loopback IntegrationTests binary (Tests/Integration, label integration), with shared helpers in Tests/Support. Each test runs in its own process under CTest (RUN_SERIAL + timeout for integration); MAFIANET_BUILD_TESTS now builds everything test-related (and requires MAFIANET_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 via RakPeerInterface::InitializeSecurity().

  • Canonical type aliases in mafianet/aliases.h over the legacy RakNet-named public types: PeerInterface (RakPeerInterface), Guid (RakNetGUID), Statistics (RakNetStatistics) and UnassignedGuid (UNASSIGNED_RAKNET_GUID). These are using aliases 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 Peer and PacketPtr in mafianet/PeerHandle.h (exported from the umbrella header). Peer owns a RakPeerInterface instance and destroys it on scope exit; PacketPtr owns a received Packet and deallocates it automatically — removing manual DestroyInstance / DeallocatePacket bookkeeping. 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, and std::optional<SystemAddress> connected_address(...) maps the UNASSIGNED_SYSTEM_ADDRESS sentinel to std::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) RemoveEntry and MoveEntry (swap-remove within a cell, with a cheap early-out when a move stays in its current cell). AddEntry / MoveEntry share upsert semantics (one entry per pointer), and GetEntries never returns duplicates. Out-of-bounds positions and query rectangles clamp to the edge cells. GridSectorizer is 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 scoped MafiaNet::Priority / MafiaNet::Reliability enum classes. Enumerator order is preserved, so underlying integer values — and the 3-bit reliability wire field — are unchanged and remain wire-compatible. Public Send() / CloseConnection() / etc. now take the new types; update call sites (e.g. HIGH_PRIORITYMafiaNet::Priority::High, RELIABLE_ORDEREDMafiaNet::Reliability::ReliableOrdered). The NUMBER_OF_PRIORITIES / NUMBER_OF_RELIABILITIES sentinels are now constexpr unsigned int counts (4 and 8) in namespace MafiaNet.

  • Removed the non-thread-safe RakNetGUID::ToString(void) member (which returned a shared static buffer). Use MafiaNet::to_string(g).c_str() instead. AddressOrGUID::ToString(bool) now self-contains its rotating buffer; SystemAddress / AddressOrGUID ToString calls are otherwise unchanged.

Bug fix

  • PeerHandle no longer dereferences a moved-from Peer in receive(); corrected the header copyright.

Testing

  • Added PointGridSectorizerTest, PeerHandleTest and GuidUtilTest; hardened DisconnectReasonTest against CI scheduler starvation with a bounded retry.

Version 0.9.0

Core

  • Strong-typed PeerGuid. A new enum class PeerGuid : uint64_t names a peer’s RakNetGUID value distinctly from NetworkID (an object id), so the two can no longer be passed interchangeably in a uint64_t-typed signature — removing a class of silent “passed the wrong id” bugs in ReplicaManager3 glue and void(uint64_t) callbacks. Convert with ToPeerGuid() / ToGuid(), and compare against the UNASSIGNED_PEER_GUID sentinel. Being a trivially-copyable 8-byte scoped enum, it serializes byte-identically through BitStream (and therefore VariableDeltaSerializer) to the raw uint64_t it 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. CloseConnection gains a final optional const BitStream *reasonData argument whose bytes are appended right after the ID_DISCONNECTION_NOTIFICATION message 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 + 1 for packet->length - 1 bytes. Only graceful disconnects carry a reason; locally-synthesized notifications (ID_CONNECTION_LOST and 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 inspect data[0] are unaffected.

Bug fix

  • RakPeer::CloseConnection no longer coerces an unresolved target index (-1 from GetIndexFromSystemAddress) to 0 and then reads remoteSystemList[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 DisconnectReasonTest covering reason round-trip, the nullptr default, and the empty-but-non-null BitStream guard.

Version 0.7.0

Plugins

  • Virtual worlds (dimensions) for ReplicaManager3. A new lightweight per-entity / per-observer VirtualWorldId tag scopes visibility at runtime — the SA-MP SetPlayerVirtualWorld / routing-bucket model for instanced interiors such as apartments. 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 the same RM3 WorldId. Derive entities from the new VirtualWorldReplica3 base (mafianet/VirtualWorldReplica3.h); Connection_RM3 gains Get/SetVirtualWorld; ReplicaManager3 gains GetConnectionsInVirtualWorld / GetGuidsInVirtualWorld (recipient-filter helpers for scoping non-replica traffic like chat and RPC) 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. See mafianet/VirtualWorld.h and the Samples/VirtualWorld demo.

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-contained Samples/VirtualWorld smoke test.

  • Fixed the test harness so a subset run (Tests <name>) calls DestroyPeers() 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 forced const methods on derived managers to const_cast away 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 returned Replica3* stays non-const, matching GetConnectionAtIndex. Source-compatible — adding const to a read accessor doesn’t break existing non-const call sites.

Version 0.6.0

Plugins

  • RPC4 handlers now carry user context. RegisterFunction, RegisterSlot, RegisterBlockingFunction and the RPC4GlobalRegistration handler constructors 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 may serve multiple object instances under one identifier. The void* approach preserves RPC4’s zero-external-dependency design.

Bug Fixes

  • RakPeer::CloseConnection no longer dereferences a null rakNetSocket during 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 RPC4ContextTest covering slot, nonblocking, and blocking handler context.

  • Quarantined the flaky ManyClientsOneServerDeallocateBlockingTest under CI while a pre-existing multithreaded teardown race is investigated.

Breaking Changes

  • RPC4 handler signatures gained a trailing void *context parameter, and the registration / global-registration functions take a context argument. There are no compatibility overloads — update handlers and registration calls (pass nullptr when 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 recursive AddUploadsFromSubdirectory. It forwards to the existing FileList::AddFile overload, 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 MafiaNet namespace throughout the library

  • Removed the legacy SLNet compatibility macro and replaced it with an MNet short-hand alias that expands to MafiaNet

  • Dropped stale RakNet namespace-alias references (the alias no longer existed in code) and migrated the remaining sample code to MafiaNet::

  • Collapsed the three header layers inherited from the RakNet/SLikeNet lineage down to the single canonical Source/include/mafianet/ set; the redirect-only Source/*.h and Source/mafianet/*.h stubs were removed and all includes now use the mafianet/... form

Bug Fixes

  • Guarded BitStream’s catch-all Write/Read templates with a std::is_trivially_copyable static_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::string BitStream specializations (wire-compatible with RakString)

  • Fixed Ranking_GetMatches serializing SubmittedMatch through the unsafe catch-all instead of its own Serialize()

Testing

  • Added the BitStreamStringTest regression test

Breaking Changes

  • Legacy include spellings are gone — include public headers via the mafianet/... path (e.g. mafianet/string.h instead of RakString.h)

  • The SLNet namespace macro has been removed — use MafiaNet (or the new MNet shorthand)

  • Serializing a non-trivially-copyable type through the generic BitStream::Write/Read now fails to compile by design; provide an explicit Serialize() or a type specialization

Version 0.4.0

Cross-Platform Support

  • Full macOS and Linux compatibility, including merged Socket2 definitions

  • Removed 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 FetchContent instead of being bundled in-tree

Bug Fixes

  • Fixed undefined behaviour from a negative doubleunsigned cast in congestion control

  • Guard against a null socket in BCS_CLOSE_CONNECTION handling

  • Fixed 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 hints

  • Removed 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() and RequestVoiceChannel() now use RakNetGUID instead of SystemAddress

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