Changelog

All notable changes to MafiaNet are documented here.

Version 0.19.0

RakVoice

  • Relayed frames are put back in order before they are decoded. DecodeIntoChannel decoded each frame on arrival and concealed any sequence gap right then. On an unordered link every reordering became a concealed frame followed by the real one discarded as late, and a loss after a stall was concealed only once the next frame landed – after the silence the stall had already cost, so the listener heard a cut followed by the decaying echo of the word before it. A frame behind a gap is now held for up to RAKVOICE_REORDER_WAIT_MS (40ms) for the missing one, or until RAKVOICE_REORDER_MAX_HELD later frames have arrived. Update() settles gaps whose wait has run out.

  • Gaps are filled in place. A gap that stays open is filled so everything after it keeps its timing. The frame right before the held one is rebuilt from that frame’s in-band FEC; any before it are extrapolated while that can still land in time (RAKVOICE_CONCEAL_WINDOW_MS, 250ms since the last decoded frame), and silence after, with the decoder reset rather than left to extrapolate from a word the listener may have heard cut off. At most RAKVOICE_MAX_CONCEALED_FRAMES are filled; the rest of a longer gap is skipped.

  • In-band FEC is on. The encoder now sets OPUS_SET_INBAND_FEC(1) and plans for RAKVOICE_FEC_LOSS_PERCENT (10%) loss, so each frame carries a low-bitrate copy of the one before it. Packets stay standard Opus: an older receiver decodes them as before and ignores the extra data.

  • Fix: a speaker’s receive sequence is seeded from the first frame heard. It started at zero, so every frame of a sender whose counter had passed 32767 – about eleven minutes of talking – read as late and was dropped until the counter wrapped. That happened whenever a decoder was re-created: after the 30s relay reap, or after eviction under SetMaxDecodedSpeakers.

  • VoiceChannel gains the receive-ordering state (incomingSeeded, lastEmit, held), about 20KB per channel.

Peer

  • Fix: two peers in one process no longer share a GUID on Linux and macOS. Off Windows the GUID was gettimeofday() in microseconds and nothing else, so two peers constructed within the same microsecond – a server and a client created back to back – got the same one. A peer then resolved the other’s GUID to itself, and CloseConnection() silently did nothing. The GUID now also mixes in std::random_device and a per-process counter, and is never 0 or UNASSIGNED_RAKNET_GUID.

  • Non-breaking. RAKNET_PROTOCOL_VERSION stays at 7, no message ids move, and the relay wire format is unchanged.

Version 0.18.0

RakVoice

  • New API: ``RakVoice::SetOrderingChannels(frameChannel, controlChannel)``. RakVoice sent every audio frame and every channel open/close control message on ordering channel 0, the channel most applications also use for their own sequenced stream. A voice frame that overtook one of the application’s sequenced messages made the receiver discard that message as stale, and a lost control message held back every later sequenced message on the channel until it was retransmitted. Both channels default to 0, so existing callers keep the old behaviour. Values outside 0..NUMBER_OF_ORDERED_STREAMS-1 are rejected and the call returns false with both channels unchanged.

  • The frame channel applies to the relay-mode UnreliableSequenced send from a client to its relay host. Frames the relay host forwards, and frames sent directly between peers, go out plain Unreliable and carry no ordering channel; the relay header’s per-speaker sequence number orders them, so several speakers never compete for one sequenced stream.

Documentation

  • The reliability guide now explains what an ordering channel protects against – a lost ReliableOrdered message holds back every later sequenced message on its channel, and a sequenced stream discards by channel rather than by object – gives the layout that follows, and tables where each plugin’s channel is set. Examples that put a position stream on the same channel as reliable events are corrected, and the RakVoice, RPC4, ReplicaManager3, FileListTransfer and DirectoryDeltaTransfer pages each gain an ordering-channel section.

  • Non-breaking. RAKNET_PROTOCOL_VERSION stays at 7, no message ids move, and ordering channels are a sender-side choice that any receiver decodes.

Version 0.17.0

Reliability layer

  • In-session path-MTU black-hole detection and recovery – the follow-up 0.16.0 deferred. The handshake still probes the path in one direction only, so a tunnel that drops large datagrams on the return path only (OpenVPN’s default configuration does not fragment oversized UDP, and its drops are direction-asymmetric), or a path that shrinks mid-session, passed the probe and then hung the connection on the first split payload while the same too-large datagram was resent until timeout. The reliability layer now recognises the black-hole signature – a reliable packet that has gone unacked through its resend budget and that would actually shrink if the MTU dropped a rung – steps the connection’s MTU one rung down the same ladder the handshake probes, and re-splits every queued message that no longer fits. Detection completes within a few retransmission timeouts, well inside the connection timeout.

  • Packets that already fit the next rung down can never trigger a step-down, no matter how often they are resent: resending them at the same size after a step-down would change nothing on the wire, so their failures indicate loss, not size. Ordinary packet loss therefore never shrinks a healthy connection’s MTU.

  • Stuck split messages are rebuilt at full length from the shared data block their fragments reference and re-sent re-split under a fresh split id, with ordering and sequencing indices, exact bit length, and ack-receipt serials preserved – the receiver’s stalled partial reassembly is superseded in place and ordered channels resume exactly where they blocked. Oversized unsplit reliable messages are simply split; oversized unsplit unreliable messages are dropped, which is what the network was already doing to them.

  • New API: ReliabilityLayer::GetCurrentMtuBytes(), and RakPeerInterface::GetMTUSize() now reports the stepped-down value after a recovery instead of the stale handshake result.

  • Non-breaking. RAKNET_PROTOCOL_VERSION stays at 7, no message ids move, and the split/reassembly wire format is unchanged – the new per-fragment bookkeeping is sender-side only. A step-down is permanent for the life of the connection; the MTU never probes back up.

Version 0.16.0

Peer

  • Fixed: tunnelled clients could not connect, because the negotiated MTU black-holed their traffic. The connection handshake probes the path in one direction only – the connecting peer pads ID_OPEN_CONNECTION_REQUEST_1 down the MTU ladder and the accepting peer echoes back whatever size arrived – and the result is then frozen for the life of the connection and applied to both directions. Nothing re-probes, and nothing detects a path-MTU black hole afterwards, so a datagram too large for the return path is resent at the same size until the connection times out. Every handshake packet is small enough to survive that, so the failure landed on the first split payload instead: the peer connected and then hung or dropped. Which tunnelled peers it hit depended entirely on their exit node’s encapsulation overhead, which is why it looked intermittent.

  • MAXIMUM_MTU_SIZE 1492 -> 1400, so the top rung clears WireGuard (1420) and typical IPSec/IKEv2 (1400) on a 1500-byte path without the peer having to discover anything. The ladder gains 1280 – the IPv6 minimum MTU, and where WireGuard-derived tunnels commonly sit – and 1024, so a peer that does step down gives up far less payload capacity than the old 1492 -> 1200 jump cost it. Lowering MAXIMUM_MTU_SIZE further is safe; raising it past ~1420 reintroduces the failure, and a unit test now fails if it is.

  • Security: an MTU reported by a remote peer is clamped to ``MAXIMUM_MTU_SIZE``. It arrives in ID_OPEN_CONNECTION_REQUEST_2 / ID_OPEN_CONNECTION_REPLY_2 before any authentication and sizes every datagram the reliability layer builds into MAXIMUM_MTU_SIZE-byte buffers, so a larger value wrote past the end of them. A peer built against a higher cap produces one; a hostile peer can name any uint16. The only guard was a RakAssert, compiled out of exactly the builds that ship – a release build would adopt an MTU of 65535 from a single forged datagram.

  • Fixed: a connection attempt whose datagram the local interface refused outright spent that MTU rung’s entire attempt budget on sends that never left the machine. The check compared Send()’s return value against 10040, which sendto never yields – it yields SOCKET_ERROR – so the branch was dead. New portable helpers RNS2_GetLastSocketError() and RNS2_IsDatagramTooLargeError() read the real error instead.

  • Fixed: a single sendto blocking for over 100 ms abandoned the whole connection attempt when the peer was already on the lowest MTU rung, turning a transient stall on a virtual adapter – a tunnel renegotiating, a full transmit queue – into ID_CONNECTION_ATTEMPT_FAILED with attempts to spare. It still drops to the lowest MTU, but keeps trying.

  • Fixed: Connect() divided sendConnectionAttemptCount by the number of MTU rungs and enforced no lower bound on it, so any value below the rung count divided by zero on the first tick of the network thread. Latent only because the default (12) exceeded the ladder.

  • Non-breaking. RAKNET_PROTOCOL_VERSION stays at 7 and no message ids move. Two peers converge on the smaller of their two caps, because the accepting side clamps to its own MAXIMUM_MTU_SIZE before replying and both sides clamp what they are told – so a server rebuilt on 0.16.0 caps every connection, including peers still built against 0.15.0, with no client update required.

Version 0.15.0

Peer

  • Session handshake: connection reporting is now gated on a session-config exchange. An opaque application payload is exchanged in both directions after the transport connection is up but before either side reports a connection. ID_CONNECTION_REQUEST_ACCEPTED and ID_NEW_INCOMING_CONNECTION are withheld until the exchange completes, so those packets mean “the remote peer’s session payload is in hand” – an application can no longer act on a connection whose configuration has not arrived yet.

  • New API, modelled on SetOfflinePingResponse/GetOfflinePingResponse: SetSessionConfig / GetSessionConfig / GetRemoteSessionConfig, plus SetSessionConfigInteractive / AcceptSession / RejectSession for servers that want to inspect a client’s payload before admitting it. Static mode needs no application code beyond SetSessionConfig; interactive mode surfaces ID_SESSION_CONFIG_REQUEST through the normal Receive() loop – no network-thread callbacks. The exchange is client-first, so a refused peer is refused having received nothing.

  • The handshake payload crosses the wire before any application-level authentication, so it was hardened accordingly: admission control counts peers still in the handshake (a stalled handshake cannot push past SetMaximumIncomingConnections invisibly), server-to-client handshake messages are bound to the connection’s initiator (a client cannot forge ID_CONNECTION_ATTEMPT_FAILED into a listening server’s queue), GetRemoteSessionConfig() buffers always carry a NUL terminator beyond the reported length, payloads are capped at 64 KB in both directions, and application traffic is blocked across a connection that has not been reported – outbound broadcasts skip handshaking peers, directed sends to them are refused, and inbound application data from them is dropped. A rejected peer never produces ID_DISCONNECTION_NOTIFICATION for a connection the application was never told existed. See advanced/session-handshake-security.rst for the trust model.

  • Breaking: RAKNET_PROTOCOL_VERSION 6 -> 7. Peers built against an older MafiaNet are rejected during the offline connection phase with ID_INCOMPATIBLE_PROTOCOL_VERSION rather than stalling in the new handshake state. The three new message ids are carved out of the reserved block, so ID_USER_PACKET_ENUM does not move.

  • Fixed: GetNumberOfAddresses() always returned MAXIMUM_NUMBER_OF_INTERNAL_IDS (10): the loop’s unassigned-entry check was dead code, so callers iterating GetLocalIP(0..count-1) printed UNASSIGNED_SYSTEM_ADDRESS garbage entries. Inherited from SLikeNet.

RPC4

  • Fixed: the four RPC4GlobalRegistration constructors never wrote a terminating NUL, so a function name of exactly the maximum length filled the buffer and the later strlen read past the array. The copy is now clamped to what fits and always terminated; RakAssert still flags truncation in debug builds.

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_PRIORITY → MafiaNet::Priority::High, RELIABLE_ORDERED → MafiaNet::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 double → unsigned 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