diff --git a/SSMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs b/SSMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
index 97d0ca4c..4f5c567c 100644
--- a/SSMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
+++ b/SSMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using SSMP.Collection;
using SSMP.Networking.Packet;
+using SSMP.Networking.Packet.Connection;
using SSMP.Networking.Packet.Update;
namespace SSMP.Api.Client.Networking;
@@ -55,10 +56,12 @@ public void CommitPacketHandlers() {
}
// Assign the addon packet info in the dictionary of the client update packet
- ClientUpdatePacket.AddonPacketInfoDict[ClientAddon.Id.Value] = new AddonPacketInfo(
+ var addonPacketInfo = new AddonPacketInfo(
PacketInstantiator!,
PacketIdSize
);
+ ClientUpdatePacket.AddonPacketInfoDict[ClientAddon.Id.Value] = addonPacketInfo;
+ ClientConnectionPacket.AddonPacketInfoDict[ClientAddon.Id.Value] = addonPacketInfo;
foreach (var idHandlerPair in PacketHandlers) {
PacketManager.RegisterClientAddonUpdatePacketHandler(
diff --git a/SSMP/Api/Client/Networking/ClientAddonNetworkSender.cs b/SSMP/Api/Client/Networking/ClientAddonNetworkSender.cs
index 1b74cd95..c62bc82b 100644
--- a/SSMP/Api/Client/Networking/ClientAddonNetworkSender.cs
+++ b/SSMP/Api/Client/Networking/ClientAddonNetworkSender.cs
@@ -1,6 +1,7 @@
using System;
using SSMP.Networking.Client;
using SSMP.Networking.Packet;
+using SSMP.Networking.Packet.Connection;
namespace SSMP.Api.Client.Networking;
@@ -61,7 +62,8 @@ public void SendSingleData(TPacketId packetId, IPacketData packetData) {
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- InvalidPacketIdMsg);
+ InvalidPacketIdMsg
+ );
}
if (!_clientAddon.Id.HasValue) {
@@ -87,7 +89,8 @@ TPacketData packetData
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- InvalidPacketIdMsg);
+ InvalidPacketIdMsg
+ );
}
if (!_clientAddon.Id.HasValue) {
@@ -101,4 +104,65 @@ TPacketData packetData
packetData
);
}
+
+ ///
+ public void SendChunkData(TPacketId packetId, IPacketData packetData) {
+ var (idValue, addonId) = ValidateCommon(packetId);
+ _netClient.UpdateManager.SendChunkPacket(BuildPacket(idValue, addonId, packetData));
+ }
+
+ ///
+ /// Validates the common client-side preconditions required before sending chunk data.
+ ///
+ /// The addon packet identifier to validate and resolve.
+ ///
+ /// The resolved packet ID byte value and the current addon ID.
+ ///
+ ///
+ /// Thrown if the client is not connected, the addon has no assigned ID, or the packet ID is invalid.
+ ///
+ private (byte idValue, byte addonId) ValidateCommon(TPacketId packetId) {
+ if (!_netClient.IsConnected) {
+ throw new InvalidOperationException(NotConnectedMsg);
+ }
+
+ if (!_clientAddon.Id.HasValue) {
+ throw new InvalidOperationException(NoClientAddonId);
+ }
+
+ return !PacketIdLookup.TryGetValue(packetId, out var idValue)
+ ? throw new InvalidOperationException(InvalidPacketIdMsg)
+ : (idValue, _clientAddon.Id.Value);
+ }
+
+ ///
+ /// Builds a network packet containing addon packet data for the given addon and packet ID.
+ ///
+ /// The resolved packet ID byte value.
+ /// The addon ID that owns the packet data.
+ /// The packet payload to send.
+ ///
+ /// A constructed packet ready to be enqueued for chunked sending.
+ ///
+ private Packet BuildPacket(byte idValue, byte addonId, IPacketData packetData) {
+ var connectionPacket = new ServerConnectionPacket();
+ var addonPacketData = new AddonPacketData(_packetIdSize) {
+ PacketData = { [idValue] = packetData }
+ };
+
+ connectionPacket.SetSendingAddonPacketData(addonId, addonPacketData);
+
+ var packet = new Packet();
+ connectionPacket.CreatePacket(packet);
+
+ if (packet.Length <= ushort.MaxValue) {
+ throw new ArgumentException(
+ $"Addon packet data size ({packet.Length} bytes) is not larger than ushort.MaxValue ({ushort.MaxValue}). " +
+ $"For payloads smaller than or equal to ushort.MaxValue, please use standard updates instead of chunk transport.",
+ nameof(packetData)
+ );
+ }
+
+ return packet;
+ }
}
diff --git a/SSMP/Api/Client/Networking/IClientAddonNetworkSender.cs b/SSMP/Api/Client/Networking/IClientAddonNetworkSender.cs
index 6705438c..91b066ce 100644
--- a/SSMP/Api/Client/Networking/IClientAddonNetworkSender.cs
+++ b/SSMP/Api/Client/Networking/IClientAddonNetworkSender.cs
@@ -29,4 +29,12 @@ void SendCollectionData(
TPacketId packetId,
TPacketData packetData
) where TPacketData : IPacketData, new();
+
+ ///
+ /// Send a single instance of IPacketData over the network through the chunk system with the given packet ID.
+ /// This should be used for large packets (exceeding 64 KiB).
+ ///
+ /// The packet ID.
+ /// An instance of IPacketData to send.
+ void SendChunkData(TPacketId packetId, IPacketData packetData);
}
diff --git a/SSMP/Api/Server/Networking/IServerAddonNetworkSender.cs b/SSMP/Api/Server/Networking/IServerAddonNetworkSender.cs
index a8b28157..45d0d8e1 100644
--- a/SSMP/Api/Server/Networking/IServerAddonNetworkSender.cs
+++ b/SSMP/Api/Server/Networking/IServerAddonNetworkSender.cs
@@ -78,4 +78,33 @@ void BroadcastCollectionData(
TPacketId packetId,
TPacketData packetData
) where TPacketData : IPacketData, new();
+
+ ///
+ /// Send a single instance of IPacketData with the given packet ID over the network to the player
+ /// with the given ID through the chunk system.
+ /// This should be used for large packets (exceeding 64 KiB).
+ ///
+ /// The packet ID.
+ /// An instance of IPacketData to send.
+ /// The ID of the player.
+ void SendChunkData(TPacketId packetId, IPacketData packetData, ushort playerId);
+
+ ///
+ /// Send a single instance of IPacketData with the given packet ID over the network to the players
+ /// with the given IDs through the chunk system.
+ /// This should be used for large packets (exceeding 64 KiB).
+ ///
+ /// The packet ID.
+ /// An instance of IPacketData to send.
+ /// The IDs of the players.
+ void SendChunkData(TPacketId packetId, IPacketData packetData, params ushort[] playerIds);
+
+ ///
+ /// Send a single instance of IPacketData with the given packet ID over the network to all connected
+ /// players through the chunk system.
+ /// This should be used for large packets (exceeding 64 KiB).
+ ///
+ /// The packet ID.
+ /// An instance of IPacketData to send.
+ void BroadcastChunkData(TPacketId packetId, IPacketData packetData);
}
diff --git a/SSMP/Api/Server/Networking/ServerAddonNetworkSender.cs b/SSMP/Api/Server/Networking/ServerAddonNetworkSender.cs
index 50e1831d..ad97faaa 100644
--- a/SSMP/Api/Server/Networking/ServerAddonNetworkSender.cs
+++ b/SSMP/Api/Server/Networking/ServerAddonNetworkSender.cs
@@ -1,6 +1,7 @@
using System;
using SSMP.Api.Client.Networking;
using SSMP.Networking.Packet;
+using SSMP.Networking.Packet.Connection;
using SSMP.Networking.Server;
namespace SSMP.Api.Server.Networking;
@@ -62,7 +63,8 @@ public void SendSingleData(TPacketId packetId, IPacketData packetData, ushort pl
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- PacketIdInvalidExceptionMsg);
+ PacketIdInvalidExceptionMsg
+ );
}
var updateManager = _netServer.GetUpdateManagerForClient(playerId);
@@ -97,7 +99,8 @@ public void BroadcastSingleData(TPacketId packetId, IPacketData packetData) {
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- PacketIdInvalidExceptionMsg);
+ PacketIdInvalidExceptionMsg
+ );
}
if (!_serverAddon.Id.HasValue) {
@@ -105,13 +108,14 @@ public void BroadcastSingleData(TPacketId packetId, IPacketData packetData) {
}
_netServer.SetDataForAllClients(updateManager => {
- updateManager?.SetAddonData(
- _serverAddon.Id.Value,
- idValue,
- _packetIdSize,
- packetData
- );
- });
+ updateManager?.SetAddonData(
+ _serverAddon.Id.Value,
+ idValue,
+ _packetIdSize,
+ packetData
+ );
+ }
+ );
}
///
@@ -126,7 +130,8 @@ ushort playerId
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- PacketIdInvalidExceptionMsg);
+ PacketIdInvalidExceptionMsg
+ );
}
var updateManager = _netServer.GetUpdateManagerForClient(playerId);
@@ -168,7 +173,8 @@ TPacketData packetData
if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
- PacketIdInvalidExceptionMsg);
+ PacketIdInvalidExceptionMsg
+ );
}
if (!_serverAddon.Id.HasValue) {
@@ -176,12 +182,101 @@ TPacketData packetData
}
_netServer.SetDataForAllClients(updateManager => {
- updateManager?.SetAddonDataAsCollection(
- _serverAddon.Id.Value,
- idValue,
- _packetIdSize,
- packetData
+ updateManager?.SetAddonDataAsCollection(
+ _serverAddon.Id.Value,
+ idValue,
+ _packetIdSize,
+ packetData
+ );
+ }
+ );
+ }
+
+ ///
+ public void SendChunkData(TPacketId packetId, IPacketData packetData, ushort playerId) {
+ var (idValue, addonId) = ValidateCommon(packetId);
+
+ var updateManager = _netServer.GetUpdateManagerForClient(playerId);
+ if (updateManager == null) {
+ throw new InvalidOperationException($"Player with ID '{playerId}' is not connected");
+ }
+
+ updateManager.SendChunkPacket(BuildPacket(idValue, addonId, packetData));
+ }
+
+ ///
+ public void SendChunkData(TPacketId packetId, IPacketData packetData, params ushort[] playerIds) {
+ var (idValue, addonId) = ValidateCommon(packetId);
+ var packet = BuildPacket(idValue, addonId, packetData);
+
+ foreach (var playerId in playerIds) {
+ var updateManager = _netServer.GetUpdateManagerForClient(playerId);
+ if (updateManager == null) {
+ throw new InvalidOperationException($"Player with ID '{playerId}' is not connected");
+ }
+
+ updateManager.SendChunkPacket(packet);
+ }
+ }
+
+ ///
+ public void BroadcastChunkData(TPacketId packetId, IPacketData packetData) {
+ var (idValue, addonId) = ValidateCommon(packetId);
+ var packet = BuildPacket(idValue, addonId, packetData);
+ _netServer.SetDataForAllClients(updateManager => updateManager.SendChunkPacket(packet));
+ }
+
+ ///
+ /// Validates the common server-side preconditions required before sending chunk data.
+ ///
+ /// The addon packet identifier to validate and resolve.
+ ///
+ /// The resolved packet ID byte value and the current addon ID.
+ ///
+ ///
+ /// Thrown if the server is not started, the addon has no assigned ID, or the packet ID is invalid.
+ ///
+ private (byte idValue, byte addonId) ValidateCommon(TPacketId packetId) {
+ if (!_netServer.IsStarted) {
+ throw new InvalidOperationException(ServerNotStartedExceptionMsg);
+ }
+
+ if (!_serverAddon.Id.HasValue) {
+ throw new InvalidOperationException(NoAddonIdMsg);
+ }
+
+ return !PacketIdLookup.TryGetValue(packetId, out var idValue)
+ ? throw new InvalidOperationException(PacketIdInvalidExceptionMsg)
+ : (idValue, _serverAddon.Id.Value);
+ }
+
+ ///
+ /// Builds a network packet containing addon packet data for the given addon and packet ID.
+ ///
+ /// The resolved packet ID byte value.
+ /// The addon ID that owns the packet data.
+ /// The packet payload to send.
+ ///
+ /// A constructed packet ready to be enqueued for chunked sending.
+ ///
+ private Packet BuildPacket(byte idValue, byte addonId, IPacketData packetData) {
+ var connectionPacket = new ClientConnectionPacket();
+ var addonPacketData = new AddonPacketData(_packetIdSize) {
+ PacketData = { [idValue] = packetData }
+ };
+ connectionPacket.SetSendingAddonPacketData(addonId, addonPacketData);
+
+ var packet = new Packet();
+ connectionPacket.CreatePacket(packet);
+
+ if (packet.Length <= ushort.MaxValue) {
+ throw new ArgumentException(
+ $"Addon packet data size ({packet.Length} bytes) is not larger than ushort.MaxValue ({ushort.MaxValue}). " +
+ $"For payloads smaller than or equal to ushort.MaxValue, please use standard updates instead of chunk transport.",
+ nameof(packetData)
);
- });
+ }
+
+ return packet;
}
}
diff --git a/SSMP/Networking/Chunk/ChunkReceiver.cs b/SSMP/Networking/Chunk/ChunkReceiver.cs
index 22d7c125..1b69bf9e 100644
--- a/SSMP/Networking/Chunk/ChunkReceiver.cs
+++ b/SSMP/Networking/Chunk/ChunkReceiver.cs
@@ -90,7 +90,7 @@ public void ProcessReceivedData(SliceData sliceData) {
if (sliceData.ChunkId == (byte) (_chunkId + 1)) {
//Logger.Debug($"Received new chunk with ID: {sliceData.ChunkId}");
SoftReset();
-
+
_chunkId += 1;
_isReceiving = true;
_numSlices = sliceData.NumSlices;
@@ -121,21 +121,22 @@ public void ProcessReceivedData(SliceData sliceData) {
// Copy over the data from the received slice into the chunk data array at the correct position
Array.Copy(
- sliceData.Data,
- 0,
- _chunkData,
- sliceData.SliceId * ConnectionManager.MaxSliceSize,
+ sliceData.Data,
+ 0,
+ _chunkData,
+ sliceData.SliceId * ConnectionManager.MaxSliceSize,
sliceData.Data.Length
);
-
- SendAckData();
- // If this is the last slice in the chunk, we can calculate the chunk size
+ // Whenever the last-ID slice arrives, correct the chunk size to account for its (potentially partial) length.
+ // This must happen before the assembly check below so that out-of-order delivery is handled correctly.
if (sliceData.SliceId == _numSlices - 1) {
_chunkSize = (_numSlices - 1) * ConnectionManager.MaxSliceSize + sliceData.Data.Length;
- //Logger.Debug($"Received last slice in chunk, chunk size: {_chunkSize}");
+ //Logger.Debug($"Corrected chunk size after receiving last-ID slice: {_chunkSize}");
}
+ SendAckData();
+
if (_numReceivedSlices == _numSlices) {
var byteArray = new byte[_chunkSize];
Array.Copy(
@@ -146,7 +147,7 @@ public void ProcessReceivedData(SliceData sliceData) {
_chunkSize
);
var packet = new Packet.Packet(byteArray);
-
+
ChunkReceivedEvent?.Invoke(packet);
_isReceiving = false;
diff --git a/SSMP/Networking/Chunk/ChunkSender.cs b/SSMP/Networking/Chunk/ChunkSender.cs
index b533146b..6087f86d 100644
--- a/SSMP/Networking/Chunk/ChunkSender.cs
+++ b/SSMP/Networking/Chunk/ChunkSender.cs
@@ -14,17 +14,13 @@ namespace SSMP.Networking.Chunk;
/// The ID of the slice.
/// The number of slices in the chunk.
/// The slice data.
-internal delegate void SetSliceDataDelegate(byte chunkId, byte sliceId, byte numSlices, byte[] data);
+internal delegate void SetSliceDataDelegate(byte chunkId, ushort sliceId, ushort numSlices, byte[] data);
///
/// Class that processes and manages chunks by sending slices of those chunks and receiving acknowledgements for those
/// slices. Uses delegate injection instead of inheritance for flexibility.
///
internal sealed class ChunkSender {
- ///
- /// The number of milliseconds to wait between sending slices.
- ///
- private const int WaitMillisBetweenSlices = 20;
///
/// The number of milliseconds to wait before re-sending a slice.
///
@@ -254,6 +250,7 @@ private void StartSends(CancellationToken cancellationToken) {
Array.Clear(_sliceStopwatches, 0, _sliceStopwatches.Length);
Array.Clear(_acked, 0, _acked.Length);
_numAckedSlices = 0;
+ _currentSliceId = 0;
var packetBytes = packet.ToArray();
@@ -292,23 +289,22 @@ private void StartSends(CancellationToken cancellationToken) {
break;
}
- long waitMillisNextSlice;
+ long waitMillisNextSlice = 0;
// Get the stopwatch for this slice, and check whether we have already sent this slice not too long ago
- // If so, we wait longer before resending the slice. Otherwise, we default to the normal send rate.
+ // If so, we wait longer before resending the slice.
sliceStopwatch = _sliceStopwatches[_currentSliceId];
- if (sliceStopwatch == null) {
- waitMillisNextSlice = WaitMillisBetweenSlices;
- } else {
+ if (sliceStopwatch != null) {
waitMillisNextSlice = WaitMillisResendSlice - sliceStopwatch.ElapsedMilliseconds;
- if (waitMillisNextSlice < 0) {
- waitMillisNextSlice = WaitMillisBetweenSlices;
- }
}
+ if (waitMillisNextSlice <= 0) {
+ continue;
+ }
+
//Logger.Debug($"Waiting on handle for next slice: {waitMillisNextSlice}");
try {
- _sliceWaitHandle.Wait((int) waitMillisNextSlice, cancellationToken);
+ _sliceWaitHandle.Wait((int)waitMillisNextSlice, cancellationToken);
} catch (OperationCanceledException) {
//Logger.Debug("Wait operation was cancelled, breaking");
break;
@@ -342,7 +338,7 @@ private void SendNextSlice() {
Array.Copy(_chunkData, startIndex, sliceBytes, 0, sliceBytes.Length);
}
- SetSliceData(_chunkId, (byte) _currentSliceId, (byte) _numSlices, sliceBytes);
+ SetSliceData(_chunkId, (ushort) _currentSliceId, (ushort) _numSlices, sliceBytes);
}
///
@@ -375,7 +371,7 @@ private bool TryGetNextSliceToSend() {
/// The ID of the slice.
/// The number of slices in this chunk.
/// The byte array containing the data of the slice.
- private void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) {
+ private void SetSliceData(byte chunkId, ushort sliceId, ushort numSlices, byte[] data) {
_setSliceData(chunkId, sliceId, numSlices, data);
}
}
diff --git a/SSMP/Networking/Client/ClientConnectionManager.cs b/SSMP/Networking/Client/ClientConnectionManager.cs
index 542f4170..11a14b66 100644
--- a/SSMP/Networking/Client/ClientConnectionManager.cs
+++ b/SSMP/Networking/Client/ClientConnectionManager.cs
@@ -102,7 +102,20 @@ private void OnChunkReceived(Packet.Packet packet) {
return;
}
- // Let the packet manager handle the connection packet, which will invoke the relevant data handlers
- PacketManager.HandleClientConnectionPacket(connectionPacket);
+ if (connectionPacket.HasNormalData) {
+ // Let the packet manager handle the connection packet,
+ // which will invoke the relevant data handlers
+ PacketManager.HandleClientConnectionPacket(connectionPacket);
+ return;
+ }
+
+ // No ServerInfo connection data exists,
+ // meaning this is a real-time gameplay addon chunk
+ foreach (var (addonId, value) in connectionPacket.GetAddonPacketData()) {
+ foreach (var (packetId, packetData) in value.PacketData) {
+ // Let the packet manager handle the connection packet, which will invoke the relevant data handlers
+ PacketManager.HandleClientAddonPacketSingle(addonId, packetId, packetData);
+ }
+ }
}
}
diff --git a/SSMP/Networking/Client/ClientUpdateManager.cs b/SSMP/Networking/Client/ClientUpdateManager.cs
index 8d2c878b..7c09e7d6 100644
--- a/SSMP/Networking/Client/ClientUpdateManager.cs
+++ b/SSMP/Networking/Client/ClientUpdateManager.cs
@@ -58,23 +58,21 @@ private PacketDataCollection GetOrCreateCollection(ServerUpdatePacketId pa
}
///
- /// Set slice data in the current packet.
+ /// Send a slice packet immediately, bypassing the gameplay tick loop.
///
/// The ID of the chunk the slice belongs to.
/// The ID of the slice within the chunk.
/// The number of slices in the chunk.
/// The raw data in the slice as a byte array.
- public void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) {
- var sliceData = new SliceData {
+ public void SetSliceData(byte chunkId, ushort sliceId, ushort numSlices, byte[] data) {
+ var slicePacket = new ServerUpdatePacket();
+ slicePacket.SetSendingPacketData(ServerUpdatePacketId.Slice, new SliceData {
ChunkId = chunkId,
SliceId = sliceId,
NumSlices = numSlices,
Data = data
- };
-
- lock (Lock) {
- CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.Slice, sliceData);
- }
+ });
+ SendSlicePacket(slicePacket);
}
///
diff --git a/SSMP/Networking/Client/NetClient.cs b/SSMP/Networking/Client/NetClient.cs
index 1e7d2826..4570b72e 100644
--- a/SSMP/Networking/Client/NetClient.cs
+++ b/SSMP/Networking/Client/NetClient.cs
@@ -102,6 +102,7 @@ public NetClient(PacketManager packetManager) {
// Create chunk sender/receiver with delegates to the update manager
_chunkSender = new ChunkSender(UpdateManager.SetSliceData);
_chunkReceiver = new ChunkReceiver(UpdateManager.SetSliceAckData);
+ UpdateManager.EnqueueChunkPacketAction = _chunkSender.EnqueuePacket;
_connectionManager = new ClientConnectionManager(_packetManager, _chunkSender, _chunkReceiver);
_connectionManager.ServerInfoReceivedEvent += OnServerInfoReceived;
diff --git a/SSMP/Networking/ConnectionManager.cs b/SSMP/Networking/ConnectionManager.cs
index 61515159..80b83a01 100644
--- a/SSMP/Networking/ConnectionManager.cs
+++ b/SSMP/Networking/ConnectionManager.cs
@@ -19,7 +19,7 @@ internal abstract class ConnectionManager(PacketManager packetManager) {
///
/// The maximum number of slices in a chunk.
///
- public const int MaxSlicesPerChunk = 256;
+ public const int MaxSlicesPerChunk = 32768;
///
/// The maximum size of a chunk in bytes.
diff --git a/SSMP/Networking/Packet/BasePacket.cs b/SSMP/Networking/Packet/BasePacket.cs
index 04379aa0..ac876718 100644
--- a/SSMP/Networking/Packet/BasePacket.cs
+++ b/SSMP/Networking/Packet/BasePacket.cs
@@ -46,6 +46,12 @@ internal abstract class BasePacket where TPacketId : Enum {
/// Whether this packet contains data that needs to be reliable.
///
public bool ContainsReliableData { get; protected set; }
+
+ ///
+ /// Gets whether this packet contains normal connection data,
+ /// without building the combined packet-data cache.
+ ///
+ public bool HasNormalData => NormalPacketData.Count > 0;
///
/// Construct the update packet with the given raw packet instance to read from.
@@ -209,11 +215,9 @@ Dictionary addonDataDict
continue;
}
- // Prepend the length of the addon packet data to the addon packet
- addonPacket.WriteLength();
-
- // Now we add the addon ID to the addon packet data packet and then the contents of the addon packet
+ // Now we add the addon ID to the addon packet data packet, then the length as an int, and then the contents of the addon packet
addonPacketDataPacket.Write(addonId);
+ addonPacketDataPacket.Write(addonPacket.Length);
addonPacketDataPacket.Write(addonPacket.ToArray());
// Potentially update whether this packet contains reliable data now
@@ -357,8 +361,8 @@ Dictionary addonDataDict
throw new Exception($"Addon with ID {addonId} has no defined addon packet info");
}
- // Read the length of the addon packet data for this addon
- var addonDataLength = packet.ReadUShort();
+ // Read the length of the addon packet data for this addon as an int
+ var addonDataLength = packet.ReadInt();
// Read exactly as many bytes as was indicated by the previously read value
var addonDataBytes = packet.ReadBytes(addonDataLength);
diff --git a/SSMP/Networking/Packet/Data/SliceAckData.cs b/SSMP/Networking/Packet/Data/SliceAckData.cs
index bd59f97d..20b73d85 100644
--- a/SSMP/Networking/Packet/Data/SliceAckData.cs
+++ b/SSMP/Networking/Packet/Data/SliceAckData.cs
@@ -31,9 +31,7 @@ internal class SliceAckData : IPacketData {
///
public void WriteData(IPacket packet) {
packet.Write(ChunkId);
-
- var encodedNumSlices = (byte) (NumSlices - 1);
- packet.Write(encodedNumSlices);
+ packet.Write(NumSlices);
// Keep track of current index for writing ack array
var currentIndex = 0;
@@ -48,9 +46,7 @@ public void WriteData(IPacket packet) {
///
public void ReadData(IPacket packet) {
ChunkId = packet.ReadByte();
-
- var encodedNumSlices = packet.ReadByte();
- NumSlices = (ushort) (encodedNumSlices + 1);
+ NumSlices = packet.ReadUShort();
var acked = new bool[ConnectionManager.MaxSlicesPerChunk];
diff --git a/SSMP/Networking/Packet/Data/SliceData.cs b/SSMP/Networking/Packet/Data/SliceData.cs
index dfa99991..6a5e8473 100644
--- a/SSMP/Networking/Packet/Data/SliceData.cs
+++ b/SSMP/Networking/Packet/Data/SliceData.cs
@@ -20,11 +20,10 @@ internal class SliceData : IPacketData {
///
/// The ID of this slice.
///
- public byte SliceId { get; set; }
+ public ushort SliceId { get; set; }
///
- /// The total number of slices in this chunk. It is an unsigned short because we can have 256 slices in a chunk.
- /// It is encoded as a byte, where all values are shifted by one since 0 is not used.
+ /// The total number of slices in this chunk.
///
public ushort NumSlices { get; set; }
@@ -37,14 +36,11 @@ internal class SliceData : IPacketData {
public void WriteData(IPacket packet) {
packet.Write(ChunkId);
packet.Write(SliceId);
-
- // Shift all values by -1 so that we can encode 256 as a number of slices
- var encodedNumSlices = (byte) (NumSlices - 1);
- packet.Write(encodedNumSlices);
+ packet.Write(NumSlices);
var length = Data.Length;
if (length > ConnectionManager.MaxSliceSize) {
- throw new ArgumentOutOfRangeException(nameof(Data), "Length of data for slice cannot exceed 1024");
+ throw new ArgumentOutOfRangeException(nameof(Data), $"Length of data for slice cannot exceed {ConnectionManager.MaxSliceSize}");
}
if (SliceId == NumSlices - 1) {
@@ -59,11 +55,8 @@ public void WriteData(IPacket packet) {
///
public void ReadData(IPacket packet) {
ChunkId = packet.ReadByte();
- SliceId = packet.ReadByte();
-
- // Read the encoded byte and shift it by 1 again
- var encodedNumSlices = packet.ReadByte();
- NumSlices = (ushort) (encodedNumSlices + 1);
+ SliceId = packet.ReadUShort();
+ NumSlices = packet.ReadUShort();
ushort length;
if (SliceId == NumSlices - 1) {
diff --git a/SSMP/Networking/Packet/IPacket.cs b/SSMP/Networking/Packet/IPacket.cs
index 4f8ac4d6..442c407d 100644
--- a/SSMP/Networking/Packet/IPacket.cs
+++ b/SSMP/Networking/Packet/IPacket.cs
@@ -113,6 +113,12 @@ public interface IPacket {
/// The enum type that the set also uses.
void WriteBitFlag(ISet set) where TEnum : Enum;
+ ///
+ /// Write an array of bytes to the packet.
+ ///
+ /// A byte array of values to write.
+ void Write(byte[] values);
+
#endregion
#region Reading integral numeric types
@@ -218,5 +224,12 @@ public interface IPacket {
/// The enum type that the set also uses.
ISet ReadBitFlag() where TEnum : Enum;
+ ///
+ /// Read an array of bytes of the given length from the packet.
+ ///
+ /// The length to read.
+ /// A byte array of the given length containing the content at the current position in the packet.
+ byte[] ReadBytes(int length);
+
#endregion
}
diff --git a/SSMP/Networking/Packet/LengthCountingPacket.cs b/SSMP/Networking/Packet/LengthCountingPacket.cs
new file mode 100644
index 00000000..0405aaac
--- /dev/null
+++ b/SSMP/Networking/Packet/LengthCountingPacket.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using SSMP.Math;
+
+namespace SSMP.Networking.Packet;
+
+///
+/// A lightweight implementation of IPacket that only counts the number of bytes written,
+/// completely avoiding any heap allocations or byte copies during size validation.
+///
+public sealed class LengthCountingPacket : IPacket {
+ ///
+ /// Gets the number of bytes counted.
+ ///
+ public int Length { get; private set; }
+
+ ///
+ /// Resets the byte count back to zero.
+ ///
+ public void Reset() => Length = 0;
+
+ ///
+ public void Write(byte value) => Length += 1;
+
+ ///
+ public void Write(ushort value) => Length += 2;
+
+ ///
+ public void Write(uint value) => Length += 4;
+
+ ///
+ public void Write(ulong value) => Length += 8;
+
+ ///
+ public void Write(sbyte value) => Length += 1;
+
+ ///
+ public void Write(short value) => Length += 2;
+
+ ///
+ public void Write(int value) => Length += 4;
+
+ ///
+ public void Write(long value) => Length += 8;
+
+ ///
+ public void Write(float value) => Length += 4;
+
+ ///
+ public void Write(double value) => Length += 8;
+
+ ///
+ public void Write(bool value) => Length += 1;
+
+ ///
+ public void Write(string value) => Length += 2 + System.Text.Encoding.UTF8.GetByteCount(value);
+
+ ///
+ public void Write(Vector2 value) => Length += 8;
+
+ ///
+ public void Write(Vector3 value) => Length += 12;
+
+ ///
+ public void Write(byte[] values) => Length += values.Length;
+
+ ///
+ public void WriteBitFlag(ISet set) where TEnum : Enum {
+ var enumLength = Enum.GetValues(typeof(TEnum)).Length;
+ switch (enumLength) {
+ case <= 8:
+ Length += 1;
+ break;
+ case <= 16:
+ Length += 2;
+ break;
+ case <= 32:
+ Length += 4;
+ break;
+ case <= 64:
+ Length += 8;
+ break;
+ }
+ }
+
+ ///
+ public byte ReadByte() => throw new NotSupportedException();
+
+ ///
+ public ushort ReadUShort() => throw new NotSupportedException();
+
+ ///
+ public uint ReadUInt() => throw new NotSupportedException();
+
+ ///
+ public ulong ReadULong() => throw new NotSupportedException();
+
+ ///
+ public sbyte ReadSByte() => throw new NotSupportedException();
+
+ ///
+ public short ReadShort() => throw new NotSupportedException();
+
+ ///
+ public int ReadInt() => throw new NotSupportedException();
+
+ ///
+ public long ReadLong() => throw new NotSupportedException();
+
+ ///
+ public float ReadFloat() => throw new NotSupportedException();
+
+ ///
+ public double ReadDouble() => throw new NotSupportedException();
+
+ ///
+ public bool ReadBool() => throw new NotSupportedException();
+
+ ///
+ public string ReadString() => throw new NotSupportedException();
+
+ ///
+ public Vector2 ReadVector2() => throw new NotSupportedException();
+
+ ///
+ public Vector3 ReadVector3() => throw new NotSupportedException();
+
+ ///
+ public ISet ReadBitFlag() where TEnum : Enum => throw new NotSupportedException();
+
+ ///
+ public byte[] ReadBytes(int length) => throw new NotSupportedException();
+}
diff --git a/SSMP/Networking/Packet/Packet.cs b/SSMP/Networking/Packet/Packet.cs
index 537c4d50..a5ef1a7e 100644
--- a/SSMP/Networking/Packet/Packet.cs
+++ b/SSMP/Networking/Packet/Packet.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using System.Threading;
using SSMP.Math;
namespace SSMP.Networking.Packet;
@@ -38,6 +39,12 @@ internal class Packet : IPacket {
///
public int Length { get; private set; }
+ ///
+ /// Thread-local length counting packet for zero-allocation size validation.
+ ///
+ private static readonly ThreadLocal LengthCountingPacket =
+ new(() => new LengthCountingPacket());
+
///
/// Creates a packet with the given byte array of data.
/// Used when receiving packets to read data from.
@@ -83,6 +90,11 @@ public Packet() {
///
public void WriteLength() {
if (_buffer == null) throw new InvalidOperationException("Cannot write to Read-Only Packet");
+ if (_buffer.Count > ushort.MaxValue)
+ throw new InvalidOperationException(
+ $"Packet size ({_buffer.Count} bytes) exceeds the {ushort.MaxValue} bytes limit for normal updates, causing truncation. Please use the SendChunkData API instead."
+ );
+
var length = (ushort) _buffer.Count;
_buffer.Insert(0, (byte) length);
_buffer.Insert(1, (byte) (length >> 8));
@@ -101,6 +113,21 @@ public byte[] ToArray() {
return copy;
}
+ ///
+ /// Validates that the serialized size of the given packet data does not exceed the 64 KiB (65535 bytes) limit,
+ /// throwing an InvalidOperationException if it does.
+ ///
+ /// The packet data to validate.
+ public static void ValidateSize(IPacketData packetData) {
+ var counter = LengthCountingPacket.Value!;
+ counter.Reset();
+ packetData.WriteData(counter);
+ if (counter.Length > ushort.MaxValue)
+ throw new InvalidOperationException(
+ $"Addon packet data size ({counter.Length} bytes) exceeds the {ushort.MaxValue} bytes limit for normal updates. Please use the SendChunkData API instead."
+ );
+ }
+
///
/// Clears the packet buffer, allowing reuse for write-mode packets.
/// Resets length and read position to 0.
@@ -113,9 +140,12 @@ public void Clear() {
if (_buffer == null) throw new InvalidOperationException("Cannot clear Read-Only Packet");
// In write-mode, the default constructor initializes _readableBuffer to an empty array.
// If _readableBuffer is non-empty, this packet was created from existing data and should not be cleared.
- if (_readableBuffer.Length != 0)
- throw new InvalidOperationException("Clear() can only be used on write-mode packets created with the default constructor.");
-
+ if (_readableBuffer.Length != 0) {
+ throw new InvalidOperationException(
+ "Clear() can only be used on write-mode packets created with the default constructor."
+ );
+ }
+
_buffer.Clear();
// Readable buffer assumes it mirrors _buffer in write mode, but usually _readableBuffer is a copy or view.
// In Write Mode (constructor Packet()), _readableBuffer is initialized to empty array.
diff --git a/SSMP/Networking/Packet/PacketManager.cs b/SSMP/Networking/Packet/PacketManager.cs
index 0ca65ed1..76ed7c3f 100644
--- a/SSMP/Networking/Packet/PacketManager.cs
+++ b/SSMP/Networking/Packet/PacketManager.cs
@@ -39,7 +39,6 @@ public delegate void GenericServerPacketHandler(ushort id, TPack
/// Manages packets that are received by the given NetClient.
///
internal class PacketManager {
-
#region Standard Packet Registries
private readonly PacketHandlerRegistry _clientUpdateRegistry = new(
@@ -54,7 +53,7 @@ internal class PacketManager {
private readonly PacketHandlerRegistry _serverConnectionRegistry = new(
"server connection", false
);
-
+
#endregion
#region Addon Packet Registries (Nested Dictionaries)
@@ -75,9 +74,9 @@ private readonly Dictionary>
_serverAddonConnectionRegistries = new();
-
+
#endregion
-
+
#region Packet Unpacking Helper
@@ -259,6 +258,21 @@ string registryName
);
}
+ private static void HandleClientAddonPacketSingle(
+ byte addonId,
+ byte packetId,
+ IPacketData packetData,
+ Dictionary> registryDict,
+ string registryName
+ ) {
+ if (!registryDict.TryGetValue(addonId, out var registry)) {
+ Logger.Warn($"There is no {registryName} handler registry for addon ID {addonId}");
+ return;
+ }
+
+ registry.Execute(packetId, handler => handler(packetData));
+ }
+
private void RegisterClientAddonHandler(
byte addonId,
byte packetId,
@@ -314,6 +328,22 @@ string registryName
);
}
+ private static void HandleServerAddonPacketSingle(
+ ushort clientId,
+ byte addonId,
+ byte packetId,
+ IPacketData packetData,
+ Dictionary> registryDict,
+ string registryName
+ ) {
+ if (!registryDict.TryGetValue(addonId, out var registry)) {
+ Logger.Warn($"There is no {registryName} handler registry for addon ID {addonId}");
+ return;
+ }
+
+ registry.Execute(packetId, handler => handler(clientId, packetData));
+ }
+
private void RegisterServerAddonHandler(
byte addonId,
byte packetId,
@@ -359,6 +389,11 @@ public void DeregisterClientAddonUpdatePacketHandler(byte addonId, byte packetId
public void ClearClientAddonUpdatePacketHandlers() => _clientAddonUpdateRegistries.Clear();
+ public void HandleClientAddonPacketSingle(byte addonId, byte packetId, IPacketData packetData) =>
+ HandleClientAddonPacketSingle(
+ addonId, packetId, packetData, _clientAddonUpdateRegistries, "client addon update"
+ );
+
public void RegisterClientAddonConnectionPacketHandler(byte addonId, byte packetId, ClientPacketHandler handler) =>
RegisterClientAddonHandler(addonId, packetId, handler, _clientAddonConnectionRegistries, "connection");
@@ -378,6 +413,11 @@ public void RegisterServerAddonUpdatePacketHandler(byte addonId, byte packetId,
public void DeregisterServerAddonUpdatePacketHandler(byte addonId, byte packetId) =>
DeregisterServerAddonHandler(addonId, packetId, _serverAddonUpdateRegistries);
+ public void HandleServerAddonPacketSingle(ushort clientId, byte addonId, byte packetId, IPacketData packetData) =>
+ HandleServerAddonPacketSingle(
+ clientId, addonId, packetId, packetData, _serverAddonUpdateRegistries, "server addon update"
+ );
+
public void RegisterServerAddonConnectionPacketHandler(byte addonId, byte packetId, ServerPacketHandler handler) =>
RegisterServerAddonHandler(addonId, packetId, handler, _serverAddonConnectionRegistries, "connection");
diff --git a/SSMP/Networking/Server/NetServer.cs b/SSMP/Networking/Server/NetServer.cs
index 955b560d..109bac5a 100644
--- a/SSMP/Networking/Server/NetServer.cs
+++ b/SSMP/Networking/Server/NetServer.cs
@@ -285,8 +285,8 @@ private void HandleClientPackets(NetServerClient client, List pac
client.UpdateManager.OnReceivePacket(serverUpdatePacket);
var packetData = serverUpdatePacket.GetPacketData();
- if (packetData.Remove(ServerUpdatePacketId.Slice, out var sliceData)) {
- client.ChunkReceiver.ProcessReceivedData((SliceData) sliceData);
+ if (packetData.Remove(ServerUpdatePacketId.Slice, out var sliceDataRaw)) {
+ client.ChunkReceiver.ProcessReceivedData((SliceData) sliceDataRaw);
}
if (packetData.Remove(ServerUpdatePacketId.SliceAck, out var sliceAckData)) {
@@ -539,11 +539,13 @@ Func packetInstantiator
}
// After we know that this call did not use a different generic, we can update packet info
- ServerUpdatePacket.AddonPacketInfoDict[addon.Id.Value] = new AddonPacketInfo(
+ var addonPacketInfo = new AddonPacketInfo(
// Transform the packet instantiator function from a TPacketId as parameter to byte
networkReceiver?.TransformPacketInstantiator(packetInstantiator)!,
(byte) Enum.GetValues(typeof(TPacketId)).Length
);
+ ServerUpdatePacket.AddonPacketInfoDict[addon.Id.Value] = addonPacketInfo;
+ ServerConnectionPacket.AddonPacketInfoDict[addon.Id.Value] = addonPacketInfo;
return (addon.NetworkReceiver as IServerAddonNetworkReceiver)!;
}
diff --git a/SSMP/Networking/Server/NetServerClient.cs b/SSMP/Networking/Server/NetServerClient.cs
index 3743c7c7..6ccdb21f 100644
--- a/SSMP/Networking/Server/NetServerClient.cs
+++ b/SSMP/Networking/Server/NetServerClient.cs
@@ -71,6 +71,7 @@ public NetServerClient(IEncryptedTransportClient transportClient, PacketManager
// Create chunk sender/receiver with delegates to the update manager
ChunkSender = new ChunkSender(UpdateManager.SetSliceData);
ChunkReceiver = new ChunkReceiver(UpdateManager.SetSliceAckData);
+ UpdateManager.EnqueueChunkPacketAction = ChunkSender.EnqueuePacket;
ConnectionManager = new ServerConnectionManager(packetManager, ChunkSender, ChunkReceiver, Id);
}
diff --git a/SSMP/Networking/Server/ServerConnectionManager.cs b/SSMP/Networking/Server/ServerConnectionManager.cs
index 8fcc2c79..4f94be4f 100644
--- a/SSMP/Networking/Server/ServerConnectionManager.cs
+++ b/SSMP/Networking/Server/ServerConnectionManager.cs
@@ -137,6 +137,19 @@ private void OnChunkReceived(Packet.Packet packet) {
return;
}
- PacketManager.HandleServerConnectionPacket(_clientId, connectionPacket);
+ if (connectionPacket.HasNormalData) {
+ // Let the packet manager handle the connection packet,
+ // which will invoke the relevant data handlers
+ PacketManager.HandleServerConnectionPacket(_clientId, connectionPacket);
+ return;
+ }
+
+ // No ClientInfo connection data exists,
+ // meaning this is a real-time gameplay addon chunk
+ foreach (var (addonId, value) in connectionPacket.GetAddonPacketData()) {
+ foreach (var (packetId, packetData) in value.PacketData) {
+ PacketManager.HandleServerAddonPacketSingle(_clientId, addonId, packetId, packetData);
+ }
+ }
}
}
diff --git a/SSMP/Networking/Server/ServerUpdateManager.cs b/SSMP/Networking/Server/ServerUpdateManager.cs
index 38389ad3..47dd6e91 100644
--- a/SSMP/Networking/Server/ServerUpdateManager.cs
+++ b/SSMP/Networking/Server/ServerUpdateManager.cs
@@ -95,23 +95,21 @@ private PacketDataCollection GetOrCreateCollection(ClientUpdatePacketId pa
}
///
- /// Set slice data in the current packet.
+ /// Send a slice packet immediately, bypassing the gameplay tick loop.
///
/// The ID of the chunk the slice belongs to.
/// The ID of the slice within the chunk.
/// The number of slices in the chunk.
/// The raw data in the slice as a byte array.
- public void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) {
- var sliceData = new SliceData {
+ public void SetSliceData(byte chunkId, ushort sliceId, ushort numSlices, byte[] data) {
+ var slicePacket = new ClientUpdatePacket();
+ slicePacket.SetSendingPacketData(ClientUpdatePacketId.Slice, new SliceData {
ChunkId = chunkId,
SliceId = sliceId,
NumSlices = numSlices,
Data = data
- };
-
- lock (Lock) {
- CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.Slice, sliceData);
- }
+ });
+ SendSlicePacket(slicePacket);
}
///
diff --git a/SSMP/Networking/UpdateManager.cs b/SSMP/Networking/UpdateManager.cs
index 2571170b..49a004d1 100644
--- a/SSMP/Networking/UpdateManager.cs
+++ b/SSMP/Networking/UpdateManager.cs
@@ -182,6 +182,17 @@ private void InitializeManagersIfNeeded() {
///
public event Action? TimeoutEvent;
+ ///
+ /// Action to enqueue a packet for chunked sending.
+ ///
+ public Action? EnqueueChunkPacketAction { get; set; }
+
+ ///
+ /// Enqueues a packet to be sent reliably as a chunk to the destination.
+ ///
+ /// The packet to send.
+ public void SendChunkPacket(Packet.Packet packet) => EnqueueChunkPacketAction?.Invoke(packet);
+
///
/// Construct the update manager with a UDP socket.
///
@@ -218,6 +229,11 @@ public void StopUpdates() {
Logger.Debug("Stopping UDP updates, sending last packet");
CreateAndSendPacket();
_cancellationTokenSource?.Cancel();
+
+ lock (Lock) {
+ Monitor.PulseAll(Lock);
+ }
+
// Wait for thread to finish before disposing shared resources
_sendThread?.Join();
_sendThread = null;
@@ -301,6 +317,9 @@ private void CreateAndSendPacket() {
// but keep the original instance for reliability data re-sending
packetToSend = CurrentUpdatePacket;
CurrentUpdatePacket = new TOutgoing();
+
+ // Pulse to wake up any threads waiting to set new slice data
+ Monitor.PulseAll(Lock);
}
// Track send time for RTT measurement (all transports)
@@ -433,6 +452,7 @@ private void SendLoop() {
if (cts == null) {
return;
}
+
var token = cts.Token;
// Safety constant: how many ms can we fall behind before giving up?
@@ -542,6 +562,7 @@ public void SetAddonData(
IPacketData packetData
) {
lock (Lock) {
+ Packet.Packet.ValidateSize(packetData);
var addonPacketData = GetOrCreateAddonPacketData(addonId, packetIdSize);
addonPacketData.PacketData[packetId] = packetData;
}
@@ -579,6 +600,8 @@ TPacketData packetData
} else {
existingDataCollection.DataInstances.Add(packetData);
}
+
+ Packet.Packet.ValidateSize(existingPacketData);
}
}
@@ -597,4 +620,35 @@ private AddonPacketData GetOrCreateAddonPacketData(byte addonId, byte packetIdSi
CurrentUpdatePacket.SetSendingAddonPacketData(addonId, addonPacketData);
return addonPacketData;
}
+
+ ///
+ /// Sends a slice packet immediately, bypassing the gameplay tick send loop.
+ ///
+ protected void SendSlicePacket(TOutgoing slicePacket) {
+ var rawPacket = new Packet.Packet();
+ lock (Lock) {
+ if (_requiresSequencing) {
+ slicePacket.Sequence = _localSequence;
+ slicePacket.Ack = _remoteSequence;
+
+ var ackField = slicePacket.AckField;
+ for (ushort i = 0; i < ConnectionManager.AckSize; i++) {
+ var pastSequence = (ushort) (_remoteSequence - i - 1);
+ ackField[i] = _receivedSequences!.Contains(pastSequence);
+ }
+ }
+
+ try {
+ slicePacket.CreatePacket(rawPacket);
+ } catch (Exception e) {
+ Logger.Error($"Failed to create slice packet: {e}");
+ return;
+ }
+
+ if (_requiresSequencing)
+ _localSequence++;
+ }
+
+ SendPacket(rawPacket, slicePacket.ContainsReliableData);
+ }
}