using System.Runtime.InteropServices; using Unity.Collections.LowLevel.Unsafe; using Random = Unity.Mathematics.Random; namespace Unity.Networking.Transport.Utilities { public struct SimulatorUtility { private int m_PacketCount; private int m_MaxPacketSize; private int m_PacketDelayMs; private int m_PacketJitterMs; /// /// Configuration parameters for the simulator pipeline stage. /// [StructLayout(LayoutKind.Sequential)] public struct Parameters : INetworkParameter { /// /// The maximum amount of packets the pipeline can keep track of. This used when a /// packet is delayed, the packet is stored in the pipeline processing buffer and can /// be later brought back. /// public int MaxPacketCount; /// /// The maximum size of a packet which the simulator stores. If a packet exceeds this size it will /// bypass the simulator. /// public int MaxPacketSize; /// /// Fixed delay to apply to all packets which pass through. /// public int PacketDelayMs; /// /// Variable delay to apply to all packets which pass through, adds or subtracts amount from fixed delay. /// public int PacketJitterMs; /// /// Fixed interval to drop packets on. This is most suitable for tests where predictable /// behaviour is desired, every Xth packet will be dropped. If PacketDropInterval is 5 /// every 5th packet is dropped. /// public int PacketDropInterval; /// /// Use a drop percentage when deciding when to drop packet. For every packet /// a random number generator is used to determine if the packet should be dropped or not. /// A percentage of 5 means approximately every 20th packet will be dropped. /// public int PacketDropPercentage; /// /// Use the fuzz factor when you want to fuzz a packet. For every packet /// a random number generator is used to determine if the packet should have the internal bits flipped. /// A percentage of 5 means approximately every 20th packet will be fuzzed, and that each bit in the packet /// has a 5 percent chance to get flipped. /// public int FuzzFactor; /// /// Use the fuzz offset in conjunction with the fuzz factor, the fuzz offset will offset where we start /// flipping bits. This is useful if you want to only fuzz a part of the packet. /// public int FuzzOffset; /// /// The random seed is used to set the initial seed of the random number generator. This is useful to get /// deterministic runs in tests for example that are dependant on the random number generator. /// public uint RandomSeed; } [StructLayout(LayoutKind.Sequential)] public struct Context { public int MaxPacketCount; public int MaxPacketSize; public int PacketDelayMs; public int PacketJitterMs; public int PacketDrop; public int FuzzOffset; public int FuzzFactor; public uint RandomSeed; public Random Random; // Statistics public int PacketCount; public int PacketDropCount; public int ReadyPackets; public int WaitingPackets; public long NextPacketTime; public long StatsTime; } [StructLayout(LayoutKind.Sequential)] public struct DelayedPacket { public int processBufferOffset; public ushort packetSize; public ushort packetHeaderPadding; public long delayUntil; } public SimulatorUtility(int packetCount, int maxPacketSize, int packetDelayMs, int packetJitterMs) { m_PacketCount = packetCount; m_MaxPacketSize = maxPacketSize; m_PacketDelayMs = packetDelayMs; m_PacketJitterMs = packetJitterMs; } public static unsafe void InitializeContext(Parameters param, byte* sharedProcessBuffer) { // Store parameters in the shared buffer space Context* ctx = (Context*) sharedProcessBuffer; ctx->MaxPacketCount = param.MaxPacketCount; ctx->MaxPacketSize = param.MaxPacketSize; ctx->PacketDelayMs = param.PacketDelayMs; ctx->PacketJitterMs = param.PacketJitterMs; ctx->PacketDrop = param.PacketDropInterval; ctx->FuzzFactor = param.FuzzFactor; ctx->FuzzOffset = param.FuzzOffset; ctx->PacketCount = 0; ctx->PacketDropCount = 0; ctx->Random = new Random(); if (param.RandomSeed > 0) { ctx->Random.InitState(param.RandomSeed); ctx->RandomSeed = param.RandomSeed; } else ctx->Random.InitState(); } public unsafe bool GetEmptyDataSlot(byte* processBufferPtr, ref int packetPayloadOffset, ref int packetDataOffset) { var dataSize = UnsafeUtility.SizeOf(); var packetPayloadStartOffset = m_PacketCount * dataSize; bool foundSlot = false; for (int i = 0; i < m_PacketCount; i++) { packetDataOffset = dataSize * i; DelayedPacket* packetData = (DelayedPacket*) (processBufferPtr + packetDataOffset); // Check if this slot is empty if (packetData->delayUntil == 0) { foundSlot = true; packetPayloadOffset = packetPayloadStartOffset + m_MaxPacketSize * i; break; } } return foundSlot; } public unsafe bool GetDelayedPacket(ref NetworkPipelineContext ctx, ref InboundSendBuffer delayedPacket, ref NetworkPipelineStage.Requests requests, long currentTimestamp) { requests = NetworkPipelineStage.Requests.None; var dataSize = UnsafeUtility.SizeOf(); byte* processBufferPtr = (byte*) ctx.internalProcessBuffer; var simCtx = (Context*) ctx.internalSharedProcessBuffer; int oldestPacketIndex = -1; long oldestTime = long.MaxValue; int readyPackets = 0; int packetsInQueue = 0; for (int i = 0; i < m_PacketCount; i++) { DelayedPacket* packet = (DelayedPacket*) (processBufferPtr + dataSize * i); if ((int) packet->delayUntil == 0) continue; packetsInQueue++; if (packet->delayUntil > currentTimestamp) continue; readyPackets++; if (oldestTime <= packet->delayUntil) continue; oldestPacketIndex = i; oldestTime = packet->delayUntil; } simCtx->ReadyPackets = readyPackets; simCtx->WaitingPackets = packetsInQueue; simCtx->NextPacketTime = oldestTime; simCtx->StatsTime = currentTimestamp; // If more than one item has expired timer we need to resume this pipeline stage if (readyPackets > 1) { requests |= NetworkPipelineStage.Requests.Resume; } // If more than one item is present (but doesn't have expired timer) we need to re-run the pipeline // in a later update call else if (packetsInQueue > 0) { requests |= NetworkPipelineStage.Requests.Update; } if (oldestPacketIndex >= 0) { DelayedPacket* packet = (DelayedPacket*) (processBufferPtr + dataSize * oldestPacketIndex); packet->delayUntil = 0; delayedPacket.bufferWithHeaders = ctx.internalProcessBuffer + packet->processBufferOffset; delayedPacket.bufferWithHeadersLength = packet->packetSize; delayedPacket.headerPadding = packet->packetHeaderPadding; delayedPacket.SetBufferFrombufferWithHeaders(); return true; } return false; } public unsafe void FuzzPacket(Context *ctx, ref InboundSendBuffer inboundBuffer) { int fuzzFactor = ctx->FuzzFactor; int fuzzOffset = ctx->FuzzOffset; int rand = ctx->Random.NextInt(0, 100); if (rand > fuzzFactor) return; var length = inboundBuffer.bufferLength; for (int i = fuzzOffset; i < length; ++i) { for (int j = 0; j < 8; ++j) { if (fuzzFactor > ctx->Random.NextInt(0, 100)) { inboundBuffer.buffer[i] ^= (byte)(1 << j); } } } } public unsafe bool DelayPacket(ref NetworkPipelineContext ctx, InboundSendBuffer inboundBuffer, ref NetworkPipelineStage.Requests requests, long timestamp) { // Find empty slot in bookkeeping data space to track this packet int packetPayloadOffset = 0; int packetDataOffset = 0; var processBufferPtr = (byte*) ctx.internalProcessBuffer; bool foundSlot = GetEmptyDataSlot(processBufferPtr, ref packetPayloadOffset, ref packetDataOffset); if (!foundSlot) { //UnityEngine.Debug.LogWarning("No space left for delaying packet (" + m_PacketCount + " packets in queue)"); return false; } UnsafeUtility.MemCpy(ctx.internalProcessBuffer + packetPayloadOffset + inboundBuffer.headerPadding, inboundBuffer.buffer, inboundBuffer.bufferLength); var param = (SimulatorUtility.Context*) ctx.internalSharedProcessBuffer; // Add tracking for this packet so we can resurrect later DelayedPacket packet; packet.delayUntil = timestamp + m_PacketDelayMs + param->Random.NextInt(m_PacketJitterMs*2) - m_PacketJitterMs; packet.processBufferOffset = packetPayloadOffset; packet.packetSize = (ushort)(inboundBuffer.headerPadding + inboundBuffer.bufferLength); packet.packetHeaderPadding = (ushort)inboundBuffer.headerPadding; byte* packetPtr = (byte*) &packet; UnsafeUtility.MemCpy(processBufferPtr + packetDataOffset, packetPtr, UnsafeUtility.SizeOf()); // Schedule an update call so packet can be resurrected later requests |= NetworkPipelineStage.Requests.Update; return true; } public unsafe bool ShouldDropPacket(Context* ctx, Parameters param, long timestamp) { if (param.PacketDropInterval > 0 && (ctx->PacketCount - 1) % param.PacketDropInterval == 0) return true; if (param.PacketDropPercentage > 0) { //var packetLoss = new System.Random().NextDouble() * 100; var packetLoss = ctx->Random.NextInt(0, 100); if (packetLoss < param.PacketDropPercentage) return true; } return false; } } }