Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion SSMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
68 changes: 66 additions & 2 deletions SSMP/Api/Client/Networking/ClientAddonNetworkSender.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using SSMP.Networking.Client;
using SSMP.Networking.Packet;
using SSMP.Networking.Packet.Connection;

namespace SSMP.Api.Client.Networking;

Expand Down Expand Up @@ -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) {
Expand All @@ -87,7 +89,8 @@ TPacketData packetData

if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
InvalidPacketIdMsg);
InvalidPacketIdMsg
);
}

if (!_clientAddon.Id.HasValue) {
Expand All @@ -101,4 +104,65 @@ TPacketData packetData
packetData
);
}

/// <inheritdoc/>
public void SendChunkData(TPacketId packetId, IPacketData packetData) {
var (idValue, addonId) = ValidateCommon(packetId);
_netClient.UpdateManager.SendChunkPacket(BuildPacket(idValue, addonId, packetData));
}

/// <summary>
/// Validates the common client-side preconditions required before sending chunk data.
/// </summary>
/// <param name="packetId">The addon packet identifier to validate and resolve.</param>
/// <returns>
/// The resolved packet ID byte value and the current addon ID.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if the client is not connected, the addon has no assigned ID, or the packet ID is invalid.
/// </exception>
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);
}

/// <summary>
/// Builds a network packet containing addon packet data for the given addon and packet ID.
/// </summary>
/// <param name="idValue">The resolved packet ID byte value.</param>
/// <param name="addonId">The addon ID that owns the packet data.</param>
/// <param name="packetData">The packet payload to send.</param>
/// <returns>
/// A constructed packet ready to be enqueued for chunked sending.
/// </returns>
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;
}
}
8 changes: 8 additions & 0 deletions SSMP/Api/Client/Networking/IClientAddonNetworkSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ void SendCollectionData<TPacketData>(
TPacketId packetId,
TPacketData packetData
) where TPacketData : IPacketData, new();

/// <summary>
/// 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).
/// </summary>
/// <param name="packetId">The packet ID.</param>
/// <param name="packetData">An instance of IPacketData to send.</param>
void SendChunkData(TPacketId packetId, IPacketData packetData);
}
29 changes: 29 additions & 0 deletions SSMP/Api/Server/Networking/IServerAddonNetworkSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,33 @@ void BroadcastCollectionData<TPacketData>(
TPacketId packetId,
TPacketData packetData
) where TPacketData : IPacketData, new();

/// <summary>
/// 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).
/// </summary>
/// <param name="packetId">The packet ID.</param>
/// <param name="packetData">An instance of IPacketData to send.</param>
/// <param name="playerId">The ID of the player.</param>
void SendChunkData(TPacketId packetId, IPacketData packetData, ushort playerId);

/// <summary>
/// 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).
/// </summary>
/// <param name="packetId">The packet ID.</param>
/// <param name="packetData">An instance of IPacketData to send.</param>
/// <param name="playerIds">The IDs of the players.</param>
void SendChunkData(TPacketId packetId, IPacketData packetData, params ushort[] playerIds);

/// <summary>
/// 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).
/// </summary>
/// <param name="packetId">The packet ID.</param>
/// <param name="packetData">An instance of IPacketData to send.</param>
void BroadcastChunkData(TPacketId packetId, IPacketData packetData);
}
129 changes: 112 additions & 17 deletions SSMP/Api/Server/Networking/ServerAddonNetworkSender.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -97,21 +99,23 @@ public void BroadcastSingleData(TPacketId packetId, IPacketData packetData) {

if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
PacketIdInvalidExceptionMsg);
PacketIdInvalidExceptionMsg
);
}

if (!_serverAddon.Id.HasValue) {
throw new InvalidOperationException(NoAddonIdMsg);
}

_netServer.SetDataForAllClients(updateManager => {
updateManager?.SetAddonData(
_serverAddon.Id.Value,
idValue,
_packetIdSize,
packetData
);
});
updateManager?.SetAddonData(
_serverAddon.Id.Value,
idValue,
_packetIdSize,
packetData
);
}
);
}

/// <inheritdoc/>
Expand All @@ -126,7 +130,8 @@ ushort playerId

if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
PacketIdInvalidExceptionMsg);
PacketIdInvalidExceptionMsg
);
}

var updateManager = _netServer.GetUpdateManagerForClient(playerId);
Expand Down Expand Up @@ -168,20 +173,110 @@ TPacketData packetData

if (!PacketIdLookup.TryGetValue(packetId, out var idValue)) {
throw new InvalidOperationException(
PacketIdInvalidExceptionMsg);
PacketIdInvalidExceptionMsg
);
}

if (!_serverAddon.Id.HasValue) {
throw new InvalidOperationException(NoAddonIdMsg);
}

_netServer.SetDataForAllClients(updateManager => {
updateManager?.SetAddonDataAsCollection(
_serverAddon.Id.Value,
idValue,
_packetIdSize,
packetData
updateManager?.SetAddonDataAsCollection(
_serverAddon.Id.Value,
idValue,
_packetIdSize,
packetData
);
}
);
}

/// <inheritdoc/>
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));
}

/// <inheritdoc/>
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);
}
}

/// <inheritdoc/>
public void BroadcastChunkData(TPacketId packetId, IPacketData packetData) {
var (idValue, addonId) = ValidateCommon(packetId);
var packet = BuildPacket(idValue, addonId, packetData);
_netServer.SetDataForAllClients(updateManager => updateManager.SendChunkPacket(packet));
}

/// <summary>
/// Validates the common server-side preconditions required before sending chunk data.
/// </summary>
/// <param name="packetId">The addon packet identifier to validate and resolve.</param>
/// <returns>
/// The resolved packet ID byte value and the current addon ID.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if the server is not started, the addon has no assigned ID, or the packet ID is invalid.
/// </exception>
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);
}

/// <summary>
/// Builds a network packet containing addon packet data for the given addon and packet ID.
/// </summary>
/// <param name="idValue">The resolved packet ID byte value.</param>
/// <param name="addonId">The addon ID that owns the packet data.</param>
/// <param name="packetData">The packet payload to send.</param>
/// <returns>
/// A constructed packet ready to be enqueued for chunked sending.
/// </returns>
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;
}
}
21 changes: 11 additions & 10 deletions SSMP/Networking/Chunk/ChunkReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -146,7 +147,7 @@ public void ProcessReceivedData(SliceData sliceData) {
_chunkSize
);
var packet = new Packet.Packet(byteArray);

ChunkReceivedEvent?.Invoke(packet);

_isReceiving = false;
Expand Down
Loading
Loading