Changelog

All notable changes to MafiaNet are documented here.

Version 0.26.0

RakPeer

  • ``WaitForPackets(timeoutMs)`` blocks until a packet is waiting for Receive(), the timeout passes or the peer shuts down, so a receive thread can sleep instead of polling. It can return true during shutdown: a receive loop exits on IsActive(). Plugin Update() still runs only inside Receive(), so wait with a short timeout.

ReplicaManager3

  • Receive on one thread while the pass runs on another. Update() may run on one thread while Receive() (so OnReceive, OnNewConnection and OnClosedConnection) runs on another. Each connection has a lock of its own: inbound state for a connection waits only while that connection is being served or its lists edited, never for the whole pass. A replica-set lock keeps replicas and the NetworkIDManager table safe from the receive thread. Construction and destruction messages, and connecting and disconnecting, still wait for a running pass. The lock order is documented on Update().

  • ``ReplicaManager3::WorldGuard`` holds the world for a scope, for an owner that walks connections after its pass.

  • ``StateDelivery`` lets a subclass deliver state from a message format of its own under the rules OnSerialize follows.

  • GetConnectionsThatHaveReplicaConstructed(), HasReplicaConstructed() and GetConstructedReplicas() no longer take the world and may be called from Deserialize() during a pass.

  • Behaviour change: Dereference() also takes a replica that left its last world out of the NetworkIDManager; Reference() tracks it again under the same id. State for a replica this manager does not reference is dropped.

  • With two threads, Deserialize() and the download callbacks run under the sender’s connection lock and must not call what takes the world (listed in the header), and a replica must be dereferenced before it is deleted.

DataStructures

  • List’s copy constructor initialises its array pointer when copying an empty list.

Breaking (ABI): RakPeerInterface gains a pure virtual and ReplicaManager3 and Connection_RM3 gain members; consumers must rebuild. Wire-compatible: no message changes.

Version 0.25.1

RakPeer

  • Fix: a remote’s library message ids are only delivered where a conforming peer sends them. Messages the library generates locally are told apart from a remote’s by their first byte, while readers take the id behind an ID_TIMESTAMP prefix as the message. Behind a timestamp, a connected peer’s message now reaches the plugins and the application only if it carries an application id (ID_USER_PACKET_ENUM and up), a ReplicaManager3 id or ID_NAT_CONNECT_AT_TIME. A timestamp with nothing behind it and a bare ID_SESSION_CONFIG_ABANDONED from a remote are dropped.

  • Behaviour change: an application that sent any other library id behind a timestamp no longer receives it.

Wire-compatible: no message changes.

Version 0.25.0

ReplicaManager3

  • Broadcasts are serialized once per pass. A replica whose Replica3::BroadcastsIdentically() returns true is serialized once per serialize pass, with no destination connection, and handed to a connection only when it has something to send, instead of being passed to every connection’s SendSerialize() every pass. Construction is unchanged. Off by default.

  • Connections can be served in parallel. With an RM3SerializeExecutor set through SetSerializeExecutor(), those replicas are serialized first on the calling thread and connections are then served their share of the pass across the executor; everything else is served on the calling thread as before. The executor contract lists what callbacks on its threads may not do.

  • Connection_RM3::SendSerialize() copies a channel from its bytes and leaves the source bitstream untouched.

  • Fix: a list-mode connection no longer leaks a record each time a replica streams out of it. QueryReplicaList’s destroy list took the LastSerializationResult off the connection’s lists without deleting it.

  • Fix: a connection’s own serialization list is read from its records. Entries were resolved through replica->lsr, which every connection overwrote and which could name another connection’s freed record. Replica3::lsr is no longer maintained.

  • Fix: game callbacks can change the world mid-pass. Dereferencing a replica or popping a connection from QuerySerialization(), Serialize() or QuerySerializationList() no longer leaves the pass using freed records or connections, or skipping the next replica or connection; GetMembershipGeneration() tracks those changes. Update() called from inside the pass returns at once instead of starting a second pass, which locked the world mutex twice on POSIX.

  • Breaking (ABI): Replica3, Connection_RM3, LastSerializationResult and ReplicaManager3 gain members.

TCPInterface

  • Fix: the TLS client builds with ``OPEN_SSL_CLIENT_SUPPORT=1`` against OpenSSL 1.1+: TLS_client_method() returns a const SSL_METHOD *.

Wire-compatible: no message changes.

Version 0.24.0

RakVoice

  • Relay hosts forward voice on the network thread. SetRelayHost() now takes the function that relays each ID_RAKVOICE_RELAY_DATA packet, called on RakPeer’s update thread before the packet is queued for Receive(), so a stalled server tick no longer breaks voice up. Passing nullptr stops being a relay host.

  • The codec can run on the network thread. SetNetworkThreadCodec(true) moves encoding, sending, decoding and the reorder timers onto RakPeer’s update thread behind one codec lock, so a stalled client keeps sending its own voice and decoding everyone else’s, and every public RakVoice method can be called from any thread. SetSpeakerFrameHandler() hands each decoded frame straight out instead of buffering it for ReceiveFrameFrom(). Both are off by default.

  • Fix: ``Deinit()`` with offline loopback on no longer frees the loopback channel twice. Its close was delivered back to the plugin inside CloseAllChannels(), which then indexed past the shrunken channel list.

  • Breaking: SetRelayHost(bool) is replaced by SetRelayHost(std::function<void(Packet *)>).

PluginInterface2

  • OnReceiveNetworkThread() sees each message from a connected system on the update thread before it is queued, and UpdateNetworkThread() is ticked once per update cycle before the send queue is drained. Both are opt-in.

RakPeer

  • Fix: constructing a peer on Windows no longer takes ~125 ms. GUID generation slept eight timer ticks for jitter the OS entropy source already made redundant.

Tooling

  • One .clang-format style for the whole tree, pinned to clang-format 18.1.8 and checked in CI.

  • Wire-compatible: no message changes.

Version 0.23.1

VariableDeltaSerializer

  • Fix: a variable whose serialized size changes no longer clears the next variable’s dirty flag. VariableListDeltaTracker::WriteVar advanced its index before clearing the flag, so the resized variable was resent on the next pass for nothing, the following one lost a resend requested through FlagDirtyFromBitArray or a lost-send receipt, and resizing the last variable wrote one past the end of the list.

  • Wire-compatible: no message changes.

Version 0.23.0

RakPeer / FileListTransfer

  • Fix: a shutdown from inside receive dispatch no longer frees what its callers still use. Shutdown() called from a plugin callback, a FileListTransfer download callback, or code handling a packet Receive returned cleared the packet pool under the packet being dispatched, and FileListTransfer::Clear() deleted the receiver whose callback was running, which was then dereferenced and deleted again (an access violation or a 0xC0000374 double free). The pool is now kept while any packet is out, Receive stops a packet whose callbacks shut the peer down, and every FileListTransfer path re-checks its receiver after each callback, which also covers RemoveReceiver and CancelReceive from a callback.

PacketLogger

  • Fix: message names are no longer shifted by one. The name table was missing ID_RAKVOICE_RELAY_DATA, so every id from 48 to 132 printed the next id’s name.

FileList / DirectoryDeltaTransfer

  • Fix: directory walks never terminated on Linux. FileList::AddFilesFromDirectory (and so every DirectoryDeltaTransfer download) looped forever on Linux, allocating until the process was killed. The _findnext emulation copied each entry name with a count equal to the destination size; the strncpy_s shim then read one byte past the end of dirent::d_name to decide whether the copy fit, saw garbage, returned ERANGE and an empty name. The empty name defeated the "."/".." filter and the walk recursed into dir// without end. macOS happened to read a zero byte there and never showed it. _findnext now copies with _TRUNCATE, and the shim reads at most count bytes of the source and rejects a copy that would not fit, as MSVC does, instead of writing its terminator one byte past the destination. Unit tests cover the shim’s bounds and the walk’s termination.

Build

  • CI runs only where it is useful. Build & Test runs on pull requests (and on demand); pushes to master no longer rebuild or retest, since master only changes through a PR that already passed. A release tag triggers the release packaging workflow and the documentation deploy, neither of which runs tests. A new push to a PR cancels that PR’s run still in flight.

  • New CMake options: ``MAFIANET_DISABLED_FEATURES`` and ``MAFIANET_MINIMAL``. The _RAKNET_SUPPORT_<Name> plugin flags from native_feature_includes.h can now be set from CMake (-DMAFIANET_DISABLED_FEATURES="ReplicaManager3;RPC4Plugin" or -DMAFIANET_MINIMAL=ON for the core transport alone). The definitions are PUBLIC on the library targets so consumers see the same configuration. Unknown names are a configure error. A linux-minimal CI job builds the MAFIANET_MINIMAL configuration. See Building MafiaNet.

