Last active
December 12, 2019 12:37
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); }); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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