From 7e8fc47803e8d9155e816cd483f778af34343b6d Mon Sep 17 00:00:00 2001 From: Martin Vagstad Date: Tue, 1 Jul 2025 08:14:09 +0200 Subject: [PATCH 1/4] Add define SIMULATE_NETWORK as an alternative way to enable simulated latency and packet loss. --- LiteNetLib/LiteNetLib.csproj | 4 ++++ LiteNetLib/NetManager.cs | 12 ++++++------ docfx_project/index.md | 4 ++-- docs/api/LiteNetLib.NetManager.html | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/LiteNetLib/LiteNetLib.csproj b/LiteNetLib/LiteNetLib.csproj index 23fc36da..83d178db 100644 --- a/LiteNetLib/LiteNetLib.csproj +++ b/LiteNetLib/LiteNetLib.csproj @@ -26,6 +26,10 @@ TRACE + + $(DefineConstants);SIMULATE_NETWORK + + true $(DefineConstants);LITENETLIB_UNSAFE diff --git a/LiteNetLib/NetManager.cs b/LiteNetLib/NetManager.cs index b260fd35..a3d9de01 100644 --- a/LiteNetLib/NetManager.cs +++ b/LiteNetLib/NetManager.cs @@ -193,12 +193,12 @@ private struct IncomingData public int DisconnectTimeout = 5000; /// - /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG mode) + /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) /// public bool SimulatePacketLoss = false; /// - /// Simulate latency by holding packets for random time. (Works only in DEBUG mode) + /// Simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) /// public bool SimulateLatency = false; @@ -614,7 +614,7 @@ private void UpdateLogic() stopwatch.Stop(); } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ProcessDelayedPackets() { if (!SimulateLatency) @@ -816,7 +816,7 @@ private void OnMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) HandleMessageReceived(packet, remoteEndPoint); } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) { if (!SimulateLatency) @@ -841,7 +841,7 @@ private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) } } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void HandleSimulatePacketLoss() { if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) @@ -1643,7 +1643,7 @@ public void Stop(bool sendDisconnectMessages) _pendingEventTail = null; } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ClearPingSimulationList() { lock (_pingSimulationList) diff --git a/docfx_project/index.md b/docfx_project/index.md index e56e5f61..110827a2 100644 --- a/docfx_project/index.md +++ b/docfx_project/index.md @@ -113,10 +113,10 @@ server.Stop(); * (including library internal keepalive packets) * default value: **5000 msec**. * **SimulatePacketLoss** - * simulate packet loss by dropping random amout of packets. (Works only in DEBUG mode) + * simulate packet loss by dropping random amout of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) * default value: **false** * **SimulateLatency** - * simulate latency by holding packets for random time. (Works only in DEBUG mode) + * simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) * default value: **false** * **SimulationPacketLossChance** * chance of packet loss when simulation enabled. value in percents. diff --git a/docs/api/LiteNetLib.NetManager.html b/docs/api/LiteNetLib.NetManager.html index 27c74cd4..eb79c537 100644 --- a/docs/api/LiteNetLib.NetManager.html +++ b/docs/api/LiteNetLib.NetManager.html @@ -586,7 +586,7 @@
Field Value

SimulateLatency

-

Simulate latency by holding packets for random time. (Works only in DEBUG mode)

+

Simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined)

Declaration
@@ -611,7 +611,7 @@
Field Value

SimulatePacketLoss

-

Simulate packet loss by dropping random amount of packets. (Works only in DEBUG mode)

+

Simulate packet loss by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined)