File names are snake_case. Every header and source under Source/ is now lowercase snake_case (BitStream.h → bit_stream.h, ReliabilityLayer.h → reliability_layer.h, peerinterface.h → peer_interface.h, DS_List.h → ds_list.h, HTTPConnection2.h → http_connection2.h), and the RakNet prefix is gone from file names (RakVoice.h → voice.h, RakPeer.cpp → peer.cpp, CCRakNetUDT.h → cc_udt.h). Class names are unchanged. The umbrella header stays mafianet/mafianet.h and the crypto/ headers were already lowercase. Since almost every rename differs from the old name only by case, no forwarding headers are possible: on Windows and macOS the old spellings still resolve, on Linux every #include "mafianet/..." must be updated (a case-insensitive search-and-replace over the names above). Library headers now include each other only as "mafianet/<name>.h"; a CI check rejects bare quoted includes inside include/mafianet/ so that string.h, time.h and assert.h can never shadow the C headers.

Extras library. EmailSender, HTTPConnection, HTTPConnection2, TelnetTransport, RakNetTransport2, ConsoleServer, CommandParserInterface, RakNetCommandParser, LogCommandParser and PacketConsoleLogger moved out of the core library into MafiaNetExtras (MafiaNet::MafiaNetExtras), built with MAFIANET_BUILD_EXTRAS=ON (forced on by MAFIANET_BUILD_SAMPLES). Headers and include paths are unchanged; code using these classes must link the extras target. See Building MafiaNet.

Deprecated containers. ds_binary_search_tree.h, ds_queue_linked_list.h, ds_huffman_encoding_tree_factory.h, ds_ordered_channel_heap.h and ds_byte_pool.h have no consumer in the library. They now emit a #pragma message on inclusion and will be removed in the next minor release; copy them into your project if you depend on them.

Removed: dead platform code. Xbox 360, PlayStation 3, PlayStation 4, PlayStation Vita and Google Native Client could not be targeted for years; their remaining headers were already empty. Removed in this release:

  • XBox360Includes.h, PS3Includes.h, PS4Includes.h, VitaIncludes.h and the matching empty source files, plus Samples/nacl_sdk.

  • socket.h / RakNetSocket.cpp, the pre-RakNetSocket2 socket class, which had no remaining consumers and survived only as commented-out console code.

  • Every __native_client__, _PS3 / __PS3__ / SN_TARGET_PS3, _PS4 and SN_TARGET_PSP2 preprocessor branch.

  • SocketDescriptor::remotePortRakNetWasStartedOn_PS3_PSP2 and SocketDescriptor::chromeInstance, and RNS2_BerkleyBindParameters::remotePortRakNetWasStartedOn_PS3_PS4_PSP2. These were only read by the removed console and Native Client paths. Code that initialised them must drop those lines; SocketDescriptor is now smaller, so code built against 0.22 must be rebuilt.

  • The RNS2_Windows_Linux_360 mix-in is now RNS2_Windows_Linux and its send function Send_Windows_Linux; both are internal to the socket layer.

The ID_XBOX_360_* and ID_XBOX_LOBBY message identifiers are kept as reserved values so the wire values that follow them do not shift.

Wire-compatible. No message changes.

Version 0.22.1

DirectoryDeltaTransfer

  • Fix: files added with ``AddFile()`` are compared by hash. AddFile stored each upload’s whole contents while a downloader sends the hash of each file it already has, so the comparison never matched and every download re-sent every file added that way. The server also held every such upload in memory, and refused any file over 536MB. AddFile now records the hash and length, as AddUploadsFromSubdirectory does; call ClearUploads() and add a file again after it changes.

  • Fix: streamed downloads are written as they arrive. A file pushed through an IncrementalReadInterface was buffered whole by the downloader and written when its last chunk arrived. Each chunk is now written at its offset as it arrives, so a download costs one chunk of memory however large its files are. A file sent in one piece is written when it completes, as before.

  • Wire-compatible. No message changes.

Version 0.22.0

FileListTransfer

  • New API: ``SetReferencePushWindow(chunksInFlight)``. A transfer read through an IncrementalReadInterface sent one chunk, then waited for the recipient’s ID_FILE_LIST_REFERENCE_PUSH_ACK before the next, so it moved one chunk per round trip whatever the link could carry. With a window of N, Send() primes N chunks and every acknowledgement sends one more, lifting the ceiling to N chunks per round trip. The chunks still go out in order on the transfer’s ordering channel, so recipients need no change. Each in-flight chunk is buffered by the sender. The window is treated as 1 once StartIncrementalReadThreads() has run, since worker threads could then send one transfer’s chunks out of order. The default, 1, keeps the previous behaviour exactly.

  • Wire-compatible. No message changes; a windowed sender works with any recipient. FileListTransfer gains a member, so code built against 0.21 must be rebuilt.

Version 0.21.1

