From e969416832a53391eda07054ac9d563dd370e894 Mon Sep 17 00:00:00 2001 From: Liparakis Date: Sat, 16 May 2026 15:24:21 +0300 Subject: [PATCH 1/4] feat: Update build copy task to use multiple paths --- SSMP/SSMP.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SSMP/SSMP.csproj b/SSMP/SSMP.csproj index e4a736e0..17f14e7b 100644 --- a/SSMP/SSMP.csproj +++ b/SSMP/SSMP.csproj @@ -66,8 +66,9 @@ + - + From 2f749519cf3bf4377a327fbb79cbf9cd66ba53d8 Mon Sep 17 00:00:00 2001 From: Liparakis Date: Tue, 19 May 2026 02:08:16 +0300 Subject: [PATCH 2/4] chore: Small naming correction --- SSMP/SSMP.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SSMP/SSMP.csproj b/SSMP/SSMP.csproj index 17f14e7b..83ef910b 100644 --- a/SSMP/SSMP.csproj +++ b/SSMP/SSMP.csproj @@ -61,12 +61,12 @@ - + - + From 0bfee8c10b412159cda2bd309827a41da0470482 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 22 May 2026 21:30:29 +0200 Subject: [PATCH 3/4] Rework MSBuild configuration for multiple output paths --- .gitignore | 2 +- SSMP/LocalBuildProperties_example.props | 8 ++++++++ SSMP/SSMP.csproj | 12 ++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 SSMP/LocalBuildProperties_example.props diff --git a/.gitignore b/.gitignore index f11af6b0..a3db2751 100644 --- a/.gitignore +++ b/.gitignore @@ -420,4 +420,4 @@ InitTestScene*.unity .idea/ */lib/ */Lib/ -SilksongPath.props \ No newline at end of file +LocalBuildProperties.props diff --git a/SSMP/LocalBuildProperties_example.props b/SSMP/LocalBuildProperties_example.props new file mode 100644 index 00000000..15fae165 --- /dev/null +++ b/SSMP/LocalBuildProperties_example.props @@ -0,0 +1,8 @@ + + + + + + + diff --git a/SSMP/SSMP.csproj b/SSMP/SSMP.csproj index 83ef910b..de9fadc4 100644 --- a/SSMP/SSMP.csproj +++ b/SSMP/SSMP.csproj @@ -1,10 +1,11 @@ - + netstandard2.1 @@ -61,14 +62,13 @@ - + - - + From f0201a21052ee83800418da6dceee325af8aaf23 Mon Sep 17 00:00:00 2001 From: Liparakis Date: Fri, 29 May 2026 04:51:06 +0300 Subject: [PATCH 4/4] feat: large addon payload support using chunk transport --- .../Networking/ClientAddonNetworkReceiver.cs | 5 +- .../Networking/ClientAddonNetworkSender.cs | 68 ++++++++- .../Networking/IClientAddonNetworkSender.cs | 8 ++ .../Networking/IServerAddonNetworkSender.cs | 29 ++++ .../Networking/ServerAddonNetworkSender.cs | 129 ++++++++++++++--- SSMP/Networking/Chunk/ChunkReceiver.cs | 21 +-- SSMP/Networking/Chunk/ChunkSender.cs | 28 ++-- .../Client/ClientConnectionManager.cs | 17 ++- SSMP/Networking/Client/ClientUpdateManager.cs | 14 +- SSMP/Networking/Client/NetClient.cs | 1 + SSMP/Networking/ConnectionManager.cs | 2 +- SSMP/Networking/Packet/BasePacket.cs | 16 ++- SSMP/Networking/Packet/Data/SliceAckData.cs | 8 +- SSMP/Networking/Packet/Data/SliceData.cs | 19 +-- SSMP/Networking/Packet/IPacket.cs | 13 ++ .../Networking/Packet/LengthCountingPacket.cs | 133 ++++++++++++++++++ SSMP/Networking/Packet/Packet.cs | 36 ++++- SSMP/Networking/Packet/PacketManager.cs | 48 ++++++- SSMP/Networking/Server/NetServer.cs | 8 +- SSMP/Networking/Server/NetServerClient.cs | 1 + .../Server/ServerConnectionManager.cs | 15 +- SSMP/Networking/Server/ServerUpdateManager.cs | 14 +- SSMP/Networking/UpdateManager.cs | 54 +++++++ 23 files changed, 586 insertions(+), 101 deletions(-) create mode 100644 SSMP/Networking/Packet/LengthCountingPacket.cs 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); + } }