Declaration
From 6a7bfb38217081d0b459b883f49374252c0709d2 Mon Sep 17 00:00:00 2001 From: Martin Vagstad Date: Tue, 1 Jul 2025 11:00:22 +0200 Subject: [PATCH 2/4] Added outbound packet loss and delay --- LiteNetLib/NetManager.Socket.cs | 44 +++++++++++-- LiteNetLib/NetManager.cs | 110 ++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 5 deletions(-) diff --git a/LiteNetLib/NetManager.Socket.cs b/LiteNetLib/NetManager.Socket.cs index 40875700..1c2d8a30 100644 --- a/LiteNetLib/NetManager.Socket.cs +++ b/LiteNetLib/NetManager.Socket.cs @@ -532,6 +532,45 @@ internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEnd message = expandedPacket.RawData; } +#if DEBUG || SIMULATE_NETWORK + // Apply outbound simulation (packet loss and latency) + if (HandleSimulateOutboundPacketLoss()) + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + return 0; // Simulate successful send to avoid triggering error handling + } + + if (HandleSimulateOutboundLatency(message, start, length, remoteEndPoint)) + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + return length; // Simulate successful send + } +#endif + + return SendRawCoreWithCleanup(message, start, length, remoteEndPoint, expandedPacket); + } + + private int SendRawCoreWithCleanup(byte[] message, int start, int length, IPEndPoint remoteEndPoint, NetPacket expandedPacket) + { + try + { + return SendRawCore(message, start, length, remoteEndPoint); + } + finally + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + } + } + + // Core socket sending logic without simulation - used by both SendRaw and delayed packet processing + internal int SendRawCore(byte[] message, int start, int length, IPEndPoint remoteEndPoint) + { + if (!_isRunning) + return 0; + var socket = _udpSocketv4; if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6 && IPv6Support) { @@ -604,11 +643,6 @@ internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEnd NetDebug.WriteError($"[S] {ex}"); return 0; } - finally - { - if (expandedPacket != null) - PoolRecycle(expandedPacket); - } if (result <= 0) return 0; diff --git a/LiteNetLib/NetManager.cs b/LiteNetLib/NetManager.cs index a3d9de01..c95549f6 100644 --- a/LiteNetLib/NetManager.cs +++ b/LiteNetLib/NetManager.cs @@ -130,6 +130,17 @@ private struct IncomingData public DateTime TimeWhenGet; } private readonly List _pingSimulationList = new List(); + + private struct OutboundDelayedPacket + { + public byte[] Data; + public int Start; + public int Length; + public IPEndPoint EndPoint; + public DateTime TimeWhenSend; + } + private readonly List _outboundSimulationList = new List(); + private readonly Random _randomGenerator = new Random(); private const int MinLatencyThreshold = 5; @@ -217,6 +228,31 @@ private struct IncomingData /// public int SimulationMaxLatency = 100; + /// + /// Simulate packet loss on outbound packets by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + /// + public bool SimulateOutboundPacketLoss = false; + + /// + /// Simulate latency on outbound packets by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + /// + public bool SimulateOutboundLatency = false; + + /// + /// Chance of outbound packet loss when simulation enabled. value in percents (1 - 100). + /// + public int SimulationOutboundPacketLossChance = 10; + + /// + /// Minimum simulated outbound latency (in milliseconds) + /// + public int SimulationOutboundMinLatency = 30; + + /// + /// Maximum simulated outbound latency (in milliseconds) + /// + public int SimulationOutboundMaxLatency = 100; + /// /// Events automatically will be called without PollEvents method from another thread /// @@ -570,6 +606,7 @@ private void UpdateLogic() try { ProcessDelayedPackets(); + ProcessDelayedOutboundPackets(); float elapsed = (float)(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0); elapsed = elapsed <= 0.0f ? 0.001f : elapsed; stopwatch.Restart(); @@ -636,6 +673,29 @@ private void ProcessDelayedPackets() } } + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ProcessDelayedOutboundPackets() + { + if (!SimulateOutboundLatency) + return; + + var time = DateTime.UtcNow; + lock (_outboundSimulationList) + { + for (int i = 0; i < _outboundSimulationList.Count; i++) + { + var outboundData = _outboundSimulationList[i]; + if (outboundData.TimeWhenSend <= time) + { + // Send the delayed packet directly to socket layer bypassing simulation + SendRawCore(outboundData.Data, outboundData.Start, outboundData.Length, outboundData.EndPoint); + _outboundSimulationList.RemoveAt(i); + i--; + } + } + } + } + private void ProcessNtpRequests(float elapsedMilliseconds) { if (_ntpRequests.IsEmpty) @@ -850,6 +910,47 @@ private void HandleSimulatePacketLoss() } } +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, IPEndPoint remoteEndPoint) + { + if (!SimulateOutboundLatency) + { + return false; + } + + int latency = _randomGenerator.Next(SimulationOutboundMinLatency, SimulationOutboundMaxLatency); + if (latency > MinLatencyThreshold) + { + // Create a copy of the data to avoid issues with recycled packets + byte[] dataCopy = new byte[length]; + Array.Copy(data, start, dataCopy, 0, length); + + lock (_outboundSimulationList) + { + _outboundSimulationList.Add(new OutboundDelayedPacket + { + Data = dataCopy, + Start = 0, + Length = length, + EndPoint = remoteEndPoint, + TimeWhenSend = DateTime.UtcNow.AddMilliseconds(latency) + }); + } + + return true; // packet was delayed + } + return false; // packet not delayed + } +#endif + +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundPacketLoss() + { + bool shouldDrop = SimulateOutboundPacketLoss && _randomGenerator.NextDouble() * 100 < SimulationOutboundPacketLossChance; + return shouldDrop; + } +#endif + private void HandleMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) { var originalPacketSize = packet.Size; @@ -1479,6 +1580,7 @@ public void PollEvents(int maxProcessedEvents = 0) if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) ManualReceive(_udpSocketv6, _bufferEndPointv6, maxProcessedEvents); ProcessDelayedPackets(); + ProcessDelayedOutboundPackets(); return; } if (UnsyncedEvents) @@ -1637,6 +1739,7 @@ public void Stop(bool sendDisconnectMessages) _lastPeerId = 0; ClearPingSimulationList(); + ClearOutboundSimulationList(); _connectedPeersCount = 0; _pendingEventHead = null; @@ -1650,6 +1753,13 @@ private void ClearPingSimulationList() _pingSimulationList.Clear(); } + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ClearOutboundSimulationList() + { + lock (_outboundSimulationList) + _outboundSimulationList.Clear(); + } + /// /// Return peers count with connection state /// From b2f5bc0c5999caf6f1c97c53e94105983b2fe069 Mon Sep 17 00:00:00 2001 From: Martin Vagstad Date: Wed, 2 Jul 2025 10:55:57 +0200 Subject: [PATCH 3/4] merge variables for inbound and outbound latency and packet loss --- LiteNetLib/NetManager.Socket.cs | 1 - LiteNetLib/NetManager.cs | 41 +++------------------------------ 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/LiteNetLib/NetManager.Socket.cs b/LiteNetLib/NetManager.Socket.cs index 1c2d8a30..6a89336c 100644 --- a/LiteNetLib/NetManager.Socket.cs +++ b/LiteNetLib/NetManager.Socket.cs @@ -533,7 +533,6 @@ internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEnd } #if DEBUG || SIMULATE_NETWORK - // Apply outbound simulation (packet loss and latency) if (HandleSimulateOutboundPacketLoss()) { if (expandedPacket != null) diff --git a/LiteNetLib/NetManager.cs b/LiteNetLib/NetManager.cs index c95549f6..bdc91cc5 100644 --- a/LiteNetLib/NetManager.cs +++ b/LiteNetLib/NetManager.cs @@ -228,31 +228,6 @@ private struct OutboundDelayedPacket /// public int SimulationMaxLatency = 100; - /// - /// Simulate packet loss on outbound packets by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) - /// - public bool SimulateOutboundPacketLoss = false; - - /// - /// Simulate latency on outbound packets by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) - /// - public bool SimulateOutboundLatency = false; - - /// - /// Chance of outbound packet loss when simulation enabled. value in percents (1 - 100). - /// - public int SimulationOutboundPacketLossChance = 10; - - /// - /// Minimum simulated outbound latency (in milliseconds) - /// - public int SimulationOutboundMinLatency = 30; - - /// - /// Maximum simulated outbound latency (in milliseconds) - /// - public int SimulationOutboundMaxLatency = 100; - /// /// Events automatically will be called without PollEvents method from another thread /// @@ -606,7 +581,6 @@ private void UpdateLogic() try { ProcessDelayedPackets(); - ProcessDelayedOutboundPackets(); float elapsed = (float)(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0); elapsed = elapsed <= 0.0f ? 0.001f : elapsed; stopwatch.Restart(); @@ -671,15 +645,7 @@ private void ProcessDelayedPackets() } } } - } - [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] - private void ProcessDelayedOutboundPackets() - { - if (!SimulateOutboundLatency) - return; - - var time = DateTime.UtcNow; lock (_outboundSimulationList) { for (int i = 0; i < _outboundSimulationList.Count; i++) @@ -913,12 +879,12 @@ private void HandleSimulatePacketLoss() #if DEBUG || SIMULATE_NETWORK private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, IPEndPoint remoteEndPoint) { - if (!SimulateOutboundLatency) + if (!SimulateLatency) { return false; } - int latency = _randomGenerator.Next(SimulationOutboundMinLatency, SimulationOutboundMaxLatency); + int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); if (latency > MinLatencyThreshold) { // Create a copy of the data to avoid issues with recycled packets @@ -946,7 +912,7 @@ private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, I #if DEBUG || SIMULATE_NETWORK private bool HandleSimulateOutboundPacketLoss() { - bool shouldDrop = SimulateOutboundPacketLoss && _randomGenerator.NextDouble() * 100 < SimulationOutboundPacketLossChance; + bool shouldDrop = SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance; return shouldDrop; } #endif @@ -1580,7 +1546,6 @@ public void PollEvents(int maxProcessedEvents = 0) if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) ManualReceive(_udpSocketv6, _bufferEndPointv6, maxProcessedEvents); ProcessDelayedPackets(); - ProcessDelayedOutboundPackets(); return; } if (UnsyncedEvents) From 4a0eae52503e56b6e0f69cae9410048269586e0a Mon Sep 17 00:00:00 2001 From: Martin Vagstad Date: Thu, 3 Jul 2025 20:15:07 +0200 Subject: [PATCH 4/4] using half of latency for each of inbound and outbound simulated latency to give the correct total latency --- LiteNetLib/NetManager.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/LiteNetLib/NetManager.cs b/LiteNetLib/NetManager.cs index bdc91cc5..6fd17cf5 100644 --- a/LiteNetLib/NetManager.cs +++ b/LiteNetLib/NetManager.cs @@ -219,12 +219,12 @@ private struct OutboundDelayedPacket public int SimulationPacketLossChance = 10; /// - /// Minimum simulated latency (in milliseconds) + /// Minimum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. /// public int SimulationMinLatency = 30; /// - /// Maximum simulated latency (in milliseconds) + /// Maximum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. /// public int SimulationMaxLatency = 100; @@ -850,8 +850,9 @@ private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) return; } - int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); - if (latency > MinLatencyThreshold) + int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); + int inboundLatency = roundTripLatency / 2; + if (inboundLatency > MinLatencyThreshold) { lock (_pingSimulationList) { @@ -859,7 +860,7 @@ private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) { Data = packet, EndPoint = remoteEndPoint, - TimeWhenGet = DateTime.UtcNow.AddMilliseconds(latency) + TimeWhenGet = DateTime.UtcNow.AddMilliseconds(inboundLatency) }); } // hold packet @@ -884,8 +885,9 @@ private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, I return false; } - int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); - if (latency > MinLatencyThreshold) + int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); + int outboundLatency = roundTripLatency / 2; + if (outboundLatency > MinLatencyThreshold) { // Create a copy of the data to avoid issues with recycled packets byte[] dataCopy = new byte[length]; @@ -899,13 +901,13 @@ private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, I Start = 0, Length = length, EndPoint = remoteEndPoint, - TimeWhenSend = DateTime.UtcNow.AddMilliseconds(latency) + TimeWhenSend = DateTime.UtcNow.AddMilliseconds(outboundLatency) }); } - return true; // packet was delayed + return true; } - return false; // packet not delayed + return false; } #endif