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.Socket.cs b/LiteNetLib/NetManager.Socket.cs index 40875700..6a89336c 100644 --- a/LiteNetLib/NetManager.Socket.cs +++ b/LiteNetLib/NetManager.Socket.cs @@ -532,6 +532,44 @@ internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEnd message = expandedPacket.RawData; } +#if DEBUG || SIMULATE_NETWORK + 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 +642,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 b260fd35..6fd17cf5 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; @@ -193,12 +204,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; @@ -208,12 +219,12 @@ private struct IncomingData 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; @@ -614,7 +625,7 @@ private void UpdateLogic() stopwatch.Stop(); } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ProcessDelayedPackets() { if (!SimulateLatency) @@ -634,6 +645,21 @@ private void ProcessDelayedPackets() } } } + + 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) @@ -816,7 +842,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) @@ -824,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) { @@ -833,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 @@ -841,7 +868,7 @@ private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) } } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void HandleSimulatePacketLoss() { if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) @@ -850,6 +877,48 @@ private void HandleSimulatePacketLoss() } } +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, IPEndPoint remoteEndPoint) + { + if (!SimulateLatency) + { + return false; + } + + 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]; + 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(outboundLatency) + }); + } + + return true; + } + return false; + } +#endif + +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundPacketLoss() + { + bool shouldDrop = SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance; + return shouldDrop; + } +#endif + private void HandleMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) { var originalPacketSize = packet.Size; @@ -1637,19 +1706,27 @@ public void Stop(bool sendDisconnectMessages) _lastPeerId = 0; ClearPingSimulationList(); + ClearOutboundSimulationList(); _connectedPeersCount = 0; _pendingEventHead = null; _pendingEventTail = null; } - [Conditional("DEBUG")] + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] private void ClearPingSimulationList() { lock (_pingSimulationList) _pingSimulationList.Clear(); } + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ClearOutboundSimulationList() + { + lock (_outboundSimulationList) + _outboundSimulationList.Clear(); + } + /// /// Return peers count with connection state /// 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