RakVoice

  • Fix: nothing is sent while a talker is silent. With VAD on, Opus’s DTX stops coding a silent talker, but every 400ms it still codes one full frame to refresh the far end’s comfort noise. Only the 1-2 byte DTX packets were dropped, so those refresh frames went out; the receiver has no comfort-noise generation and cannot tell DTX from the talker stopping, so each arrived alone and played as an isolated burst of noise in the middle of the silence. A frame coded while the encoder reports OPUS_GET_IN_DTX is now dropped too. The decision lives in the new protected EncodeFrame().

  • Wire-compatible. Fewer frames are sent; their format is unchanged.

Version 0.21.0

RakPeer: session handshake for admission control

  • New API: ``SetMaximumPendingSessions(total, perAddress)``. A peer in the session handshake used to count against SetMaximumIncomingConnections(), so peers that stalled it – or an interactive server still deciding on them – took the slots meant for players. With a pool, every remote-initiated peer that is not yet CONNECTED, from its first open-connection request on, counts against total instead, and at most perAddress of them may come from one IP. The incoming limit then bounds CONNECTED peers only: a handshake may start on a full server, so an interactive server can queue, and an AcceptSession() that would exceed the limit refuses the peer instead. Startup()’s maxConnections must leave room for both. total 0, the default, keeps the previous accounting exactly.

  • New API: ``SetSessionTimeout()``. Bounds how long a session handshake waits for its answer, on either end, instead of the connection timeout. 0, the default, keeps using the connection timeout.

  • New API: ``SendSessionStatus()`` and ``ID_SESSION_CONFIG_STATUS``. An interactive server can tell a peer awaiting its decision what is happening (“checking the whitelist”, a queue position). The client receives it before any connection is reported, and it restarts the session timeout on both ends. A client cannot push one at a server.

  • New: ``ID_SESSION_CONFIG_ABANDONED``. An interactive server is told when a request it was holding goes away unanswered – the client left, the transport dropped, or the session timeout passed – so it can drop the decision it was working on. Sent once, never for a request the application answered.

  • New header: ``session_admission.h``. The slot accounting as pure functions, unit tested directly.

  • New guide: Admission and Queues. How the pool, the session timeout, status and abandonment fit together, with a complete queue.

  • Wire-compatible. RAKNET_PROTOCOL_VERSION stays at 7; both message ids take reserved slots (ID_RESERVED_6 and ID_RESERVED_7), so every other id keeps its value. RakPeerInterface gains virtual functions, so code built against 0.20 must be rebuilt.

Version 0.20.0

ReplicaManager3

  • Fix: no message for a channel group with nothing to send. Connection_RM3::SendSerialize split the channels into messages wherever neighbouring channels had different send parameters, and sent every message it opened. A state-only change on a replica with an unreliable pose channel and a reliable state channel also sent an empty unreliable message for the pose channel. Channels are now grouped by their send parameters wherever they sit, and a group is sent only when one of its channels is selected and carries data. Channels that share parameters without being adjacent now share one message.

  • Fix: world 255 has a slot. worldsArray held 255 entries for a uint8_t WorldId, and OnReceive indexes it with the world id read off the wire before checking it, so a packet naming world 255 read past the array.

  • Fix: ``GetWorldCount()`` and ``GetWorldIdAtIndex()`` are defined. Both were declared in the public header and never implemented, so any caller failed to link.

TwoWayAuthentication

  • New API: ``SetTimeout()`` / ``GetTimeout()``. One value bounds both how long a Challenge() waits for its answer and how long a nonce handed to another system stays redeemable, so the two ends of a handshake cannot disagree. The default, TWO_WAY_AUTHENTICATION_DEFAULT_TIMEOUT_MS, is five seconds; challenges used to time out after three to six. Expiry is now exact rather than checked every three seconds.

  • Fix: every expired nonce is removed at once. NonceGenerator::Update removed at most one per call, so a busy peer kept expired nonces long past their lifetime. It now takes the lifetime as an argument.

StatisticsHistoryPlugin

  • New API: ``SetSampleInterval()``. RakPeer updates every plugin on each Receive(), so a busy receive loop sampled every connection once per packet. The interval sets a minimum time between samples; the default, 0, samples on every update as before.

Documentation

  • The TwoWayAuthentication page listed setters that do not exist; the statistics guide attached StatisticsHistory, which is not a plugin. Both are corrected.

  • Non-breaking on the wire. RAKNET_PROTOCOL_VERSION stays at 7, no message ids move, and ReplicaManager3 serialize messages keep their layout. NonceGenerator::Update gains a parameter and ReplicaManager3’s layout changes, so code built against 0.19 must be rebuilt.

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. voice.h moved to mafianet/voice.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/peer_handle.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/virtual_world_replica3.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/virtual_world.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