Skip to content

Instantly share code, notes, and snippets.

@noggs
Last active December 12, 2019 12:37
Show Gist options
  • Save noggs/f0351cbbe5c99a4e482c4ad7e9efa7b0 to your computer and use it in GitHub Desktop.
Save noggs/f0351cbbe5c99a4e482c4ad7e9efa7b0 to your computer and use it in GitHub Desktop.
Drop in wrapper for the SteamNetworking component of Steamworks.NET for easily testing different network connections. Set the Latency or Packetloss properties and the rest is handled by the wrapper. Currently uses Unity for Random and Time functions, but they could easily be replaced.
using System;
using System.Collections.Generic;
using UnityEngine;
using Steamworks;
public class SteamworksNetEmu
{
private int _latency = 0;
private int _packetloss = 0;
private const int HeaderLen = 3;
public bool IsActive { get { return _latency != 0 || _packetloss != 0; } }
public int Latency { get => _latency; set => _latency = value; }
public int Packetloss { get => _packetloss; set => _packetloss = value; }
private class DeferredPacket
{
public byte[] data;
public CSteamID steamID;
public uint dataLen;
public EP2PSend sendType;
public float time; // time this packet should be actually sent/received
};
private List<DeferredPacket> _packetsIn = new List<DeferredPacket>();
private List<DeferredPacket> _packetsOut = new List<DeferredPacket>();
public bool IsP2PPacketAvailable(out uint pcubMsgSize, int nChannel = 0)
{
if (nChannel == 0 && (_latency > 0 || _packetsIn.Count > 0))
{
if (_packetsIn.Count > 0 && _packetsIn[0].time < Time.realtimeSinceStartup)
{
pcubMsgSize = _packetsIn[0].dataLen;
return true;
}
pcubMsgSize = 0;
return false;
}
else
{
return SteamNetworking.IsP2PPacketAvailable(out pcubMsgSize, nChannel);
}
}
public bool ReadP2PPacket(byte[] pubDest, uint cubDest, out uint pcubMsgSize, out CSteamID psteamIDRemote, int nChannel = 0)
{
if (nChannel == 0 && (_latency > 0 || _packetsIn.Count > 0))
{
// this should never be false if IsP2PPacketAvailable has returned true, but check anyway
if (_packetsIn.Count > 0 && _packetsIn[0].time < Time.realtimeSinceStartup)
{
DeferredPacket dp = _packetsIn[0];
pcubMsgSize = dp.dataLen;
Array.Copy(dp.data, 0, pubDest, 0, Math.Min(dp.dataLen, cubDest));
psteamIDRemote = dp.steamID;
// pop it!
_packetsIn.RemoveAt(0);
return true;
}
pcubMsgSize = 0;
psteamIDRemote = new CSteamID();
return false;
}
else if (SteamNetworking.ReadP2PPacket(pubDest, cubDest, out pcubMsgSize, out psteamIDRemote, nChannel))
{
int sendType = -1;
if (pcubMsgSize > 2 && pubDest[0] == 0x5C && pubDest[1] == 0xC5)
{
sendType = (int)pubDest[2];
Array.Copy(pubDest, HeaderLen, pubDest, 0, pcubMsgSize - HeaderLen);
pcubMsgSize -= HeaderLen;
}
// if packetloss, decide whether to drop it or not
// we can only safely drop it if we know 100% it was not sent as Reliable
if (_packetloss > 0 && sendType != -1 && CanDrop((EP2PSend)sendType))
{
float pl = _packetloss / 100.0f;
if (UnityEngine.Random.value < pl)
// dropping the packet
return false;
}
return true;
}
return false;
}
bool CanDrop(EP2PSend sendType)
{
return (sendType != EP2PSend.k_EP2PSendReliable && sendType != EP2PSend.k_EP2PSendReliableWithBuffering);
}
public bool SendP2PPacket(CSteamID steamID, byte[] data, uint dataLen, EP2PSend type, int nChannel = 0)
{
// delay send packet
// if Active, prepend a special byte sequence to the start of the message, this relies on a
// unique byte sequence which never appears in the wild. This reduces the bandwidth overhead
// of emulation to 0 when not active at the risk of a msg header clash.
if (IsActive)
{
// if packetloss, decide whether to drop it or not
if (_packetloss > 0 && CanDrop(type))
{
float pl = _packetloss / 100.0f;
if (UnityEngine.Random.value < pl)
// dropping the packet
return true;
}
byte[] newData = new byte[dataLen + HeaderLen];
newData[0] = 0x5C;
newData[1] = 0xC5;
newData[2] = (byte)type; // store the type so we know if we can drop it or not on receipt
Array.Copy(data, 0, newData, HeaderLen, dataLen);
// if latency, store for send later
if (_latency > 0)
{
float time = Time.realtimeSinceStartup + ((_latency / 1000.0f) * UnityEngine.Random.value);
_packetsOut.Add(new DeferredPacket
{
data = newData,
steamID = steamID,
dataLen = dataLen + HeaderLen,
sendType = type,
time = time
});
return true;
}
else
return SteamNetworking.SendP2PPacket(steamID, newData, dataLen + HeaderLen, type);
}
else
{
return SteamNetworking.SendP2PPacket(steamID, data, dataLen, type);
}
}
// must be called regularly to send delayed packets
public void Update()
{
// ensure list is sorted by time ASCending
_packetsOut.Sort((a, b) => { return a.time.CompareTo(b.time); });
float currentTime = Time.realtimeSinceStartup;
int deleteIndex = -1;
for (int i = 0; i < _packetsOut.Count; ++i)
{
DeferredPacket dp = _packetsOut[i];
if (dp.time < currentTime)
{
// send it! if it fails, exit and try again next frame
int nChannel = 0;
if (SteamNetworking.SendP2PPacket(dp.steamID, dp.data, dp.dataLen, dp.sendType, nChannel))
deleteIndex = i;
else
break;
}
else
break;
}
if (deleteIndex != -1)
{
_packetsOut.RemoveRange(0, deleteIndex + 1);
}
// if _latency > 0, read all steam packets here and buffer them
if (_latency > 0)
{
uint dataLen;
while (SteamNetworking.IsP2PPacketAvailable(out dataLen))
{
byte[] data = new byte[dataLen];
uint dataActualLen = 0;
CSteamID steamID;
if (SteamNetworking.ReadP2PPacket(data, dataLen, out dataActualLen, out steamID))
{
int sendType = -1; // unknown send type unless it contains our header
if (dataLen > 2 && data[0] == 0x5C && data[1] == 0xC5)
{
sendType = (int)data[2]; // if type != Reliable, we can drop it!
Array.Copy(data, HeaderLen, data, 0, dataLen - HeaderLen); // overwrite the NetEmu header
dataLen -= HeaderLen;
}
// if packetloss, decide whether to drop it or not
// we can only safely drop it if we know 100% it was not sent as Reliable
if (_packetloss > 0 && sendType != -1 && CanDrop((EP2PSend)sendType))
{
float pl = _packetloss / 100.0f;
if (UnityEngine.Random.value < pl)
// dropping the packet
continue;
}
// what time should we actually handle this packet?
float time = Time.realtimeSinceStartup + ((_latency / 1000.0f) * UnityEngine.Random.value);
_packetsIn.Add(new DeferredPacket
{
dataLen = dataLen,
data = data,
steamID = steamID,
sendType = (sendType == -1) ? EP2PSend.k_EP2PSendReliable : (EP2PSend)sendType,
time = time
});
}
}
_packetsIn.Sort((a, b) => { return a.time.CompareTo(b.time); });
}
}
}
public class UsageExample : MonoBehavior
{
SteamworksNetEmu netEmu = new SteamworksNetEmu();
void Start()
{
netEmu.Latency = 100; // add 100ms latency
netEmu.Packetloss = 50; // set 50% packetloss
}
void Update()
{
// replace calls to SteamNetworking.*P2P* with equivalent netEmu calls
// SteamNetworking.IsP2PPacketAvailable() => netEmu.IsP2PPacketAvailable()
// SteamNetworking.ReadP2PPacket() => netEmu.ReadP2PPacket()
// SteamNetworking.SendP2PPacket() => netEmu.SendP2PPacket()
byte [] msgData = new byte[1024];
CSteamID senderID;
uint msgSize = 0;
while (netEmu.IsP2PPacketAvailable(out msgSize)) {
if (netEmu.ReadP2PPacket(msgData, (uint)msgData.Length, out msgSize, out senderID)) {
// echo message back to sender
netEmu.SendP2PPacket(senderID, msgData, msgSize, EP2PSend.k_EP2PSendUnreliable);
}
}
// must call this regularly to send/recv messages from Steam
netEmu.Update();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment