Basic Networking done
This commit is contained in:
178
Assets/Mirror/Runtime/Transport/FallbackTransport.cs
Normal file
178
Assets/Mirror/Runtime/Transport/FallbackTransport.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
// uses the first available transport for server and client.
|
||||
// example: to use Apathy if on Windows/Mac/Linux and fall back to Telepathy
|
||||
// otherwise.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// Deprecated 2021-05-13
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/fallback-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
[Obsolete("Fallback Transport will be retired. It was only needed for Apathy/Libuv. Use kcp or Telepathy instead, those run everywhere.")]
|
||||
public class FallbackTransport : Transport
|
||||
{
|
||||
public Transport[] transports;
|
||||
|
||||
// the first transport that is available on this platform
|
||||
Transport available;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (transports == null || transports.Length == 0)
|
||||
{
|
||||
throw new Exception("FallbackTransport requires at least 1 underlying transport");
|
||||
}
|
||||
available = GetAvailableTransport();
|
||||
Debug.Log("FallbackTransport available: " + available.GetType());
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
available.enabled = true;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
available.enabled = false;
|
||||
}
|
||||
|
||||
// The client just uses the first transport available
|
||||
Transport GetAvailableTransport()
|
||||
{
|
||||
foreach (Transport transport in transports)
|
||||
{
|
||||
if (transport.Available())
|
||||
{
|
||||
return transport;
|
||||
}
|
||||
}
|
||||
throw new Exception("No transport suitable for this platform");
|
||||
}
|
||||
|
||||
public override bool Available()
|
||||
{
|
||||
return available.Available();
|
||||
}
|
||||
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
available.OnClientConnected = OnClientConnected;
|
||||
available.OnClientDataReceived = OnClientDataReceived;
|
||||
available.OnClientError = OnClientError;
|
||||
available.OnClientDisconnected = OnClientDisconnected;
|
||||
available.ClientConnect(address);
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
foreach (Transport transport in transports)
|
||||
{
|
||||
if (transport.Available())
|
||||
{
|
||||
try
|
||||
{
|
||||
transport.ClientConnect(uri);
|
||||
available = transport;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// transport does not support the schema, just move on to the next one
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Exception("No transport suitable for this platform");
|
||||
}
|
||||
|
||||
public override bool ClientConnected()
|
||||
{
|
||||
return available.ClientConnected();
|
||||
}
|
||||
|
||||
public override void ClientDisconnect()
|
||||
{
|
||||
available.ClientDisconnect();
|
||||
}
|
||||
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
available.ClientSend(segment, channelId);
|
||||
}
|
||||
|
||||
// right now this just returns the first available uri,
|
||||
// should we return the list of all available uri?
|
||||
public override Uri ServerUri() => available.ServerUri();
|
||||
|
||||
public override bool ServerActive()
|
||||
{
|
||||
return available.ServerActive();
|
||||
}
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
return available.ServerGetClientAddress(connectionId);
|
||||
}
|
||||
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
available.ServerDisconnect(connectionId);
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
available.ServerSend(connectionId, segment, channelId);
|
||||
}
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
available.OnServerConnected = OnServerConnected;
|
||||
available.OnServerDataReceived = OnServerDataReceived;
|
||||
available.OnServerError = OnServerError;
|
||||
available.OnServerDisconnected = OnServerDisconnected;
|
||||
available.ServerStart();
|
||||
}
|
||||
|
||||
public override void ServerStop()
|
||||
{
|
||||
available.ServerStop();
|
||||
}
|
||||
|
||||
public override void ClientEarlyUpdate() => available.ClientEarlyUpdate();
|
||||
public override void ServerEarlyUpdate() => available.ServerEarlyUpdate();
|
||||
public override void ClientLateUpdate() => available.ClientLateUpdate();
|
||||
public override void ServerLateUpdate() => available.ServerLateUpdate();
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
available.Shutdown();
|
||||
}
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = 0)
|
||||
{
|
||||
// finding the max packet size in a fallback environment has to be
|
||||
// done very carefully:
|
||||
// * servers and clients might run different transports depending on
|
||||
// which platform they are on.
|
||||
// * there should only ever be ONE true max packet size for everyone,
|
||||
// otherwise a spawn message might be sent to all tcp sockets, but
|
||||
// be too big for some udp sockets. that would be a debugging
|
||||
// nightmare and allow for possible exploits and players on
|
||||
// different platforms seeing a different game state.
|
||||
// => the safest solution is to use the smallest max size for all
|
||||
// transports. that will never fail.
|
||||
int mininumAllowedSize = int.MaxValue;
|
||||
foreach (Transport transport in transports)
|
||||
{
|
||||
int size = transport.GetMaxPacketSize(channelId);
|
||||
mininumAllowedSize = Mathf.Min(size, mininumAllowedSize);
|
||||
}
|
||||
return mininumAllowedSize;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return available.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/FallbackTransport.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/FallbackTransport.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 330c9aab13d2d42069c6ebbe582b73ca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/Ignorance.meta
Normal file
8
Assets/Mirror/Runtime/Transport/Ignorance.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc98cd95e3fb22b4eb88082706967357
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/Ignorance/Core.meta
Normal file
8
Assets/Mirror/Runtime/Transport/Ignorance/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e03829cb9e647274db0f6a7b8b1b757b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,296 @@
|
||||
// Ignorance 1.4.x
|
||||
// Ignorance. It really kicks the Unity LLAPIs ass.
|
||||
// https://github.com/SoftwareGuy/Ignorance
|
||||
// -----------------
|
||||
// Copyright (c) 2019 - 2020 Matt Coburn (SoftwareGuy/Coburn64)
|
||||
// Ignorance Transport is licensed under the MIT license. Refer
|
||||
// to the LICENSE file for more information.
|
||||
|
||||
using ENet;
|
||||
// using NetStack.Buffers;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using Event = ENet.Event; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
using EventType = ENet.EventType; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
using Object = System.Object; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
public class IgnoranceClient
|
||||
{
|
||||
// Client connection address and port
|
||||
public string ConnectAddress = "127.0.0.1";
|
||||
public int ConnectPort = 7777;
|
||||
// How many channels are expected
|
||||
public int ExpectedChannels = 2;
|
||||
// Native poll waiting time
|
||||
public int PollTime = 1;
|
||||
// Maximum Packet Size
|
||||
public int MaximumPacketSize = 33554432;
|
||||
// General Verbosity by default.
|
||||
public int Verbosity = 1;
|
||||
|
||||
// Queues
|
||||
public ConcurrentQueue<IgnoranceIncomingPacket> Incoming = new ConcurrentQueue<IgnoranceIncomingPacket>();
|
||||
public ConcurrentQueue<IgnoranceOutgoingPacket> Outgoing = new ConcurrentQueue<IgnoranceOutgoingPacket>();
|
||||
public ConcurrentQueue<IgnoranceCommandPacket> Commands = new ConcurrentQueue<IgnoranceCommandPacket>();
|
||||
public ConcurrentQueue<IgnoranceConnectionEvent> ConnectionEvents = new ConcurrentQueue<IgnoranceConnectionEvent>();
|
||||
public ConcurrentQueue<IgnoranceClientStats> StatusUpdates = new ConcurrentQueue<IgnoranceClientStats>();
|
||||
|
||||
public bool IsAlive => WorkerThread != null && WorkerThread.IsAlive;
|
||||
|
||||
private volatile bool CeaseOperation = false;
|
||||
private Thread WorkerThread;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Debug.Log("IgnoranceClient.Start()");
|
||||
|
||||
if (WorkerThread != null && WorkerThread.IsAlive)
|
||||
{
|
||||
// Cannot do that.
|
||||
Debug.LogError("A worker thread is already running. Cannot start another.");
|
||||
return;
|
||||
}
|
||||
|
||||
CeaseOperation = false;
|
||||
ThreadParamInfo threadParams = new ThreadParamInfo()
|
||||
{
|
||||
Address = ConnectAddress,
|
||||
Port = ConnectPort,
|
||||
Channels = ExpectedChannels,
|
||||
PollTime = PollTime,
|
||||
PacketSizeLimit = MaximumPacketSize,
|
||||
Verbosity = Verbosity
|
||||
};
|
||||
|
||||
// Drain queues.
|
||||
if (Incoming != null) while (Incoming.TryDequeue(out _)) ;
|
||||
if (Outgoing != null) while (Outgoing.TryDequeue(out _)) ;
|
||||
if (Commands != null) while (Commands.TryDequeue(out _)) ;
|
||||
if (ConnectionEvents != null) while (ConnectionEvents.TryDequeue(out _)) ;
|
||||
if (StatusUpdates != null) while (StatusUpdates.TryDequeue(out _)) ;
|
||||
|
||||
WorkerThread = new Thread(ThreadWorker);
|
||||
WorkerThread.Start(threadParams);
|
||||
|
||||
Debug.Log("Client has dispatched worker thread.");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
Debug.Log("Telling client thread to stop, this may take a while depending on network load");
|
||||
CeaseOperation = true;
|
||||
}
|
||||
|
||||
// This runs in a seperate thread, be careful accessing anything outside of it's thread
|
||||
// or you may get an AccessViolation/crash.
|
||||
private void ThreadWorker(Object parameters)
|
||||
{
|
||||
if (Verbosity > 0)
|
||||
Debug.Log("Ignorance Client: Initializing. Please stand by...");
|
||||
|
||||
ThreadParamInfo setupInfo;
|
||||
Address clientAddress = new Address();
|
||||
Peer clientPeer;
|
||||
Host clientENetHost;
|
||||
Event clientENetEvent;
|
||||
IgnoranceClientStats icsu = default;
|
||||
|
||||
// Grab the setup information.
|
||||
if (parameters.GetType() == typeof(ThreadParamInfo))
|
||||
{
|
||||
setupInfo = (ThreadParamInfo)parameters;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Ignorance Client: Startup failure: Invalid thread parameters. Aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to initialize ENet inside the thread.
|
||||
if (Library.Initialize())
|
||||
{
|
||||
Debug.Log("Ignorance Client: ENet initialized.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Ignorance Client: Failed to initialize ENet. This threads' fucked.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to connect to our target.
|
||||
clientAddress.SetHost(setupInfo.Address);
|
||||
clientAddress.Port = (ushort)setupInfo.Port;
|
||||
|
||||
using (clientENetHost = new Host())
|
||||
{
|
||||
// TODO: Maybe try catch this
|
||||
clientENetHost.Create();
|
||||
clientPeer = clientENetHost.Connect(clientAddress, setupInfo.Channels);
|
||||
|
||||
while (!CeaseOperation)
|
||||
{
|
||||
bool pollComplete = false;
|
||||
|
||||
// Step 0: Handle commands.
|
||||
while (Commands.TryDequeue(out IgnoranceCommandPacket commandPacket))
|
||||
{
|
||||
switch (commandPacket.Type)
|
||||
{
|
||||
default:
|
||||
break;
|
||||
|
||||
case IgnoranceCommandType.ClientWantsToStop:
|
||||
CeaseOperation = true;
|
||||
break;
|
||||
|
||||
case IgnoranceCommandType.ClientRequestsStatusUpdate:
|
||||
// Respond with statistics so far.
|
||||
if (!clientPeer.IsSet)
|
||||
break;
|
||||
|
||||
icsu.RTT = clientPeer.RoundTripTime;
|
||||
|
||||
icsu.BytesReceived = clientPeer.BytesReceived;
|
||||
icsu.BytesSent = clientPeer.BytesSent;
|
||||
|
||||
icsu.PacketsReceived = clientENetHost.PacketsReceived;
|
||||
icsu.PacketsSent = clientPeer.PacketsSent;
|
||||
icsu.PacketsLost = clientPeer.PacketsLost;
|
||||
|
||||
StatusUpdates.Enqueue(icsu);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Step 1: Send out data.
|
||||
// ---> Sending to Server
|
||||
while (Outgoing.TryDequeue(out IgnoranceOutgoingPacket outgoingPacket))
|
||||
{
|
||||
// TODO: Revise this, could we tell the Peer to disconnect right here?
|
||||
// Stop early if we get a client stop packet.
|
||||
// if (outgoingPacket.Type == IgnorancePacketType.ClientWantsToStop) break;
|
||||
|
||||
int ret = clientPeer.Send(outgoingPacket.Channel, ref outgoingPacket.Payload);
|
||||
|
||||
if (ret < 0 && setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Client: ENet error code {ret} while sending packet to Peer {outgoingPacket.NativePeerId}.");
|
||||
}
|
||||
|
||||
// Step 2:
|
||||
// <----- Receive Data packets
|
||||
// This loops until polling is completed. It may take a while, if it's
|
||||
// a slow networking day.
|
||||
while (!pollComplete)
|
||||
{
|
||||
Packet incomingPacket;
|
||||
Peer incomingPeer;
|
||||
int incomingPacketLength;
|
||||
|
||||
// Any events worth checking out?
|
||||
if (clientENetHost.CheckEvents(out clientENetEvent) <= 0)
|
||||
{
|
||||
// If service time is met, break out of it.
|
||||
if (clientENetHost.Service(setupInfo.PollTime, out clientENetEvent) <= 0) break;
|
||||
|
||||
// Poll is done.
|
||||
pollComplete = true;
|
||||
}
|
||||
|
||||
// Setup the packet references.
|
||||
incomingPeer = clientENetEvent.Peer;
|
||||
|
||||
// Now, let's handle those events.
|
||||
switch (clientENetEvent.Type)
|
||||
{
|
||||
case EventType.None:
|
||||
default:
|
||||
break;
|
||||
|
||||
case EventType.Connect:
|
||||
ConnectionEvents.Enqueue(new IgnoranceConnectionEvent()
|
||||
{
|
||||
NativePeerId = incomingPeer.ID,
|
||||
IP = incomingPeer.IP,
|
||||
Port = incomingPeer.Port
|
||||
});
|
||||
break;
|
||||
|
||||
case EventType.Disconnect:
|
||||
case EventType.Timeout:
|
||||
ConnectionEvents.Enqueue(new IgnoranceConnectionEvent()
|
||||
{
|
||||
WasDisconnect = true
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case EventType.Receive:
|
||||
// Receive event type usually includes a packet; so cache its reference.
|
||||
incomingPacket = clientENetEvent.Packet;
|
||||
|
||||
if (!incomingPacket.IsSet)
|
||||
{
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Client: A receive event did not supply us with a packet to work with. This should never happen.");
|
||||
break;
|
||||
}
|
||||
|
||||
incomingPacketLength = incomingPacket.Length;
|
||||
|
||||
// Never consume more than we can have capacity for.
|
||||
if (incomingPacketLength > setupInfo.PacketSizeLimit)
|
||||
{
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Client: Incoming packet is too big. My limit is {setupInfo.PacketSizeLimit} byte(s) whilest this packet is {incomingPacketLength} bytes.");
|
||||
|
||||
incomingPacket.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
IgnoranceIncomingPacket incomingQueuePacket = new IgnoranceIncomingPacket
|
||||
{
|
||||
Channel = clientENetEvent.ChannelID,
|
||||
NativePeerId = incomingPeer.ID,
|
||||
Payload = incomingPacket
|
||||
};
|
||||
|
||||
Incoming.Enqueue(incomingQueuePacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("Ignorance Server: Shutdown commencing, disconnecting and flushing connection.");
|
||||
|
||||
// Flush the client and disconnect.
|
||||
clientPeer.Disconnect(0);
|
||||
clientENetHost.Flush();
|
||||
|
||||
// Fix for client stuck in limbo, since the disconnection event may not be fired until next loop.
|
||||
ConnectionEvents.Enqueue(new IgnoranceConnectionEvent()
|
||||
{
|
||||
WasDisconnect = true
|
||||
});
|
||||
}
|
||||
|
||||
// Deinitialize
|
||||
Library.Deinitialize();
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.Log("Ignorance Client: Shutdown complete.");
|
||||
}
|
||||
|
||||
|
||||
private struct ThreadParamInfo
|
||||
{
|
||||
public int Channels;
|
||||
public int PollTime;
|
||||
public int Port;
|
||||
public int PacketSizeLimit;
|
||||
public int Verbosity;
|
||||
public string Address;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7b9e2c091c3d42439840a02fe700252
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,328 @@
|
||||
// Ignorance 1.4.x
|
||||
// Ignorance. It really kicks the Unity LLAPIs ass.
|
||||
// https://github.com/SoftwareGuy/Ignorance
|
||||
// -----------------
|
||||
// Copyright (c) 2019 - 2020 Matt Coburn (SoftwareGuy/Coburn64)
|
||||
// Ignorance Transport is licensed under the MIT license. Refer
|
||||
// to the LICENSE file for more information.
|
||||
|
||||
using ENet;
|
||||
// using NetStack.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using Event = ENet.Event; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
using EventType = ENet.EventType; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
using Object = System.Object; // fixes CS0104 ambigous reference between the same thing in UnityEngine
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
public class IgnoranceServer
|
||||
{
|
||||
// Server Properties
|
||||
// - Bind Settings
|
||||
public string BindAddress = "127.0.0.1";
|
||||
public int BindPort = 7777;
|
||||
// - Maximum allowed channels, peers, etc.
|
||||
public int MaximumChannels = 2;
|
||||
public int MaximumPeers = 100;
|
||||
public int MaximumPacketSize = 33554432; // ENet.cs: uint maxPacketSize = 32 * 1024 * 1024 = 33554432
|
||||
// - Native poll waiting time
|
||||
public int PollTime = 1;
|
||||
public int Verbosity = 1;
|
||||
|
||||
public bool IsAlive => WorkerThread != null && WorkerThread.IsAlive;
|
||||
|
||||
private volatile bool CeaseOperation = false;
|
||||
|
||||
// Queues
|
||||
public ConcurrentQueue<IgnoranceIncomingPacket> Incoming = new ConcurrentQueue<IgnoranceIncomingPacket>();
|
||||
public ConcurrentQueue<IgnoranceOutgoingPacket> Outgoing = new ConcurrentQueue<IgnoranceOutgoingPacket>();
|
||||
public ConcurrentQueue<IgnoranceCommandPacket> Commands = new ConcurrentQueue<IgnoranceCommandPacket>();
|
||||
public ConcurrentQueue<IgnoranceConnectionEvent> ConnectionEvents = new ConcurrentQueue<IgnoranceConnectionEvent>();
|
||||
public ConcurrentQueue<IgnoranceConnectionEvent> DisconnectionEvents = new ConcurrentQueue<IgnoranceConnectionEvent>();
|
||||
|
||||
// Thread
|
||||
private Thread WorkerThread;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (WorkerThread != null && WorkerThread.IsAlive)
|
||||
{
|
||||
// Cannot do that.
|
||||
Debug.LogError("A worker thread is already running. Cannot start another.");
|
||||
return;
|
||||
}
|
||||
|
||||
CeaseOperation = false;
|
||||
ThreadParamInfo threadParams = new ThreadParamInfo()
|
||||
{
|
||||
Address = BindAddress,
|
||||
Port = BindPort,
|
||||
Peers = MaximumPeers,
|
||||
Channels = MaximumChannels,
|
||||
PollTime = PollTime,
|
||||
PacketSizeLimit = MaximumPacketSize,
|
||||
Verbosity = Verbosity
|
||||
};
|
||||
|
||||
// Drain queues.
|
||||
if (Incoming != null) while (Incoming.TryDequeue(out _)) ;
|
||||
if (Outgoing != null) while (Outgoing.TryDequeue(out _)) ;
|
||||
if (Commands != null) while (Commands.TryDequeue(out _)) ;
|
||||
if (ConnectionEvents != null) while (ConnectionEvents.TryDequeue(out _)) ;
|
||||
if (DisconnectionEvents != null) while (DisconnectionEvents.TryDequeue(out _)) ;
|
||||
|
||||
WorkerThread = new Thread(ThreadWorker);
|
||||
WorkerThread.Start(threadParams);
|
||||
|
||||
// Announce
|
||||
if (Verbosity > 0)
|
||||
Debug.Log("Server has dispatched worker thread.");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (Verbosity > 0)
|
||||
Debug.Log("Telling server thread to stop, this may take a while depending on network load");
|
||||
CeaseOperation = true;
|
||||
}
|
||||
|
||||
private void ThreadWorker(Object parameters)
|
||||
{
|
||||
if (Verbosity > 0)
|
||||
Debug.Log("Ignorance Server: Initializing. Please stand by...");
|
||||
|
||||
// Thread cache items
|
||||
ThreadParamInfo setupInfo;
|
||||
Address serverAddress = new Address();
|
||||
Host serverENetHost;
|
||||
Event serverENetEvent;
|
||||
|
||||
Peer[] serverPeerArray;
|
||||
|
||||
// Grab the setup information.
|
||||
if (parameters.GetType() == typeof(ThreadParamInfo))
|
||||
{
|
||||
setupInfo = (ThreadParamInfo)parameters;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Ignorance Server: Startup failure: Invalid thread parameters. Aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to initialize ENet inside the thread.
|
||||
if (Library.Initialize())
|
||||
{
|
||||
Debug.Log("Ignorance Server: ENet initialized.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Ignorance Server: Failed to initialize ENet. This threads' fucked.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure the server address.
|
||||
serverAddress.SetHost(setupInfo.Address);
|
||||
serverAddress.Port = (ushort)setupInfo.Port;
|
||||
serverPeerArray = new Peer[setupInfo.Peers];
|
||||
|
||||
using (serverENetHost = new Host())
|
||||
{
|
||||
// Create the server object.
|
||||
serverENetHost.Create(serverAddress, setupInfo.Peers, setupInfo.Channels);
|
||||
|
||||
// Loop until we're told to cease operations.
|
||||
while (!CeaseOperation)
|
||||
{
|
||||
// Intermission: Command Handling
|
||||
while (Commands.TryDequeue(out IgnoranceCommandPacket commandPacket))
|
||||
{
|
||||
switch (commandPacket.Type)
|
||||
{
|
||||
default:
|
||||
break;
|
||||
|
||||
// Boot a Peer off the Server.
|
||||
case IgnoranceCommandType.ServerKickPeer:
|
||||
uint targetPeer = commandPacket.PeerId;
|
||||
|
||||
if (!serverPeerArray[targetPeer].IsSet) continue;
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.Log($"Ignorance Server: Booting Peer {targetPeer} off this server instance.");
|
||||
|
||||
IgnoranceConnectionEvent iced = new IgnoranceConnectionEvent()
|
||||
{
|
||||
WasDisconnect = true,
|
||||
NativePeerId = targetPeer
|
||||
};
|
||||
|
||||
DisconnectionEvents.Enqueue(iced);
|
||||
|
||||
// Disconnect and reset the peer array's entry for that peer.
|
||||
serverPeerArray[targetPeer].DisconnectNow(0);
|
||||
serverPeerArray[targetPeer] = default;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step One:
|
||||
// ---> Sending to peers
|
||||
while (Outgoing.TryDequeue(out IgnoranceOutgoingPacket outgoingPacket))
|
||||
{
|
||||
// Only create a packet if the server knows the peer.
|
||||
if (serverPeerArray[outgoingPacket.NativePeerId].IsSet)
|
||||
{
|
||||
int ret = serverPeerArray[outgoingPacket.NativePeerId].Send(outgoingPacket.Channel, ref outgoingPacket.Payload);
|
||||
|
||||
if (ret < 0 && setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Server: ENet error code {ret} while sending packet to Peer {outgoingPacket.NativePeerId}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// A peer might have disconnected, this is OK - just log the packet if set to paranoid.
|
||||
if (setupInfo.Verbosity > 1)
|
||||
Debug.LogWarning("Ignorance Server: Can't send packet, a native peer object is not set. This may be normal if the Peer has disconnected before this send cycle.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Step 2
|
||||
// <--- Receiving from peers
|
||||
bool pollComplete = false;
|
||||
|
||||
while (!pollComplete)
|
||||
{
|
||||
Packet incomingPacket;
|
||||
Peer incomingPeer;
|
||||
int incomingPacketLength;
|
||||
|
||||
// Any events happening?
|
||||
if (serverENetHost.CheckEvents(out serverENetEvent) <= 0)
|
||||
{
|
||||
// If service time is met, break out of it.
|
||||
if (serverENetHost.Service(setupInfo.PollTime, out serverENetEvent) <= 0) break;
|
||||
|
||||
pollComplete = true;
|
||||
}
|
||||
|
||||
// Setup the packet references.
|
||||
incomingPeer = serverENetEvent.Peer;
|
||||
|
||||
switch (serverENetEvent.Type)
|
||||
{
|
||||
// Idle.
|
||||
case EventType.None:
|
||||
default:
|
||||
break;
|
||||
|
||||
// Connection Event.
|
||||
case EventType.Connect:
|
||||
if (setupInfo.Verbosity > 1)
|
||||
Debug.Log("Ignorance Server: Here comes a new Peer connection.");
|
||||
|
||||
IgnoranceConnectionEvent ice = new IgnoranceConnectionEvent()
|
||||
{
|
||||
NativePeerId = incomingPeer.ID,
|
||||
IP = incomingPeer.IP,
|
||||
Port = incomingPeer.Port
|
||||
};
|
||||
|
||||
ConnectionEvents.Enqueue(ice);
|
||||
|
||||
// Assign a reference to the Peer.
|
||||
serverPeerArray[incomingPeer.ID] = incomingPeer;
|
||||
break;
|
||||
|
||||
// Disconnect/Timeout. Mirror doesn't care if it's either, so we lump them together.
|
||||
case EventType.Disconnect:
|
||||
case EventType.Timeout:
|
||||
if (!serverPeerArray[incomingPeer.ID].IsSet) break;
|
||||
|
||||
if (setupInfo.Verbosity > 1)
|
||||
Debug.Log("Ignorance Server: Peer disconnection.");
|
||||
|
||||
IgnoranceConnectionEvent iced = new IgnoranceConnectionEvent()
|
||||
{
|
||||
WasDisconnect = true,
|
||||
NativePeerId = incomingPeer.ID
|
||||
};
|
||||
|
||||
DisconnectionEvents.Enqueue(iced);
|
||||
|
||||
// Reset the peer array's entry for that peer.
|
||||
serverPeerArray[incomingPeer.ID] = default;
|
||||
break;
|
||||
|
||||
case EventType.Receive:
|
||||
// Receive event type usually includes a packet; so cache its reference.
|
||||
incomingPacket = serverENetEvent.Packet;
|
||||
if (!incomingPacket.IsSet)
|
||||
{
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Server: A receive event did not supply us with a packet to work with. This should never happen.");
|
||||
break;
|
||||
}
|
||||
|
||||
incomingPacketLength = incomingPacket.Length;
|
||||
|
||||
// Firstly check if the packet is too big. If it is, do not process it - drop it.
|
||||
if (incomingPacketLength > setupInfo.PacketSizeLimit)
|
||||
{
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.LogWarning($"Ignorance Server: Incoming packet is too big. My limit is {setupInfo.PacketSizeLimit} byte(s) whilest this packet is {incomingPacketLength} bytes.");
|
||||
|
||||
incomingPacket.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
IgnoranceIncomingPacket incomingQueuePacket = new IgnoranceIncomingPacket
|
||||
{
|
||||
Channel = serverENetEvent.ChannelID,
|
||||
NativePeerId = incomingPeer.ID,
|
||||
Payload = incomingPacket,
|
||||
};
|
||||
|
||||
// Enqueue.
|
||||
Incoming.Enqueue(incomingQueuePacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Verbosity > 0)
|
||||
Debug.Log("Ignorance Server: Shutdown commencing, flushing connections.");
|
||||
|
||||
// Cleanup and flush everything.
|
||||
serverENetHost.Flush();
|
||||
|
||||
// Kick everyone.
|
||||
for (int i = 0; i < serverPeerArray.Length; i++)
|
||||
{
|
||||
if (!serverPeerArray[i].IsSet) continue;
|
||||
serverPeerArray[i].DisconnectNow(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush again to ensure ENet gets those Disconnection stuff out.
|
||||
// May not be needed; better to err on side of caution
|
||||
|
||||
if (setupInfo.Verbosity > 0)
|
||||
Debug.Log("Ignorance Server: Shutdown complete.");
|
||||
|
||||
Library.Deinitialize();
|
||||
}
|
||||
|
||||
private struct ThreadParamInfo
|
||||
{
|
||||
public int Channels;
|
||||
public int Peers;
|
||||
public int PollTime;
|
||||
public int Port;
|
||||
public int PacketSizeLimit;
|
||||
public int Verbosity;
|
||||
public string Address;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1069f42b88a4adb4ab1990cec4949343
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12f903db684732e45b130ad56f7c86c1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1411
Assets/Mirror/Runtime/Transport/Ignorance/Dependencies/ENet.cs
Normal file
1411
Assets/Mirror/Runtime/Transport/Ignorance/Dependencies/ENet.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12a7875e95f5ebb4a9b58390441dd933
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/Ignorance/Editor.meta
Normal file
8
Assets/Mirror/Runtime/Transport/Ignorance/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0995e08af14888348b42ecaa6eb21544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the given define symbols to PlayerSettings define symbols.
|
||||
/// Just add your own define symbols to the Symbols property at the below.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public class AddIgnoranceDefine : Editor
|
||||
{
|
||||
private static string existingDefines = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Symbols that will be added to the editor
|
||||
/// </summary>
|
||||
public static readonly string[] Symbols = new string[] {
|
||||
"IGNORANCE", // Ignorance exists
|
||||
"IGNORANCE_1", // Major version
|
||||
"IGNORANCE_1_4" // Major and minor version
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Do not remove these symbols
|
||||
/// </summary>
|
||||
public static readonly string[] DoNotRemoveTheseSymbols = new string[]
|
||||
{
|
||||
"IGNORANCE_NO_UPNP",
|
||||
"IGNORANCE_MIRROR_POLLING"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Add define symbols as soon as Unity gets done compiling.
|
||||
/// </summary>
|
||||
static AddIgnoranceDefine()
|
||||
{
|
||||
// Get the current scripting defines
|
||||
string definesString = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
|
||||
if (existingDefines == definesString)
|
||||
{
|
||||
// 1.2.6: There is no need to apply the changes, return.
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the string to a list
|
||||
List<string> allDefines = definesString.Split(';').ToList();
|
||||
// Remove any old version defines from previous installs
|
||||
allDefines.RemoveAll(IsSafeToRemove);
|
||||
// x => x.StartsWith("IGNORANCE") && !DoesSymbolExistInBlacklist(x));
|
||||
// Add any symbols that weren't already in the list
|
||||
allDefines.AddRange(Symbols.Except(allDefines));
|
||||
|
||||
string newDefines = string.Join(";", allDefines.ToArray());
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(
|
||||
EditorUserBuildSettings.selectedBuildTargetGroup,
|
||||
newDefines
|
||||
);
|
||||
|
||||
existingDefines = newDefines;
|
||||
}
|
||||
|
||||
// 1.2.4: Workaround to stop things from eating custom IGNORANCE_ symbols
|
||||
static bool DoesSymbolExistInBlacklist(string symbol)
|
||||
{
|
||||
foreach(string s in DoNotRemoveTheseSymbols)
|
||||
{
|
||||
if (s == symbol.Trim()) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsSafeToRemove (string input)
|
||||
{
|
||||
if (input.StartsWith("IGNORANCE") && !DoesSymbolExistInBlacklist(input)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed8acbde141f2d8469baf2142712de9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
public class IgnoranceToolbox
|
||||
{
|
||||
#pragma warning disable IDE0051
|
||||
[MenuItem("Ignorance/Mirror/Switch Update Method")]
|
||||
public static void SwitchIgnoranceUpdateMethod ()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[MenuItem("Ignorance/Debug/Reveal ENet Native Library Name")]
|
||||
public static void RevealEnetLibraryName()
|
||||
{
|
||||
EditorUtility.DisplayDialog("Enet Library Name", $"Use this for debugging.\nYour platform expects the native Enet library to be called: {ENet.Native.nativeLibraryName}", "Got it");
|
||||
}
|
||||
|
||||
[MenuItem("Ignorance/RTFM/Github Repository")]
|
||||
private static void LaunchGithubRepo()
|
||||
{
|
||||
UnityEngine.Application.OpenURL("https://github.com/SoftwareGuy/Ignorance");
|
||||
}
|
||||
|
||||
[MenuItem("Ignorance/RTFM/Github Issue Tracker")]
|
||||
private static void LaunchGithubIssueTracker()
|
||||
{
|
||||
UnityEngine.Application.OpenURL("https://github.com/SoftwareGuy/Ignorance/issues");
|
||||
}
|
||||
|
||||
[MenuItem("Ignorance/RTFM/ENet-CSharp Fork")]
|
||||
private static void LaunchENetCSharpForkRepo()
|
||||
{
|
||||
UnityEngine.Application.OpenURL("https://github.com/SoftwareGuy/ENet-CSharp");
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fdecc996313d614ca16214d4e2b9162
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
746
Assets/Mirror/Runtime/Transport/Ignorance/Ignorance.cs
Normal file
746
Assets/Mirror/Runtime/Transport/Ignorance/Ignorance.cs
Normal file
@@ -0,0 +1,746 @@
|
||||
// Ignorance 1.4.x
|
||||
// Ignorance. It really kicks the Unity LLAPIs ass.
|
||||
// https://github.com/SoftwareGuy/Ignorance
|
||||
// -----------------
|
||||
// Copyright (c) 2019 - 2020 Matt Coburn (SoftwareGuy/Coburn64)
|
||||
// Ignorance Transport is licensed under the MIT license. Refer
|
||||
// to the LICENSE file for more information.
|
||||
// -----------------
|
||||
// Ignorance Experimental (New) Version
|
||||
// -----------------
|
||||
using ENet;
|
||||
using Mirror;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class Ignorance : Transport
|
||||
{
|
||||
#region Inspector options
|
||||
public int port = 7777;
|
||||
|
||||
[Header("Debug & Logging Configuration")]
|
||||
[Tooltip("How verbose do you want Ignorance to be?")]
|
||||
public IgnoranceLogType LogType = IgnoranceLogType.Standard;
|
||||
[Tooltip("Uses OnGUI to present you with statistics for Server and Client backend instances.")]
|
||||
public bool DebugDisplay = false;
|
||||
|
||||
[Header("Server Configuration")]
|
||||
[Tooltip("Should the server bind to all interfaces?")]
|
||||
public bool serverBindsAll = true;
|
||||
[Tooltip("This is only used if Server Binds All is unticked.")]
|
||||
public string serverBindAddress = string.Empty;
|
||||
[Tooltip("This tells ENet how many Peer slots to create. Helps performance, avoids looping over huge native arrays. Recommended: Max Mirror players, rounded to nearest 10. (Example: 16 -> 20).")]
|
||||
public int serverMaxPeerCapacity = 50;
|
||||
[Tooltip("How long ENet waits in native world. The higher this value, the more CPU usage. Lower values may/may not impact performance at high packet load.")]
|
||||
public int serverMaxNativeWaitTime = 1;
|
||||
|
||||
[Header("Client Configuration")]
|
||||
[Tooltip("How long ENet waits in native world. The higher this value, the more CPU usage used. This is for the client, unlike the one above. Higher value probably trades CPU for more responsive networking.")]
|
||||
public int clientMaxNativeWaitTime = 3;
|
||||
[Tooltip("Interval between asking ENet for client status updates. Set to -1 to disable.")]
|
||||
public int clientStatusUpdateInterval = -1;
|
||||
|
||||
[Header("Channel Configuration")]
|
||||
[Tooltip("You must define your channels in the array shown here, otherwise ENet will not know what channel delivery type to use.")]
|
||||
public IgnoranceChannelTypes[] Channels;
|
||||
|
||||
[Header("Low-level Tweaking")]
|
||||
[Tooltip("Used internally to keep allocations to a minimum. This is how much memory will be consumed by the packet buffer on startup, and then reused.")]
|
||||
public int PacketBufferCapacity = 4096;
|
||||
|
||||
[Tooltip("For UDP based protocols, it's best to keep your data under the safe MTU of 1200 bytes. You can increase this, however beware this may open you up to allocation attacks.")]
|
||||
public int MaxAllowedPacketSize = 33554432;
|
||||
#endregion
|
||||
|
||||
#region Public Statistics
|
||||
public IgnoranceClientStats ClientStatistics;
|
||||
#endregion
|
||||
|
||||
#if MIRROR_26_0_OR_NEWER
|
||||
public override bool Available()
|
||||
{
|
||||
// Ignorance is not available for Unity WebGL, the PS4 (no dev kit to confirm) or Switch (port exists but I have no access to said code).
|
||||
// Ignorance is available for most other operating systems.
|
||||
#if (UNITY_WEBGL || UNITY_PS4 || UNITY_SWITCH)
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (LogType != IgnoranceLogType.Nothing)
|
||||
Debug.Log($"Thanks for using Ignorance {IgnoranceInternals.Version}. Keep up to date, report bugs and support the developer at https://github.com/SoftwareGuy/Ignorance!");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Ignorance v{IgnoranceInternals.Version}";
|
||||
}
|
||||
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
ClientState = ConnectionState.Connecting;
|
||||
cachedConnectionAddress = address;
|
||||
|
||||
// Initialize.
|
||||
InitializeClientBackend();
|
||||
|
||||
// Get going.
|
||||
ignoreDataPackets = false;
|
||||
|
||||
// Start!
|
||||
Client.Start();
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != IgnoranceInternals.Scheme)
|
||||
throw new ArgumentException($"You used an invalid URI: {uri}. Please use {IgnoranceInternals.Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
if (!uri.IsDefaultPort)
|
||||
// Set the communication port to the one specified.
|
||||
port = uri.Port;
|
||||
|
||||
// Pass onwards to the proper handler.
|
||||
ClientConnect(uri.Host);
|
||||
}
|
||||
|
||||
public override bool ClientConnected() => ClientState == ConnectionState.Connected;
|
||||
|
||||
public override void ClientDisconnect()
|
||||
{
|
||||
if (Client != null)
|
||||
Client.Stop();
|
||||
|
||||
// TODO: Figure this one out to see if it's related to a race condition.
|
||||
// Maybe experiment with a while loop to pause main thread when disconnecting,
|
||||
// since client might not stop on a dime.
|
||||
// while(Client.IsAlive) ;
|
||||
// v1.4.0b1: Probably fixed in IgnoranceClient.cs; need further testing.
|
||||
|
||||
// ignoreDataPackets = true;
|
||||
ClientState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
#if !MIRROR_37_0_OR_NEWER
|
||||
public override void ClientSend(int channelId, ArraySegment<byte> segment)
|
||||
#else
|
||||
// v1.4.0b6: Mirror rearranged the ClientSend params, so we need to apply a fix for that or
|
||||
// we end up using the obsoleted version. The obsolete version isn't a fatal error, but
|
||||
// it's best to stick with the new structures.
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
#endif
|
||||
{
|
||||
if (Client == null)
|
||||
{
|
||||
Debug.LogError("Client object is null, this shouldn't really happen but it did...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelId < 0 || channelId > Channels.Length)
|
||||
{
|
||||
Debug.LogError("Channel ID is out of bounds.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create our struct...
|
||||
Packet clientOutgoingPacket = default;
|
||||
int byteCount = segment.Count;
|
||||
int byteOffset = segment.Offset;
|
||||
// Set our desired flags...
|
||||
PacketFlags desiredFlags = (PacketFlags)Channels[channelId];
|
||||
|
||||
// Warn if over recommended MTU...
|
||||
bool flagsSet = (desiredFlags & ReliableOrUnreliableFragmented) > 0;
|
||||
|
||||
if (LogType != IgnoranceLogType.Nothing && byteCount > 1200 && !flagsSet)
|
||||
Debug.LogWarning($"Warning: Client trying to send a Unreliable packet bigger than the recommended ENet 1200 byte MTU ({byteCount} > 1200). ENet will force Reliable Fragmented delivery.");
|
||||
|
||||
// Create the packet.
|
||||
clientOutgoingPacket.Create(segment.Array, byteOffset, byteCount + byteOffset, desiredFlags);
|
||||
// byteCount
|
||||
|
||||
// Enqueue the packet.
|
||||
IgnoranceOutgoingPacket dispatchPacket = new IgnoranceOutgoingPacket
|
||||
{
|
||||
Channel = (byte)channelId,
|
||||
Payload = clientOutgoingPacket
|
||||
};
|
||||
|
||||
// Pass the packet onto the thread for dispatch.
|
||||
Client.Outgoing.Enqueue(dispatchPacket);
|
||||
}
|
||||
|
||||
public override bool ServerActive()
|
||||
{
|
||||
// Very simple check.
|
||||
return Server != null && Server.IsAlive;
|
||||
}
|
||||
|
||||
#if !MIRROR_37_0_OR_NEWER
|
||||
// Workaround for legacy Mirror versions.
|
||||
public override bool ServerDisconnect(int connectionId) => ServerDisconnectLegacy(connectionId);
|
||||
#else
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
Debug.LogError("Cannot enqueue kick packet; our Server object is null. Something has gone wrong.");
|
||||
// Return here because otherwise we will get a NRE when trying to enqueue the kick packet.
|
||||
return;
|
||||
}
|
||||
|
||||
IgnoranceCommandPacket kickPacket = new IgnoranceCommandPacket
|
||||
{
|
||||
Type = IgnoranceCommandType.ServerKickPeer,
|
||||
PeerId = (uint)connectionId - 1 // ENet's native peer ID will be ConnID - 1
|
||||
};
|
||||
|
||||
// Pass the packet onto the thread for dispatch.
|
||||
Server.Commands.Enqueue(kickPacket);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
if (ConnectionLookupDict.TryGetValue(connectionId, out PeerConnectionData details))
|
||||
return $"{details.IP}:{details.Port}";
|
||||
|
||||
return "(unavailable)";
|
||||
}
|
||||
|
||||
#if !MIRROR_37_0_OR_NEWER
|
||||
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment)
|
||||
#else
|
||||
// v1.4.0b6: Mirror rearranged the ServerSend params, so we need to apply a fix for that or
|
||||
// we end up using the obsoleted version. The obsolete version isn't a fatal error, but
|
||||
// it's best to stick with the new structures.
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
#endif
|
||||
{
|
||||
// Debug.Log($"ServerSend({connectionId}, {channelId}, <{segment.Count} byte segment>)");
|
||||
|
||||
if (Server == null)
|
||||
{
|
||||
Debug.LogError("Cannot enqueue data packet; our Server object is null. Something has gone wrong.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelId < 0 || channelId > Channels.Length)
|
||||
{
|
||||
Debug.LogError("Channel ID is out of bounds.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Packet Struct
|
||||
Packet serverOutgoingPacket = default;
|
||||
int byteCount = segment.Count;
|
||||
int byteOffset = segment.Offset;
|
||||
PacketFlags desiredFlags = (PacketFlags)Channels[channelId];
|
||||
|
||||
// Warn if over recommended MTU
|
||||
bool flagsSet = (desiredFlags & ReliableOrUnreliableFragmented) > 0;
|
||||
|
||||
if (LogType != IgnoranceLogType.Nothing && byteCount > 1200 && !flagsSet)
|
||||
Debug.LogWarning($"Warning: Server trying to send a Unreliable packet bigger than the recommended ENet 1200 byte MTU ({byteCount} > 1200). ENet will force Reliable Fragmented delivery.");
|
||||
|
||||
// Create the packet.
|
||||
serverOutgoingPacket.Create(segment.Array, byteOffset, byteCount + byteOffset, (PacketFlags)Channels[channelId]);
|
||||
|
||||
// Enqueue the packet.
|
||||
IgnoranceOutgoingPacket dispatchPacket = new IgnoranceOutgoingPacket
|
||||
{
|
||||
Channel = (byte)channelId,
|
||||
NativePeerId = (uint)connectionId - 1, // ENet's native peer ID will be ConnID - 1
|
||||
Payload = serverOutgoingPacket
|
||||
};
|
||||
|
||||
Server.Outgoing.Enqueue(dispatchPacket);
|
||||
|
||||
}
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (LogType != IgnoranceLogType.Nothing)
|
||||
Debug.Log("Ignorance Server Instance starting up...");
|
||||
|
||||
InitializeServerBackend();
|
||||
|
||||
Server.Start();
|
||||
}
|
||||
|
||||
public override void ServerStop()
|
||||
{
|
||||
if (Server != null)
|
||||
{
|
||||
if (LogType != IgnoranceLogType.Nothing)
|
||||
Debug.Log("Ignorance Server Instance shutting down...");
|
||||
|
||||
Server.Stop();
|
||||
}
|
||||
|
||||
ConnectionLookupDict.Clear();
|
||||
}
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = IgnoranceInternals.Scheme,
|
||||
Host = serverBindAddress,
|
||||
Port = port
|
||||
};
|
||||
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
// TODO: Nothing needed here?
|
||||
}
|
||||
|
||||
// Check to ensure channels 0 and 1 mimic LLAPI. Override this at your own risk.
|
||||
private void OnValidate()
|
||||
{
|
||||
if (Channels != null && Channels.Length >= 2)
|
||||
{
|
||||
// Check to make sure that Channel 0 and 1 are correct.
|
||||
if (Channels[0] != IgnoranceChannelTypes.Reliable)
|
||||
{
|
||||
Debug.LogWarning("Please do not modify Ignorance Channel 0. The channel will be reset to Reliable delivery. If you need a channel with a different delivery, define and use it instead.");
|
||||
Channels[0] = IgnoranceChannelTypes.Reliable;
|
||||
}
|
||||
if (Channels[1] != IgnoranceChannelTypes.Unreliable)
|
||||
{
|
||||
Debug.LogWarning("Please do not modify Ignorance Channel 1. The channel will be reset to Unreliable delivery. If you need a channel with a different delivery, define and use it instead.");
|
||||
Channels[1] = IgnoranceChannelTypes.Unreliable;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Invalid Channels setting, fixing. If you've just added Ignorance to your NetworkManager GameObject, seeing this message is normal.");
|
||||
Channels = new IgnoranceChannelTypes[2]
|
||||
{
|
||||
|
||||
IgnoranceChannelTypes.Reliable,
|
||||
IgnoranceChannelTypes.Unreliable
|
||||
};
|
||||
}
|
||||
|
||||
// ENet only supports a maximum of 32MB packet size.
|
||||
if (MaxAllowedPacketSize > 33554432)
|
||||
MaxAllowedPacketSize = 33554432;
|
||||
}
|
||||
|
||||
private void InitializeServerBackend()
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
Debug.LogWarning("IgnoranceServer reference for Server mode was null. This shouldn't happen, but to be safe we'll reinitialize it.");
|
||||
Server = new IgnoranceServer();
|
||||
}
|
||||
|
||||
// Set up the new IgnoranceServer reference.
|
||||
if (serverBindsAll)
|
||||
// MacOS is special. It's also a massive thorn in my backside.
|
||||
Server.BindAddress = IgnoranceInternals.BindAllMacs;
|
||||
else
|
||||
// Use the supplied bind address.
|
||||
Server.BindAddress = serverBindAddress;
|
||||
|
||||
// Sets port, maximum peers, max channels, the server poll time, maximum packet size and verbosity.
|
||||
Server.BindPort = port;
|
||||
Server.MaximumPeers = serverMaxPeerCapacity;
|
||||
Server.MaximumChannels = Channels.Length;
|
||||
Server.PollTime = serverMaxNativeWaitTime;
|
||||
Server.MaximumPacketSize = MaxAllowedPacketSize;
|
||||
Server.Verbosity = (int)LogType;
|
||||
|
||||
// Initializes the packet buffer.
|
||||
// Allocates once, that's it.
|
||||
if (InternalPacketBuffer == null)
|
||||
InternalPacketBuffer = new byte[PacketBufferCapacity];
|
||||
}
|
||||
|
||||
private void InitializeClientBackend()
|
||||
{
|
||||
if (Client == null)
|
||||
{
|
||||
Debug.LogWarning("Ignorance: IgnoranceClient reference for Client mode was null. This shouldn't happen, but to be safe we'll reinitialize it.");
|
||||
Client = new IgnoranceClient();
|
||||
}
|
||||
|
||||
// Sets address, port, channels to expect, verbosity, the server poll time and maximum packet size.
|
||||
Client.ConnectAddress = cachedConnectionAddress;
|
||||
Client.ConnectPort = port;
|
||||
Client.ExpectedChannels = Channels.Length;
|
||||
Client.PollTime = clientMaxNativeWaitTime;
|
||||
Client.MaximumPacketSize = MaxAllowedPacketSize;
|
||||
Client.Verbosity = (int)LogType;
|
||||
|
||||
// Initializes the packet buffer.
|
||||
// Allocates once, that's it.
|
||||
if (InternalPacketBuffer == null)
|
||||
InternalPacketBuffer = new byte[PacketBufferCapacity];
|
||||
}
|
||||
|
||||
private void ProcessServerPackets()
|
||||
{
|
||||
IgnoranceIncomingPacket incomingPacket;
|
||||
IgnoranceConnectionEvent connectionEvent;
|
||||
int adjustedConnectionId;
|
||||
Packet payload;
|
||||
|
||||
// Incoming connection events.
|
||||
while (Server.ConnectionEvents.TryDequeue(out connectionEvent))
|
||||
{
|
||||
adjustedConnectionId = (int)connectionEvent.NativePeerId + 1;
|
||||
|
||||
if (LogType == IgnoranceLogType.Verbose)
|
||||
Debug.Log($"Processing a server connection event from ENet native peer {connectionEvent.NativePeerId}. This peer would be Mirror ConnID {adjustedConnectionId}.");
|
||||
|
||||
// TODO: Investigate ArgumentException: An item with the same key has already been added. Key: <id>
|
||||
ConnectionLookupDict.Add(adjustedConnectionId, new PeerConnectionData
|
||||
{
|
||||
NativePeerId = connectionEvent.NativePeerId,
|
||||
IP = connectionEvent.IP,
|
||||
Port = connectionEvent.Port
|
||||
});
|
||||
|
||||
OnServerConnected?.Invoke(adjustedConnectionId);
|
||||
}
|
||||
|
||||
// Handle incoming data packets.
|
||||
// Console.WriteLine($"Server Incoming Queue is {Server.Incoming.Count}");
|
||||
while (Server.Incoming.TryDequeue(out incomingPacket))
|
||||
{
|
||||
adjustedConnectionId = (int)incomingPacket.NativePeerId + 1;
|
||||
payload = incomingPacket.Payload;
|
||||
|
||||
int length = payload.Length;
|
||||
ArraySegment<byte> dataSegment;
|
||||
|
||||
// Copy to working buffer and dispose of it.
|
||||
if (length > InternalPacketBuffer.Length)
|
||||
{
|
||||
byte[] oneFreshNTastyGcAlloc = new byte[length];
|
||||
|
||||
payload.CopyTo(oneFreshNTastyGcAlloc);
|
||||
dataSegment = new ArraySegment<byte>(oneFreshNTastyGcAlloc, 0, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
payload.CopyTo(InternalPacketBuffer);
|
||||
dataSegment = new ArraySegment<byte>(InternalPacketBuffer, 0, length);
|
||||
}
|
||||
|
||||
payload.Dispose();
|
||||
|
||||
OnServerDataReceived?.Invoke(adjustedConnectionId, dataSegment, incomingPacket.Channel);
|
||||
|
||||
// Some messages can disable the transport
|
||||
// If the transport was disabled by any of the messages, we have to break out of the loop and wait until we've been re-enabled.
|
||||
if (!enabled)
|
||||
break;
|
||||
}
|
||||
|
||||
// Disconnection events.
|
||||
while (Server.DisconnectionEvents.TryDequeue(out IgnoranceConnectionEvent disconnectionEvent))
|
||||
{
|
||||
adjustedConnectionId = (int)disconnectionEvent.NativePeerId + 1;
|
||||
|
||||
if (LogType == IgnoranceLogType.Verbose)
|
||||
Debug.Log($"ProcessServerPackets fired; handling disconnection event from native peer {disconnectionEvent.NativePeerId}.");
|
||||
|
||||
ConnectionLookupDict.Remove(adjustedConnectionId);
|
||||
|
||||
// Invoke Mirror handler.
|
||||
OnServerDisconnected?.Invoke(adjustedConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessClientPackets()
|
||||
{
|
||||
IgnoranceIncomingPacket incomingPacket;
|
||||
IgnoranceCommandPacket commandPacket;
|
||||
IgnoranceClientStats clientStats;
|
||||
Packet payload;
|
||||
IgnoranceConnectionEvent connectionEvent;
|
||||
|
||||
// Handle connection events.
|
||||
while (Client.ConnectionEvents.TryDequeue(out connectionEvent))
|
||||
{
|
||||
if (LogType == IgnoranceLogType.Verbose)
|
||||
Debug.Log($"ProcessClientConnectionEvents fired: processing a client ConnectionEvents queue item.");
|
||||
|
||||
if (connectionEvent.WasDisconnect)
|
||||
{
|
||||
// Disconnected from server.
|
||||
ClientState = ConnectionState.Disconnected;
|
||||
|
||||
if (LogType != IgnoranceLogType.Nothing)
|
||||
Debug.Log($"Ignorance Client has been disconnected from server.");
|
||||
|
||||
ignoreDataPackets = true;
|
||||
OnClientDisconnected?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Connected to server.
|
||||
ClientState = ConnectionState.Connected;
|
||||
|
||||
if (LogType != IgnoranceLogType.Nothing)
|
||||
Debug.Log($"Ignorance Client successfully connected to server at address {connectionEvent.IP}:{connectionEvent.Port}");
|
||||
|
||||
ignoreDataPackets = false;
|
||||
OnClientConnected?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
// Now handle the incoming messages.
|
||||
while (Client.Incoming.TryDequeue(out incomingPacket))
|
||||
{
|
||||
// Temporary fix: if ENet thread is too fast for Mirror, then ignore the packet.
|
||||
// This is seen sometimes if you stop the client and there's still stuff in the queue.
|
||||
if (ignoreDataPackets)
|
||||
{
|
||||
if (LogType == IgnoranceLogType.Verbose)
|
||||
Debug.Log("ProcessClientPackets cycle skipped; ignoring data packet");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise client recieved data, advise Mirror.
|
||||
// print($"Byte array: {incomingPacket.RentedByteArray.Length}. Packet Length: {incomingPacket.Length}");
|
||||
payload = incomingPacket.Payload;
|
||||
int length = payload.Length;
|
||||
ArraySegment<byte> dataSegment;
|
||||
|
||||
// Copy to working buffer and dispose of it.
|
||||
if (length > InternalPacketBuffer.Length)
|
||||
{
|
||||
// Unity's favourite: A fresh 'n' tasty GC Allocation!
|
||||
byte[] oneFreshNTastyGcAlloc = new byte[length];
|
||||
|
||||
payload.CopyTo(oneFreshNTastyGcAlloc);
|
||||
dataSegment = new ArraySegment<byte>(oneFreshNTastyGcAlloc, 0, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
payload.CopyTo(InternalPacketBuffer);
|
||||
dataSegment = new ArraySegment<byte>(InternalPacketBuffer, 0, length);
|
||||
}
|
||||
|
||||
payload.Dispose();
|
||||
|
||||
OnClientDataReceived?.Invoke(dataSegment, incomingPacket.Channel);
|
||||
|
||||
// Some messages can disable the transport
|
||||
// If the transport was disabled by any of the messages, we have to break out of the loop and wait until we've been re-enabled.
|
||||
if (!enabled)
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 3: Handle other commands.
|
||||
while (Client.Commands.TryDequeue(out commandPacket))
|
||||
{
|
||||
switch (commandPacket.Type)
|
||||
{
|
||||
// ...
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Handle status updates.
|
||||
if (Client.StatusUpdates.TryDequeue(out clientStats))
|
||||
{
|
||||
ClientStatistics = clientStats;
|
||||
}
|
||||
}
|
||||
|
||||
#region Main Thread Processing and Polling
|
||||
// Ignorance 1.4.0b5: To use Mirror's polling or not use Mirror's polling, that is up to the developer to decide
|
||||
#if !IGNORANCE_MIRROR_POLLING
|
||||
// IMPORTANT: Set Ignorance' execution order before everything else. Yes, that's -32000 !!
|
||||
// This ensures it has priority over other things.
|
||||
|
||||
// FixedUpdate can be called many times per frame.
|
||||
// Once we've handled stuff, we set a flag so that we don't poll again for this frame.
|
||||
|
||||
private bool fixedUpdateCompletedWork;
|
||||
public void FixedUpdate()
|
||||
{
|
||||
if (!enabled) return;
|
||||
if (fixedUpdateCompletedWork) return;
|
||||
|
||||
ProcessAndExecuteAllPackets();
|
||||
|
||||
// Flip the bool to signal we've done our work.
|
||||
fixedUpdateCompletedWork = true;
|
||||
}
|
||||
|
||||
// Normally, Mirror blocks Update() due to poor design decisions...
|
||||
// But thanks to Vincenzo, we've found a way to bypass that block.
|
||||
// Update is called once per frame. We don't have to worry about this shit now.
|
||||
public new void Update()
|
||||
{
|
||||
if (!enabled) return;
|
||||
|
||||
// Process what FixedUpdate missed, only if the boolean is not set.
|
||||
if (!fixedUpdateCompletedWork)
|
||||
ProcessAndExecuteAllPackets();
|
||||
|
||||
// Flip back the bool, so it can be reset.
|
||||
fixedUpdateCompletedWork = false;
|
||||
}
|
||||
|
||||
// Processes and Executes All Packets.
|
||||
private void ProcessAndExecuteAllPackets()
|
||||
{
|
||||
// Process Server Events...
|
||||
if (Server.IsAlive)
|
||||
ProcessServerPackets();
|
||||
|
||||
// Process Client Events...
|
||||
if (Client.IsAlive)
|
||||
{
|
||||
ProcessClientPackets();
|
||||
|
||||
if (ClientState == ConnectionState.Connected && clientStatusUpdateInterval > -1)
|
||||
{
|
||||
statusUpdateTimer += Time.deltaTime;
|
||||
|
||||
if (statusUpdateTimer >= clientStatusUpdateInterval)
|
||||
{
|
||||
Client.Commands.Enqueue(new IgnoranceCommandPacket { Type = IgnoranceCommandType.ClientRequestsStatusUpdate });
|
||||
statusUpdateTimer = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
// This section will be compiled in instead if you enable IGNORANCE_MIRROR_POLLING.
|
||||
|
||||
public override void ServerEarlyUpdate() {
|
||||
// This is used by Mirror to consume the incoming server packets.
|
||||
if (!enabled) return;
|
||||
|
||||
// Process Server Events...
|
||||
if (Server.IsAlive)
|
||||
ProcessServerPackets();
|
||||
}
|
||||
|
||||
public override void ClientEarlyUpdate() {
|
||||
// This is used by Mirror to consume the incoming client packets.
|
||||
if(!enabled) return;
|
||||
|
||||
if(Client.IsAlive)
|
||||
{
|
||||
ProcessClientPackets();
|
||||
|
||||
if (ClientState == ConnectionState.Connected && clientStatusUpdateInterval > -1)
|
||||
{
|
||||
statusUpdateTimer += Time.deltaTime;
|
||||
|
||||
if (statusUpdateTimer >= clientStatusUpdateInterval)
|
||||
{
|
||||
Client.Commands.Enqueue(new IgnoranceCommandPacket { Type = IgnoranceCommandType.ClientRequestsStatusUpdate });
|
||||
statusUpdateTimer = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
public override void ClientLateUpdate() {
|
||||
// This is used by Mirror to send out the outgoing client packets.
|
||||
if (!enabled) return;
|
||||
}
|
||||
|
||||
public override void ServerLateUpdate() {
|
||||
// This is used by Mirror to send out the outgoing server packets.
|
||||
if (!enabled) return;
|
||||
}
|
||||
*/
|
||||
#endif
|
||||
#endregion
|
||||
|
||||
#region Debug
|
||||
private void OnGUI()
|
||||
{
|
||||
if (DebugDisplay)
|
||||
GUI.Box(new Rect(
|
||||
new Vector2(32, Screen.height - 240), new Vector2(200, 160)),
|
||||
|
||||
"-- CLIENT --\n" +
|
||||
$"State: {ClientState}\n" +
|
||||
$"Incoming Queue: {Client.Incoming.Count}\n" +
|
||||
$"Outgoing Queue: {Client.Outgoing.Count}\n\n" +
|
||||
|
||||
"-- SERVER --\n" +
|
||||
$"Incoming Queue: {Server.Incoming.Count}\n" +
|
||||
$"Outgoing Queue: {Server.Outgoing.Count}\n" +
|
||||
$"ConnEvent Queue: {Server.ConnectionEvents.Count}"
|
||||
);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = 0) => MaxAllowedPacketSize;
|
||||
|
||||
// UDP Recommended Max MTU = 1200.
|
||||
public override int GetMaxBatchSize(int channelId) {
|
||||
bool isFragmentedAlready = ((PacketFlags)Channels[channelId] & ReliableOrUnreliableFragmented) > 0;
|
||||
return isFragmentedAlready ? MaxAllowedPacketSize : 1200;
|
||||
}
|
||||
|
||||
#region Internals
|
||||
private bool ignoreDataPackets;
|
||||
private string cachedConnectionAddress = string.Empty;
|
||||
private IgnoranceServer Server = new IgnoranceServer();
|
||||
private IgnoranceClient Client = new IgnoranceClient();
|
||||
private Dictionary<int, PeerConnectionData> ConnectionLookupDict = new Dictionary<int, PeerConnectionData>();
|
||||
|
||||
private enum ConnectionState { Connecting, Connected, Disconnecting, Disconnected }
|
||||
private ConnectionState ClientState = ConnectionState.Disconnected;
|
||||
private byte[] InternalPacketBuffer;
|
||||
|
||||
private const PacketFlags ReliableOrUnreliableFragmented = PacketFlags.Reliable | PacketFlags.UnreliableFragmented;
|
||||
|
||||
private float statusUpdateTimer = 0f;
|
||||
#endregion
|
||||
|
||||
#region Legacy Overrides
|
||||
#if !MIRROR_37_0_OR_NEWER
|
||||
public bool ServerDisconnectLegacy(int connectionId)
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
Debug.LogError("Cannot enqueue kick packet; our Server object is null. Something has gone wrong.");
|
||||
// Return here because otherwise we will get a NRE when trying to enqueue the kick packet.
|
||||
return false;
|
||||
}
|
||||
|
||||
IgnoranceCommandPacket kickPacket = new IgnoranceCommandPacket
|
||||
{
|
||||
Type = IgnoranceCommandType.ServerKickPeer,
|
||||
PeerId = (uint)connectionId - 1 // ENet's native peer ID will be ConnID - 1
|
||||
};
|
||||
|
||||
// Pass the packet onto the thread for dispatch.
|
||||
Server.Commands.Enqueue(kickPacket);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endregion
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/Ignorance/Ignorance.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/Ignorance/Ignorance.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 872fa23ef6e77334ca452ce16f6cd091
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: -32000
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using ENet;
|
||||
|
||||
namespace IgnoranceTransport
|
||||
{
|
||||
// Snipped from the transport files, as this will help
|
||||
// me keep things up to date.
|
||||
[Serializable]
|
||||
public enum IgnoranceChannelTypes
|
||||
{
|
||||
Reliable = PacketFlags.Reliable, // TCP Emulation.
|
||||
ReliableUnsequenced = PacketFlags.Reliable | PacketFlags.Unsequenced, // TCP Emulation, but no sequencing.
|
||||
Unreliable = PacketFlags.Unsequenced, // Pure UDP.
|
||||
UnreliableFragmented = PacketFlags.UnreliableFragmented, // Pure UDP, but fragmented.
|
||||
UnreliableSequenced = PacketFlags.None, // Pure UDP, but sequenced.
|
||||
Unthrottled = PacketFlags.Unthrottled, // Apparently ENet's version of Taco Bell.
|
||||
}
|
||||
|
||||
public class IgnoranceInternals
|
||||
{
|
||||
public const string Version = "1.4.0b6";
|
||||
public const string Scheme = "enet";
|
||||
public const string BindAllIPv4 = "0.0.0.0";
|
||||
public const string BindAllMacs = "::0";
|
||||
}
|
||||
|
||||
public enum IgnoranceLogType
|
||||
{
|
||||
Nothing,
|
||||
Standard,
|
||||
Verbose
|
||||
}
|
||||
|
||||
// Struct optimized for cache efficiency. (Thanks Vincenzo!)
|
||||
public struct IgnoranceIncomingPacket
|
||||
{
|
||||
public byte Channel;
|
||||
public uint NativePeerId;
|
||||
public Packet Payload;
|
||||
}
|
||||
|
||||
// Struct optimized for cache efficiency. (Thanks Vincenzo!)
|
||||
public struct IgnoranceOutgoingPacket
|
||||
{
|
||||
public byte Channel;
|
||||
public uint NativePeerId;
|
||||
public Packet Payload;
|
||||
}
|
||||
|
||||
// Struct optimized for cache efficiency. (Thanks Vincenzo!)
|
||||
public struct IgnoranceConnectionEvent
|
||||
{
|
||||
public bool WasDisconnect;
|
||||
public ushort Port;
|
||||
public uint NativePeerId;
|
||||
public string IP;
|
||||
}
|
||||
|
||||
public struct IgnoranceCommandPacket
|
||||
{
|
||||
public IgnoranceCommandType Type;
|
||||
public uint PeerId;
|
||||
}
|
||||
|
||||
public struct IgnoranceClientStats
|
||||
{
|
||||
// Stats only - may not always be used!
|
||||
public uint RTT;
|
||||
public ulong BytesReceived;
|
||||
public ulong BytesSent;
|
||||
public ulong PacketsReceived;
|
||||
public ulong PacketsSent;
|
||||
public ulong PacketsLost;
|
||||
}
|
||||
|
||||
public enum IgnoranceCommandType
|
||||
{
|
||||
// Client
|
||||
ClientWantsToStop,
|
||||
ClientRequestsStatusUpdate,
|
||||
// ENet internal
|
||||
ResponseToClientStatusRequest,
|
||||
// Server
|
||||
ServerKickPeer
|
||||
}
|
||||
|
||||
// TODO: Optimize struct for Cache performance.
|
||||
public struct PeerConnectionData
|
||||
{
|
||||
public ushort Port;
|
||||
public uint NativePeerId;
|
||||
public string IP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b2d1f7f7d9d3d64297755419f6ab925
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/Ignorance/Plugins.meta
Normal file
8
Assets/Mirror/Runtime/Transport/Ignorance/Plugins.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8bd04b9965420e40ac911fbf4294e1c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c90c88052305a054d87177362f63dea8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4a42b5855436864693138f74335c094
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,111 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b3abd2268dea254da11190b00d16051
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 0
|
||||
Exclude Editor: 1
|
||||
Exclude Linux: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude LinuxUniversal: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude WebGL: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 1
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: ARM64
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Facebook: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Facebook: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Linux
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: LinuxUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
WebGL: WebGL
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfbaf4fbcf8987e4585ca666e28f4871
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,106 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3ce4b8600e09d74ead46137ba184d01
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 0
|
||||
Exclude Editor: 1
|
||||
Exclude Linux: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude LinuxUniversal: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude WebGL: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 1
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Facebook: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Facebook: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Linux
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: x86
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: LinuxUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3486c84aaebff4489e2e11f5bd3030f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba78b07719f786c4cae4c52b9528903f
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 0
|
||||
Exclude Editor: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 1
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e3ca9b41c9f4064581a6b56ae549a9c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,97 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2bd341c8082ddd47b4b5cc9bfdf2332
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
'': Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 1
|
||||
Exclude Editor: 0
|
||||
Exclude Linux: 1
|
||||
Exclude Linux64: 0
|
||||
Exclude LinuxUniversal: 0
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude WebGL: 1
|
||||
Exclude Win: 0
|
||||
Exclude Win64: 0
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86_64
|
||||
DefaultValueInitialized: true
|
||||
OS: Linux
|
||||
- first:
|
||||
Facebook: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Facebook: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Linux
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86_64
|
||||
- first:
|
||||
Standalone: LinuxUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86_64
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8837cb1a7320b8b4f824a02e23f4a545
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
This is a Windows x64 build of ENet-CSharp.
|
||||
|
||||
If you require a version of ENet for 32 Bit computer systems (ie. Windows 7/8/10 32Bit) then please get in touch, or you can install the requirements yourself and compile it using ENet-CSharp's MSBuild-based build system. Get in touch if you want me to compile them for you, but keep in mind that I do not support them when reporting bugs.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09ee288deb259474c819a5e3daf54b80
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,111 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc98033c6bc234a419ccd053e7173781
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
'': Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 1
|
||||
Exclude Editor: 0
|
||||
Exclude Linux: 0
|
||||
Exclude Linux64: 0
|
||||
Exclude LinuxUniversal: 0
|
||||
Exclude OSXUniversal: 0
|
||||
Exclude WebGL: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 0
|
||||
Exclude iOS: 1
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Facebook: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Facebook: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Linux
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: x86_64
|
||||
- first:
|
||||
Standalone: LinuxUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
WebGL: WebGL
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7edc275f3670f4251b90301cbb9f7413
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1485de7d76884a64bac00df01454081e
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 1
|
||||
Exclude Editor: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 0
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: ARM64
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa239cb4c47fc9447816815f80667fea
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 1
|
||||
Exclude Editor: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 0
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: ARMv7
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 385bb48fb124b5f40a79bdecf421671f
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
: Any
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
Exclude Android: 1
|
||||
Exclude Editor: 1
|
||||
Exclude Linux64: 1
|
||||
Exclude OSXUniversal: 1
|
||||
Exclude Win: 1
|
||||
Exclude Win64: 1
|
||||
Exclude iOS: 0
|
||||
- first:
|
||||
Android: Android
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: ARMv7
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
DefaultValueInitialized: true
|
||||
OS: AnyOS
|
||||
- first:
|
||||
Standalone: Linux64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: None
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: X64
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 957ba76da095cd64aaa658f034ab78e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5317859893ad2cf48a6df1d585ebdd2c
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 1
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
35
Assets/Mirror/Runtime/Transport/Ignorance/Plugins/readme.txt
Normal file
35
Assets/Mirror/Runtime/Transport/Ignorance/Plugins/readme.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
ENET Pre-compiled Binary Library Blobs
|
||||
==========================
|
||||
This folder contains pre-compiled binaries for a variety of different platforms.
|
||||
|
||||
A brief summary of these folders are as follows:
|
||||
|
||||
- Windows, Mac, Linux
|
||||
-- 64bit (x64)
|
||||
|
||||
- Android (Kitkat 4.4 minimum target OS)
|
||||
-- ARMv7 (armeabi-v7a), ARMv8/AArch64 (arm64-v8a)
|
||||
|
||||
- iOS
|
||||
-- FAT Library (armv7 + arm64). Targeted for iOS 8 minimum. Unsigned library.
|
||||
|
||||
DEBUG VERSIONS
|
||||
===============
|
||||
Debug versions of the libraries can be obtained at https://github.com/SoftwareGuy/ENet-CSharp/releases.
|
||||
Otherwise you can also compile the library yourself with Debug enabled.
|
||||
|
||||
DOT POINTS
|
||||
===========
|
||||
1. 32bit Support for Ignorance has been removed. Originally, I did not want to support 32bit operating systems,
|
||||
however due to some countries in the world still stuck in the 32bit era (Brasil, some Russian areas, etc) I added them as a
|
||||
goodwill gesture. However, since those who needed the libraries have now vanished, I have stopped building 32bit versions of ENet.
|
||||
|
||||
COMPILE THE CODE YOURSELF
|
||||
=========================
|
||||
If you don't trust the above binaries then git clone the ENET-CSharp repository (http://github.com/SoftwareGuy/ENet-CSharp) and read the readme.
|
||||
|
||||
EXCLUSION INSTRUCTIONS
|
||||
======================
|
||||
No need, the meta data will cover that for you.
|
||||
|
||||
Still don't know what to do with these? Drop by the Mirror discord and post in the Ignorance channel.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a28193472bc84d341ab4aee18c471a93
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1
Assets/Mirror/Runtime/Transport/Ignorance/version.txt
Normal file
1
Assets/Mirror/Runtime/Transport/Ignorance/version.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.4.0b6
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad80075449f17c548877161f32a9841a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 953bb5ec5ab2346a092f58061e01ba65
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/MirrorTransport.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bdb797750d0a490684410110bf48192
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,342 @@
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class KcpTransport : Transport
|
||||
{
|
||||
// scheme used by this transport
|
||||
public const string Scheme = "kcp";
|
||||
|
||||
// common
|
||||
[Header("Transport Configuration")]
|
||||
public ushort Port = 7777;
|
||||
[Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")]
|
||||
public bool DualMode = true;
|
||||
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]
|
||||
public bool NoDelay = true;
|
||||
[Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")]
|
||||
public uint Interval = 10;
|
||||
[Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")]
|
||||
public int Timeout = 10000;
|
||||
|
||||
[Header("Advanced")]
|
||||
[Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")]
|
||||
public int FastResend = 2;
|
||||
[Tooltip("KCP congestion window. Enabled in normal mode, disabled in turbo mode. Disable this for high scale games if connections get choked regularly.")]
|
||||
public bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
|
||||
[Tooltip("KCP window size can be modified to support higher loads.")]
|
||||
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP window size can be modified to support higher loads.")]
|
||||
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("Enable to use where-allocation NonAlloc KcpServer/Client/Connection versions. Highly recommended on all Unity platforms.")]
|
||||
public bool NonAlloc = true;
|
||||
|
||||
// server & client (where-allocation NonAlloc versions)
|
||||
KcpServer server;
|
||||
KcpClient client;
|
||||
|
||||
// debugging
|
||||
[Header("Debug")]
|
||||
public bool debugLog;
|
||||
// show statistics in OnGUI
|
||||
public bool statisticsGUI;
|
||||
// log statistics for headless servers that can't show them in GUI
|
||||
public bool statisticsLog;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// logging
|
||||
// Log.Info should use Debug.Log if enabled, or nothing otherwise
|
||||
// (don't want to spam the console on headless servers)
|
||||
if (debugLog)
|
||||
Log.Info = Debug.Log;
|
||||
else
|
||||
Log.Info = _ => {};
|
||||
Log.Warning = Debug.LogWarning;
|
||||
Log.Error = Debug.LogError;
|
||||
|
||||
// client
|
||||
client = NonAlloc
|
||||
? new KcpClientNonAlloc(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
|
||||
() => OnClientDisconnected.Invoke())
|
||||
: new KcpClient(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message) => OnClientDataReceived.Invoke(message, Channels.Reliable),
|
||||
() => OnClientDisconnected.Invoke());
|
||||
|
||||
// server
|
||||
server = NonAlloc
|
||||
? new KcpServerNonAlloc(
|
||||
(connectionId) => OnServerConnected.Invoke(connectionId),
|
||||
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
DualMode,
|
||||
NoDelay,
|
||||
Interval,
|
||||
FastResend,
|
||||
CongestionWindow,
|
||||
SendWindowSize,
|
||||
ReceiveWindowSize,
|
||||
Timeout)
|
||||
: new KcpServer(
|
||||
(connectionId) => OnServerConnected.Invoke(connectionId),
|
||||
(connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
DualMode,
|
||||
NoDelay,
|
||||
Interval,
|
||||
FastResend,
|
||||
CongestionWindow,
|
||||
SendWindowSize,
|
||||
ReceiveWindowSize,
|
||||
Timeout);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Debug.Log("KcpTransport initialized!");
|
||||
}
|
||||
|
||||
// all except WebGL
|
||||
public override bool Available() =>
|
||||
Application.platform != RuntimePlatform.WebGLPlayer;
|
||||
|
||||
// client
|
||||
public override bool ClientConnected() => client.connected;
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
|
||||
}
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
// switch to kcp channel.
|
||||
// unreliable or reliable.
|
||||
// default to reliable just to be sure.
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Unreliable:
|
||||
client.Send(segment, KcpChannel.Unreliable);
|
||||
break;
|
||||
default:
|
||||
client.Send(segment, KcpChannel.Reliable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
public override void ClientDisconnect() => client.Disconnect();
|
||||
// process incoming in early update
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
// scene change messages disable transports to stop them from
|
||||
// processing while changing the scene.
|
||||
// -> we need to check enabled here
|
||||
// -> and in kcp's internal loops, see Awake() OnCheckEnabled setup!
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
if (enabled) client.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
public override void ClientLateUpdate() => client.TickOutgoing();
|
||||
|
||||
// scene change message will disable transports.
|
||||
// kcp processes messages in an internal loop which should be
|
||||
// stopped immediately after scene change (= after disabled)
|
||||
// => kcp has tests to guaranteed that calling .Pause() during the
|
||||
// receive loop stops the receive loop immediately, not after.
|
||||
void OnEnable()
|
||||
{
|
||||
// unpause when enabled again
|
||||
client?.Unpause();
|
||||
server?.Unpause();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// pause immediately when not enabled anymore
|
||||
client?.Pause();
|
||||
server?.Pause();
|
||||
}
|
||||
|
||||
// server
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
builder.Port = Port;
|
||||
return builder.Uri;
|
||||
}
|
||||
public override bool ServerActive() => server.IsActive();
|
||||
public override void ServerStart() => server.Start(Port);
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
// switch to kcp channel.
|
||||
// unreliable or reliable.
|
||||
// default to reliable just to be sure.
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Unreliable:
|
||||
server.Send(connectionId, segment, KcpChannel.Unreliable);
|
||||
break;
|
||||
default:
|
||||
server.Send(connectionId, segment, KcpChannel.Reliable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
|
||||
public override void ServerStop() => server.Stop();
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
// scene change messages disable transports to stop them from
|
||||
// processing while changing the scene.
|
||||
// -> we need to check enabled here
|
||||
// -> and in kcp's internal loops, see Awake() OnCheckEnabled setup!
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
if (enabled) server.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
public override void ServerLateUpdate() => server.TickOutgoing();
|
||||
|
||||
// common
|
||||
public override void Shutdown() {}
|
||||
|
||||
// max message size
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable)
|
||||
{
|
||||
// switch to kcp channel.
|
||||
// unreliable or reliable.
|
||||
// default to reliable just to be sure.
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Unreliable:
|
||||
return KcpConnection.UnreliableMaxMessageSize;
|
||||
default:
|
||||
return KcpConnection.ReliableMaxMessageSize;
|
||||
}
|
||||
}
|
||||
|
||||
// kcp reliable channel max packet size is MTU * WND_RCV
|
||||
// this allows 144kb messages. but due to head of line blocking, all
|
||||
// other messages would have to wait until the maxed size one is
|
||||
// delivered. batching 144kb messages each time would be EXTREMELY slow
|
||||
// and fill the send queue nearly immediately when using it over the
|
||||
// network.
|
||||
// => instead we always use MTU sized batches.
|
||||
// => people can still send maxed size if needed.
|
||||
public override int GetBatchThreshold(int channelId) =>
|
||||
KcpConnection.UnreliableMaxMessageSize;
|
||||
|
||||
// server statistics
|
||||
// LONG to avoid int overflows with connections.Sum.
|
||||
// see also: https://github.com/vis2k/Mirror/pull/2777
|
||||
public long GetAverageMaxSendRate() =>
|
||||
server.connections.Count > 0
|
||||
? server.connections.Values.Sum(conn => (long)conn.MaxSendRate) / server.connections.Count
|
||||
: 0;
|
||||
public long GetAverageMaxReceiveRate() =>
|
||||
server.connections.Count > 0
|
||||
? server.connections.Values.Sum(conn => (long)conn.MaxReceiveRate) / server.connections.Count
|
||||
: 0;
|
||||
long GetTotalSendQueue() =>
|
||||
server.connections.Values.Sum(conn => conn.SendQueueCount);
|
||||
long GetTotalReceiveQueue() =>
|
||||
server.connections.Values.Sum(conn => conn.ReceiveQueueCount);
|
||||
long GetTotalSendBuffer() =>
|
||||
server.connections.Values.Sum(conn => conn.SendBufferCount);
|
||||
long GetTotalReceiveBuffer() =>
|
||||
server.connections.Values.Sum(conn => conn.ReceiveBufferCount);
|
||||
|
||||
// PrettyBytes function from DOTSNET
|
||||
// pretty prints bytes as KB/MB/GB/etc.
|
||||
// long to support > 2GB
|
||||
// divides by floats to return "2.5MB" etc.
|
||||
public static string PrettyBytes(long bytes)
|
||||
{
|
||||
// bytes
|
||||
if (bytes < 1024)
|
||||
return $"{bytes} B";
|
||||
// kilobytes
|
||||
else if (bytes < 1024L * 1024L)
|
||||
return $"{(bytes / 1024f):F2} KB";
|
||||
// megabytes
|
||||
else if (bytes < 1024 * 1024L * 1024L)
|
||||
return $"{(bytes / (1024f * 1024f)):F2} MB";
|
||||
// gigabytes
|
||||
return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB";
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!statisticsGUI) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(5, 110, 300, 300));
|
||||
|
||||
if (ServerActive())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("SERVER");
|
||||
GUILayout.Label($" connections: {server.connections.Count}");
|
||||
GUILayout.Label($" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s");
|
||||
GUILayout.Label($" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s");
|
||||
GUILayout.Label($" SendQueue: {GetTotalSendQueue()}");
|
||||
GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}");
|
||||
GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}");
|
||||
GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("CLIENT");
|
||||
GUILayout.Label($" MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s");
|
||||
GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s");
|
||||
GUILayout.Label($" SendQueue: {client.connection.SendQueueCount}");
|
||||
GUILayout.Label($" ReceiveQueue: {client.connection.ReceiveQueueCount}");
|
||||
GUILayout.Label($" SendBuffer: {client.connection.SendBufferCount}");
|
||||
GUILayout.Label($" ReceiveBuffer: {client.connection.ReceiveBufferCount}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
void OnLogStatistics()
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" connections: {server.connections.Count}\n";
|
||||
log += $" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n";
|
||||
log += $" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n";
|
||||
log += $" SendQueue: {GetTotalSendQueue()}\n";
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Debug.Log(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s\n";
|
||||
log += $" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s\n";
|
||||
log += $" SendQueue: {client.connection.SendQueueCount}\n";
|
||||
log += $" ReceiveQueue: {client.connection.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.connection.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.connection.ReceiveBufferCount}\n\n";
|
||||
Debug.Log(log);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => "KCP";
|
||||
}
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b0fecffa3f624585964b0d0eb21b18e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a1c8e8c022d4731a481c1808f37e5d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal file
24
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 limpo1989
|
||||
Copyright (c) 2020 Paul Pacheco
|
||||
Copyright (c) 2020 Lymdun
|
||||
Copyright (c) 2020 vis2k
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/LICENSE.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a3e8369060cf4e94ac117603de47aa6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
94
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal file
94
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION
Normal file
@@ -0,0 +1,94 @@
|
||||
V1.12 [2021-07-16]
|
||||
- where-allocation removed. will be optional in the future.
|
||||
- Tests: don't depend on Unity anymore
|
||||
- fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls
|
||||
OnDisconnected to let the user now.
|
||||
- fix: KcpServer.DualMode is now configurable in the constructor instead of
|
||||
using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too.
|
||||
|
||||
V1.11 rollback [2021-06-01]
|
||||
- perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime
|
||||
resizing/allocations
|
||||
|
||||
V1.10 [2021-05-28]
|
||||
- feature: configurable Timeout
|
||||
- allocations explained with comments (C# ReceiveFrom / IPEndPoint.GetHashCode)
|
||||
- fix: #17 KcpConnection.ReceiveNextReliable now assigns message default so it
|
||||
works in .net too
|
||||
- fix: Segment pool is not static anymore. Each kcp instance now has it's own
|
||||
Pool<Segment>. fixes #18 concurrency issues
|
||||
|
||||
V1.9 [2021-03-02]
|
||||
- Tick() split into TickIncoming()/TickOutgoing() to use in Mirror's new update
|
||||
functions. allows to minimize latency.
|
||||
=> original Tick() is still supported for convenience. simply processes both!
|
||||
|
||||
V1.8 [2021-02-14]
|
||||
- fix: Unity IPv6 errors on Nintendo Switch
|
||||
- fix: KcpConnection now disconnects if data message was received without content.
|
||||
previously it would call OnData with an empty ArraySegment, causing all kinds of
|
||||
weird behaviour in Mirror/DOTSNET. Added tests too.
|
||||
- fix: KcpConnection.SendData: don't allow sending empty messages anymore. disconnect
|
||||
and log a warning to make it completely obvious.
|
||||
|
||||
V1.7 [2021-01-13]
|
||||
- fix: unreliable messages reset timeout now too
|
||||
- perf: KcpConnection OnCheckEnabled callback changed to a simple 'paused' boolean.
|
||||
This is faster than invoking a Func<bool> every time and allows us to fix #8 more
|
||||
easily later by calling .Pause/.Unpause from OnEnable/OnDisable in MirrorTransport.
|
||||
- fix #8: Unpause now resets timeout to fix a bug where Mirror would pause kcp,
|
||||
change the scene which took >10s, then unpause and kcp would detect the lack of
|
||||
any messages for >10s as timeout. Added test to make sure it never happens again.
|
||||
- MirrorTransport: statistics logging for headless servers
|
||||
- Mirror Transport: Send/Receive window size increased once more from 2048 to 4096.
|
||||
|
||||
V1.6 [2021-01-10]
|
||||
- Unreliable channel added!
|
||||
- perf: KcpHeader byte added to every kcp message to indicate
|
||||
Handshake/Data/Ping/Disconnect instead of scanning each message for Hello/Byte/Ping
|
||||
content via SegmentEquals. It's a lot cleaner, should be faster and should avoid
|
||||
edge cases where a message content would equal Hello/Ping/Bye sequence accidentally.
|
||||
- Kcp.Input: offset moved to parameters for cases where it's needed
|
||||
- Kcp.SetMtu from original Kcp.c
|
||||
|
||||
V1.5 [2021-01-07]
|
||||
- KcpConnection.MaxSend/ReceiveRate calculation based on the article
|
||||
- MirrorTransport: large send/recv window size defaults to avoid high latencies caused
|
||||
by packets not being processed fast enough
|
||||
- MirrorTransport: show MaxSend/ReceiveRate in debug gui
|
||||
- MirrorTransport: don't Log.Info to console in headless mode if debug log is disabled
|
||||
|
||||
V1.4 [2020-11-27]
|
||||
- fix: OnCheckEnabled added. KcpConnection message processing while loop can now
|
||||
be interrupted immediately. fixes Mirror Transport scene changes which need to stop
|
||||
processing any messages immediately after a scene message)
|
||||
- perf: Mirror KcpTransport: FastResend enabled by default. turbo mode according to:
|
||||
https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration
|
||||
- perf: Mirror KcpTransport: CongestionControl disabled by default (turbo mode)
|
||||
|
||||
V1.3 [2020-11-17]
|
||||
- Log.Info/Warning/Error so logging doesn't depend on UnityEngine anymore
|
||||
- fix: Server.Tick catches SocketException which happens if Android client is killed
|
||||
- MirrorTransport: debugLog option added that can be checked in Unity Inspector
|
||||
- Utils.Clamp so Kcp.cs doesn't depend on UnityEngine
|
||||
- Utils.SegmentsEqual: use Linq SequenceEqual so it doesn't depend on UnityEngine
|
||||
=> kcp2k can now be used in any C# project even without Unity
|
||||
|
||||
V1.2 [2020-11-10]
|
||||
- more tests added
|
||||
- fix: raw receive buffers are now all of MTU size
|
||||
- fix: raw receive detects error where buffer was too small for msgLength and
|
||||
result in excess data being dropped silently
|
||||
- KcpConnection.MaxMessageSize added for use in high level
|
||||
- KcpConnection.MaxMessageSize increased from 1200 bytes to to maximum allowed
|
||||
message size of 145KB for kcp (based on mtu, overhead, wnd_rcv)
|
||||
|
||||
V1.1 [2020-10-30]
|
||||
- high level cleanup, fixes, improvements
|
||||
|
||||
V1.0 [2020-10-22]
|
||||
- Kcp.cs now mirrors original Kcp.c behaviour
|
||||
(this fixes dozens of bugs)
|
||||
|
||||
V0.1
|
||||
- initial kcp-csharp based version
|
||||
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
7
Assets/Mirror/Runtime/Transport/KCP/kcp2k/VERSION.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed3f2cf1bbf1b4d53a6f2c103d311f71
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a54d18b954cb4407a28b633fc32ea6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
// channel type and header for raw messages
|
||||
public enum KcpChannel : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Reliable = 0x01,
|
||||
Unreliable = 0x02
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e852b2532fb248d19715cfebe371db3
|
||||
timeCreated: 1610081248
|
||||
120
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
Normal file
120
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpClient.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
// kcp client logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClient
|
||||
{
|
||||
// events
|
||||
public Action OnConnected;
|
||||
public Action<ArraySegment<byte>> OnData;
|
||||
public Action OnDisconnected;
|
||||
|
||||
// state
|
||||
public KcpClientConnection connection;
|
||||
public bool connected;
|
||||
|
||||
public KcpClient(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
|
||||
{
|
||||
this.OnConnected = OnConnected;
|
||||
this.OnData = OnData;
|
||||
this.OnDisconnected = OnDisconnected;
|
||||
}
|
||||
|
||||
// CreateConnection can be overwritten for where-allocation:
|
||||
// https://github.com/vis2k/where-allocation
|
||||
protected virtual KcpClientConnection CreateConnection() =>
|
||||
new KcpClientConnection();
|
||||
|
||||
public void Connect(string address, ushort port, bool noDelay, uint interval, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = KcpConnection.DEFAULT_TIMEOUT)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
Log.Warning("KCP: client already connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
// create connection
|
||||
connection = CreateConnection();
|
||||
|
||||
// setup events
|
||||
connection.OnAuthenticated = () =>
|
||||
{
|
||||
Log.Info($"KCP: OnClientConnected");
|
||||
connected = true;
|
||||
OnConnected.Invoke();
|
||||
};
|
||||
connection.OnData = (message) =>
|
||||
{
|
||||
//Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})");
|
||||
OnData.Invoke(message);
|
||||
};
|
||||
connection.OnDisconnected = () =>
|
||||
{
|
||||
Log.Info($"KCP: OnClientDisconnected");
|
||||
connected = false;
|
||||
connection = null;
|
||||
OnDisconnected.Invoke();
|
||||
};
|
||||
|
||||
// connect
|
||||
connection.Connect(address, port, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
connection.SendData(segment, channel);
|
||||
}
|
||||
else Log.Warning("KCP: can't send because client not connected!");
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
// only if connected
|
||||
// otherwise we end up in a deadlock because of an open Mirror bug:
|
||||
// https://github.com/vis2k/Mirror/issues/2353
|
||||
if (connected)
|
||||
{
|
||||
// call Disconnect and let the connection handle it.
|
||||
// DO NOT set it to null yet. it needs to be updated a few more
|
||||
// times first. let the connection handle it!
|
||||
connection?.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
public void TickIncoming()
|
||||
{
|
||||
// recv on socket first, then process incoming
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
// (connection is null if not active)
|
||||
connection?.RawReceive();
|
||||
connection?.TickIncoming();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
public void TickOutgoing()
|
||||
{
|
||||
// process outgoing
|
||||
// (connection is null if not active)
|
||||
connection?.TickOutgoing();
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
|
||||
// pause/unpause to safely support mirror scene handling and to
|
||||
// immediately pause the receive while loop if needed.
|
||||
public void Pause() => connection?.Pause();
|
||||
public void Unpause() => connection?.Unpause();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa069a28ed24fedb533c102d9742b36
|
||||
timeCreated: 1603786960
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClientConnection : KcpConnection
|
||||
{
|
||||
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
|
||||
// if MaxMessageSize is larger. kcp always sends in MTU
|
||||
// segments and having a buffer smaller than MTU would
|
||||
// silently drop excess data.
|
||||
// => we need the MTU to fit channel + message!
|
||||
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
|
||||
|
||||
// helper function to resolve host to IPAddress
|
||||
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
addresses = Dns.GetHostAddresses(hostname);
|
||||
return addresses.Length >= 1;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
Log.Info($"Failed to resolve host: {hostname}");
|
||||
addresses = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// EndPoint & Receive functions can be overwritten for where-allocation:
|
||||
// https://github.com/vis2k/where-allocation
|
||||
// NOTE: Client's SendTo doesn't allocate, don't need a virtual.
|
||||
protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) =>
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], port);
|
||||
|
||||
protected virtual int ReceiveFrom(byte[] buffer) =>
|
||||
socket.ReceiveFrom(buffer, ref remoteEndPoint);
|
||||
|
||||
public void Connect(string host, ushort port, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
|
||||
{
|
||||
Log.Info($"KcpClient: connect to {host}:{port}");
|
||||
|
||||
// try resolve host name
|
||||
if (ResolveHostname(host, out IPAddress[] addresses))
|
||||
{
|
||||
// create remote endpoint
|
||||
CreateRemoteEndPoint(addresses, port);
|
||||
|
||||
// create socket
|
||||
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Connect(remoteEndPoint);
|
||||
|
||||
// set up kcp
|
||||
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
|
||||
|
||||
// client should send handshake to server as very first message
|
||||
SendHandshake();
|
||||
|
||||
RawReceive();
|
||||
}
|
||||
// otherwise call OnDisconnected to let the user know.
|
||||
else OnDisconnected();
|
||||
}
|
||||
|
||||
|
||||
// call from transport update
|
||||
public void RawReceive()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (socket != null)
|
||||
{
|
||||
while (socket.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
int msgLength = ReceiveFrom(rawReceiveBuffer);
|
||||
// IMPORTANT: detect if buffer was too small for the
|
||||
// received msgLength. otherwise the excess
|
||||
// data would be silently lost.
|
||||
// (see ReceiveFrom documentation)
|
||||
if (msgLength <= rawReceiveBuffer.Length)
|
||||
{
|
||||
//Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
|
||||
RawInput(rawReceiveBuffer, msgLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting.");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// this is fine, the socket might have been closed in the other end
|
||||
catch (SocketException) {}
|
||||
}
|
||||
|
||||
protected override void Dispose()
|
||||
{
|
||||
socket.Close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
protected override void RawSend(byte[] data, int length)
|
||||
{
|
||||
socket.Send(data, length, SocketFlags.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96512e74aa8214a6faa8a412a7a07877
|
||||
timeCreated: 1602601237
|
||||
@@ -0,0 +1,674 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
enum KcpState { Connected, Authenticated, Disconnected }
|
||||
|
||||
public abstract class KcpConnection
|
||||
{
|
||||
protected Socket socket;
|
||||
protected EndPoint remoteEndPoint;
|
||||
internal Kcp kcp;
|
||||
|
||||
// kcp can have several different states, let's use a state machine
|
||||
KcpState state = KcpState.Disconnected;
|
||||
|
||||
public Action OnAuthenticated;
|
||||
public Action<ArraySegment<byte>> OnData;
|
||||
public Action OnDisconnected;
|
||||
|
||||
// Mirror needs a way to stop the kcp message processing while loop
|
||||
// immediately after a scene change message. Mirror can't process any
|
||||
// other messages during a scene change.
|
||||
// (could be useful for others too)
|
||||
bool paused;
|
||||
|
||||
// If we don't receive anything these many milliseconds
|
||||
// then consider us disconnected
|
||||
public const int DEFAULT_TIMEOUT = 10000;
|
||||
public int timeout = DEFAULT_TIMEOUT;
|
||||
uint lastReceiveTime;
|
||||
|
||||
// internal time.
|
||||
// StopWatch offers ElapsedMilliSeconds and should be more precise than
|
||||
// Unity's time.deltaTime over long periods.
|
||||
readonly Stopwatch refTime = new Stopwatch();
|
||||
|
||||
// we need to subtract the channel byte from every MaxMessageSize
|
||||
// calculation.
|
||||
// we also need to tell kcp to use MTU-1 to leave space for the byte.
|
||||
const int CHANNEL_HEADER_SIZE = 1;
|
||||
|
||||
// reliable channel (= kcp) MaxMessageSize so the outside knows largest
|
||||
// allowed message to send the calculation in Send() is not obvious at
|
||||
// all, so let's provide the helper here.
|
||||
//
|
||||
// kcp does fragmentation, so max message is way larger than MTU.
|
||||
//
|
||||
// -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD
|
||||
// -> Send() checks if fragment count < WND_RCV, so we use WND_RCV - 1.
|
||||
// note that Send() checks WND_RCV instead of wnd_rcv which may or
|
||||
// may not be a bug in original kcp. but since it uses the define, we
|
||||
// can use that here too.
|
||||
// -> we add 1 byte KcpHeader enum to each message, so -1
|
||||
//
|
||||
// IMPORTANT: max message is MTU * WND_RCV, in other words it completely
|
||||
// fills the receive window! due to head of line blocking,
|
||||
// all other messages have to wait while a maxed size message
|
||||
// is being delivered.
|
||||
// => in other words, DO NOT use max size all the time like
|
||||
// for batching.
|
||||
// => sending UNRELIABLE max message size most of the time is
|
||||
// best for performance (use that one for batching!)
|
||||
public const int ReliableMaxMessageSize = (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * (Kcp.WND_RCV - 1) - 1;
|
||||
|
||||
// unreliable max message size is simply MTU - channel header size
|
||||
public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE;
|
||||
|
||||
// buffer to receive kcp's processed messages (avoids allocations).
|
||||
// IMPORTANT: this is for KCP messages. so it needs to be of size:
|
||||
// 1 byte header + MaxMessageSize content
|
||||
byte[] kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize];
|
||||
|
||||
// send buffer for handing user messages to kcp for processing.
|
||||
// (avoids allocations).
|
||||
// IMPORTANT: needs to be of size:
|
||||
// 1 byte header + MaxMessageSize content
|
||||
byte[] kcpSendBuffer = new byte[1 + ReliableMaxMessageSize];
|
||||
|
||||
// raw send buffer is exactly MTU.
|
||||
byte[] rawSendBuffer = new byte[Kcp.MTU_DEF];
|
||||
|
||||
// send a ping occasionally so we don't time out on the other end.
|
||||
// for example, creating a character in an MMO could easily take a
|
||||
// minute of no data being sent. which doesn't mean we want to time out.
|
||||
// same goes for slow paced card games etc.
|
||||
public const int PING_INTERVAL = 1000;
|
||||
uint lastPingTime;
|
||||
|
||||
// if we send more than kcp can handle, we will get ever growing
|
||||
// send/recv buffers and queues and minutes of latency.
|
||||
// => if a connection can't keep up, it should be disconnected instead
|
||||
// to protect the server under heavy load, and because there is no
|
||||
// point in growing to gigabytes of memory or minutes of latency!
|
||||
// => 2k isn't enough. we reach 2k when spawning 4k monsters at once
|
||||
// easily, but it does recover over time.
|
||||
// => 10k seems safe.
|
||||
//
|
||||
// note: we have a ChokeConnectionAutoDisconnects test for this too!
|
||||
internal const int QueueDisconnectThreshold = 10000;
|
||||
|
||||
// getters for queue and buffer counts, used for debug info
|
||||
public int SendQueueCount => kcp.snd_queue.Count;
|
||||
public int ReceiveQueueCount => kcp.rcv_queue.Count;
|
||||
public int SendBufferCount => kcp.snd_buf.Count;
|
||||
public int ReceiveBufferCount => kcp.rcv_buf.Count;
|
||||
|
||||
// maximum send rate per second can be calculated from kcp parameters
|
||||
// source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html
|
||||
//
|
||||
// KCP can send/receive a maximum of WND*MTU per interval.
|
||||
// multiple by 1000ms / interval to get the per-second rate.
|
||||
//
|
||||
// example:
|
||||
// WND(32) * MTU(1400) = 43.75KB
|
||||
// => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s
|
||||
//
|
||||
// returns bytes/second!
|
||||
public uint MaxSendRate =>
|
||||
kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval;
|
||||
|
||||
public uint MaxReceiveRate =>
|
||||
kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval;
|
||||
|
||||
// SetupKcp creates and configures a new KCP instance.
|
||||
// => useful to start from a fresh state every time the client connects
|
||||
// => NoDelay, interval, wnd size are the most important configurations.
|
||||
// let's force require the parameters so we don't forget it anywhere.
|
||||
protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
|
||||
{
|
||||
// set up kcp over reliable channel (that's what kcp is for)
|
||||
kcp = new Kcp(0, RawSendReliable);
|
||||
// set nodelay.
|
||||
// note that kcp uses 'nocwnd' internally so we negate the parameter
|
||||
kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow);
|
||||
kcp.SetWindowSize(sendWindowSize, receiveWindowSize);
|
||||
|
||||
// IMPORTANT: high level needs to add 1 channel byte to each raw
|
||||
// message. so while Kcp.MTU_DEF is perfect, we actually need to
|
||||
// tell kcp to use MTU-1 so we can still put the header into the
|
||||
// message afterwards.
|
||||
kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE);
|
||||
|
||||
this.timeout = timeout;
|
||||
state = KcpState.Connected;
|
||||
|
||||
refTime.Start();
|
||||
}
|
||||
|
||||
void HandleTimeout(uint time)
|
||||
{
|
||||
// note: we are also sending a ping regularly, so timeout should
|
||||
// only ever happen if the connection is truly gone.
|
||||
if (time >= lastReceiveTime + timeout)
|
||||
{
|
||||
Log.Warning($"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting.");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleDeadLink()
|
||||
{
|
||||
// kcp has 'dead_link' detection. might as well use it.
|
||||
if (kcp.state == -1)
|
||||
{
|
||||
Log.Warning("KCP Connection dead_link detected. Disconnecting.");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// send a ping occasionally in order to not time out on the other end.
|
||||
void HandlePing(uint time)
|
||||
{
|
||||
// enough time elapsed since last ping?
|
||||
if (time >= lastPingTime + PING_INTERVAL)
|
||||
{
|
||||
// ping again and reset time
|
||||
//Log.Debug("KCP: sending ping...");
|
||||
SendPing();
|
||||
lastPingTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleChoked()
|
||||
{
|
||||
// disconnect connections that can't process the load.
|
||||
// see QueueSizeDisconnect comments.
|
||||
// => include all of kcp's buffers and the unreliable queue!
|
||||
int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
|
||||
kcp.rcv_buf.Count + kcp.snd_buf.Count;
|
||||
if (total >= QueueDisconnectThreshold)
|
||||
{
|
||||
Log.Warning($"KCP: disconnecting connection because it can't process data fast enough.\n" +
|
||||
$"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
|
||||
$"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
|
||||
$"* Or perhaps the network is simply too slow on our end, or on the other end.\n");
|
||||
|
||||
// let's clear all pending sends before disconnting with 'Bye'.
|
||||
// otherwise a single Flush in Disconnect() won't be enough to
|
||||
// flush thousands of messages to finally deliver 'Bye'.
|
||||
// this is just faster and more robust.
|
||||
kcp.snd_queue.Clear();
|
||||
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// reads the next reliable message type & content from kcp.
|
||||
// -> to avoid buffering, unreliable messages call OnData directly.
|
||||
bool ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message)
|
||||
{
|
||||
int msgSize = kcp.PeekSize();
|
||||
if (msgSize > 0)
|
||||
{
|
||||
// only allow receiving up to buffer sized messages.
|
||||
// otherwise we would get BlockCopy ArgumentException anyway.
|
||||
if (msgSize <= kcpMessageBuffer.Length)
|
||||
{
|
||||
// receive from kcp
|
||||
int received = kcp.Receive(kcpMessageBuffer, msgSize);
|
||||
if (received >= 0)
|
||||
{
|
||||
// extract header & content without header
|
||||
header = (KcpHeader)kcpMessageBuffer[0];
|
||||
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
|
||||
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if receive failed, close everything
|
||||
Log.Warning($"Receive failed with error={received}. closing connection.");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
// we don't allow sending messages > Max, so this must be an
|
||||
// attacker. let's disconnect to avoid allocation attacks etc.
|
||||
else
|
||||
{
|
||||
Log.Warning($"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
message = default;
|
||||
header = KcpHeader.Disconnect;
|
||||
return false;
|
||||
}
|
||||
|
||||
void TickIncoming_Connected(uint time)
|
||||
{
|
||||
// detect common events & ping
|
||||
HandleTimeout(time);
|
||||
HandleDeadLink();
|
||||
HandlePing(time);
|
||||
HandleChoked();
|
||||
|
||||
// any reliable kcp message received?
|
||||
if (ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
|
||||
{
|
||||
// message type FSM. no default so we never miss a case.
|
||||
switch (header)
|
||||
{
|
||||
case KcpHeader.Handshake:
|
||||
{
|
||||
// we were waiting for a handshake.
|
||||
// it proves that the other end speaks our protocol.
|
||||
Log.Info("KCP: received handshake");
|
||||
state = KcpState.Authenticated;
|
||||
OnAuthenticated?.Invoke();
|
||||
break;
|
||||
}
|
||||
case KcpHeader.Ping:
|
||||
{
|
||||
// ping keeps kcp from timing out. do nothing.
|
||||
break;
|
||||
}
|
||||
case KcpHeader.Data:
|
||||
case KcpHeader.Disconnect:
|
||||
{
|
||||
// everything else is not allowed during handshake!
|
||||
Log.Warning($"KCP: received invalid header {header} while Connected. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TickIncoming_Authenticated(uint time)
|
||||
{
|
||||
// detect common events & ping
|
||||
HandleTimeout(time);
|
||||
HandleDeadLink();
|
||||
HandlePing(time);
|
||||
HandleChoked();
|
||||
|
||||
// process all received messages
|
||||
//
|
||||
// Mirror scene changing requires transports to immediately stop
|
||||
// processing any more messages after a scene message was
|
||||
// received. and since we are in a while loop here, we need this
|
||||
// extra check.
|
||||
//
|
||||
// note while that this is mainly for Mirror, but might be
|
||||
// useful in other applications too.
|
||||
//
|
||||
// note that we check it BEFORE ever calling ReceiveNext. otherwise
|
||||
// we would silently eat the received message and never process it.
|
||||
while (!paused &&
|
||||
ReceiveNextReliable(out KcpHeader header, out ArraySegment<byte> message))
|
||||
{
|
||||
// message type FSM. no default so we never miss a case.
|
||||
switch (header)
|
||||
{
|
||||
case KcpHeader.Handshake:
|
||||
{
|
||||
// should never receive another handshake after auth
|
||||
Log.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
case KcpHeader.Data:
|
||||
{
|
||||
// call OnData IF the message contained actual data
|
||||
if (message.Count > 0)
|
||||
{
|
||||
//Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}");
|
||||
OnData?.Invoke(message);
|
||||
}
|
||||
// empty data = attacker, or something went wrong
|
||||
else
|
||||
{
|
||||
Log.Warning("KCP: received empty Data message while Authenticated. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KcpHeader.Ping:
|
||||
{
|
||||
// ping keeps kcp from timing out. do nothing.
|
||||
break;
|
||||
}
|
||||
case KcpHeader.Disconnect:
|
||||
{
|
||||
// disconnect might happen
|
||||
Log.Info("KCP: received disconnect message");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TickIncoming()
|
||||
{
|
||||
uint time = (uint)refTime.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case KcpState.Connected:
|
||||
{
|
||||
TickIncoming_Connected(time);
|
||||
break;
|
||||
}
|
||||
case KcpState.Authenticated:
|
||||
{
|
||||
TickIncoming_Authenticated(time);
|
||||
break;
|
||||
}
|
||||
case KcpState.Disconnected:
|
||||
{
|
||||
// do nothing while disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// this is ok, the connection was closed
|
||||
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (ObjectDisposedException exception)
|
||||
{
|
||||
// fine, socket was closed
|
||||
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// unexpected
|
||||
Log.Error(ex.ToString());
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void TickOutgoing()
|
||||
{
|
||||
uint time = (uint)refTime.ElapsedMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case KcpState.Connected:
|
||||
case KcpState.Authenticated:
|
||||
{
|
||||
// update flushes out messages
|
||||
kcp.Update(time);
|
||||
break;
|
||||
}
|
||||
case KcpState.Disconnected:
|
||||
{
|
||||
// do nothing while disconnected
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// this is ok, the connection was closed
|
||||
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (ObjectDisposedException exception)
|
||||
{
|
||||
// fine, socket was closed
|
||||
Log.Info($"KCP Connection: Disconnecting because {exception}. This is fine.");
|
||||
Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// unexpected
|
||||
Log.Error(ex.ToString());
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void RawInput(byte[] buffer, int msgLength)
|
||||
{
|
||||
// parse channel
|
||||
if (msgLength > 0)
|
||||
{
|
||||
byte channel = buffer[0];
|
||||
switch (channel)
|
||||
{
|
||||
case (byte)KcpChannel.Reliable:
|
||||
{
|
||||
// input into kcp, but skip channel byte
|
||||
int input = kcp.Input(buffer, 1, msgLength - 1);
|
||||
if (input != 0)
|
||||
{
|
||||
Log.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case (byte)KcpChannel.Unreliable:
|
||||
{
|
||||
// ideally we would queue all unreliable messages and
|
||||
// then process them in ReceiveNext() together with the
|
||||
// reliable messages, but:
|
||||
// -> queues/allocations/pools are slow and complex.
|
||||
// -> DOTSNET 10k is actually slower if we use pooled
|
||||
// unreliable messages for transform messages.
|
||||
//
|
||||
// DOTSNET 10k benchmark:
|
||||
// reliable-only: 170 FPS
|
||||
// unreliable queued: 130-150 FPS
|
||||
// unreliable direct: 183 FPS(!)
|
||||
//
|
||||
// DOTSNET 50k benchmark:
|
||||
// reliable-only: FAILS (queues keep growing)
|
||||
// unreliable direct: 18-22 FPS(!)
|
||||
//
|
||||
// -> all unreliable messages are DATA messages anyway.
|
||||
// -> let's skip the magic and call OnData directly if
|
||||
// the current state allows it.
|
||||
if (state == KcpState.Authenticated)
|
||||
{
|
||||
// only process messages while not paused for Mirror
|
||||
// scene switching etc.
|
||||
// -> if an unreliable message comes in while
|
||||
// paused, simply drop it. it's unreliable!
|
||||
if (!paused)
|
||||
{
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(buffer, 1, msgLength - 1);
|
||||
OnData?.Invoke(message);
|
||||
}
|
||||
|
||||
// set last receive time to avoid timeout.
|
||||
// -> we do this in ANY case even if not enabled.
|
||||
// a message is a message.
|
||||
// -> we set last receive time for both reliable and
|
||||
// unreliable messages. both count.
|
||||
// otherwise a connection might time out even
|
||||
// though unreliable were received, but no
|
||||
// reliable was received.
|
||||
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// should never
|
||||
Log.Warning($"KCP: received unreliable message in state {state}. Disconnecting the connection.");
|
||||
Disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// not a valid channel. random data or attacks.
|
||||
Log.Info($"Disconnecting connection because of invalid channel header: {channel}");
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// raw send puts the data into the socket
|
||||
protected abstract void RawSend(byte[] data, int length);
|
||||
|
||||
// raw send called by kcp
|
||||
void RawSendReliable(byte[] data, int length)
|
||||
{
|
||||
// copy channel header, data into raw send buffer, then send
|
||||
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
|
||||
Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length);
|
||||
RawSend(rawSendBuffer, length + 1);
|
||||
}
|
||||
|
||||
void SendReliable(KcpHeader header, ArraySegment<byte> content)
|
||||
{
|
||||
// 1 byte header + content needs to fit into send buffer
|
||||
if (1 + content.Count <= kcpSendBuffer.Length) // TODO
|
||||
{
|
||||
// copy header, content (if any) into send buffer
|
||||
kcpSendBuffer[0] = (byte)header;
|
||||
if (content.Count > 0)
|
||||
Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count);
|
||||
|
||||
// send to kcp for processing
|
||||
int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count);
|
||||
if (sent < 0)
|
||||
{
|
||||
Log.Warning($"Send failed with error={sent} for content with length={content.Count}");
|
||||
}
|
||||
}
|
||||
// otherwise content is larger than MaxMessageSize. let user know!
|
||||
else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize}");
|
||||
}
|
||||
|
||||
void SendUnreliable(ArraySegment<byte> message)
|
||||
{
|
||||
// message size needs to be <= unreliable max size
|
||||
if (message.Count <= UnreliableMaxMessageSize)
|
||||
{
|
||||
// copy channel header, data into raw send buffer, then send
|
||||
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
|
||||
Buffer.BlockCopy(message.Array, 0, rawSendBuffer, 1, message.Count);
|
||||
RawSend(rawSendBuffer, message.Count + 1);
|
||||
}
|
||||
// otherwise content is larger than MaxMessageSize. let user know!
|
||||
else Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}");
|
||||
}
|
||||
|
||||
// server & client need to send handshake at different times, so we need
|
||||
// to expose the function.
|
||||
// * client should send it immediately.
|
||||
// * server should send it as reply to client's handshake, not before
|
||||
// (server should not reply to random internet messages with handshake)
|
||||
// => handshake info needs to be delivered, so it goes over reliable.
|
||||
public void SendHandshake()
|
||||
{
|
||||
Log.Info("KcpConnection: sending Handshake to other end!");
|
||||
SendReliable(KcpHeader.Handshake, default);
|
||||
}
|
||||
|
||||
public void SendData(ArraySegment<byte> data, KcpChannel channel)
|
||||
{
|
||||
// sending empty segments is not allowed.
|
||||
// nobody should ever try to send empty data.
|
||||
// it means that something went wrong, e.g. in Mirror/DOTSNET.
|
||||
// let's make it obvious so it's easy to debug.
|
||||
if (data.Count == 0)
|
||||
{
|
||||
Log.Warning("KcpConnection: tried sending empty message. This should never happen. Disconnecting.");
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case KcpChannel.Reliable:
|
||||
SendReliable(KcpHeader.Data, data);
|
||||
break;
|
||||
case KcpChannel.Unreliable:
|
||||
SendUnreliable(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ping goes through kcp to keep it from timing out, so it goes over the
|
||||
// reliable channel.
|
||||
void SendPing() => SendReliable(KcpHeader.Ping, default);
|
||||
|
||||
// disconnect info needs to be delivered, so it goes over reliable
|
||||
void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default);
|
||||
|
||||
protected virtual void Dispose() {}
|
||||
|
||||
// disconnect this connection
|
||||
public void Disconnect()
|
||||
{
|
||||
// only if not disconnected yet
|
||||
if (state == KcpState.Disconnected)
|
||||
return;
|
||||
|
||||
// send a disconnect message
|
||||
if (socket.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendDisconnect();
|
||||
kcp.Flush();
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// this is ok, the connection was already closed
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// this is normal when we stop the server
|
||||
// the socket is stopped so we can't send anything anymore
|
||||
// to the clients
|
||||
|
||||
// the clients will eventually timeout and realize they
|
||||
// were disconnected
|
||||
}
|
||||
}
|
||||
|
||||
// set as Disconnected, call event
|
||||
Log.Info("KCP Connection: Disconnected.");
|
||||
state = KcpState.Disconnected;
|
||||
OnDisconnected?.Invoke();
|
||||
}
|
||||
|
||||
// get remote endpoint
|
||||
public EndPoint GetRemoteEndPoint() => remoteEndPoint;
|
||||
|
||||
// pause/unpause to safely support mirror scene handling and to
|
||||
// immediately pause the receive while loop if needed.
|
||||
public void Pause() => paused = true;
|
||||
public void Unpause()
|
||||
{
|
||||
// unpause
|
||||
paused = false;
|
||||
|
||||
// reset the timeout.
|
||||
// we have likely been paused for > timeout seconds, but that
|
||||
// doesn't mean we should disconnect. for example, Mirror pauses
|
||||
// kcp during scene changes which could easily take > 10s timeout:
|
||||
// see also: https://github.com/vis2k/kcp2k/issues/8
|
||||
// => Unpause completely resets the timeout instead of restoring the
|
||||
// time difference when we started pausing. it's more simple and
|
||||
// it's a good idea to start counting from 0 after we unpaused!
|
||||
lastReceiveTime = (uint)refTime.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
|
||||
timeCreated: 1602600432
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace kcp2k
|
||||
{
|
||||
// header for messages processed by kcp.
|
||||
// this is NOT for the raw receive messages(!) because handshake/disconnect
|
||||
// need to be sent reliably. it's not enough to have those in rawreceive
|
||||
// because those messages might get lost without being resent!
|
||||
public enum KcpHeader : byte
|
||||
{
|
||||
// don't react on 0x00. might help to filter out random noise.
|
||||
Handshake = 0x01,
|
||||
// ping goes over reliable & KcpHeader for now. could go over reliable
|
||||
// too. there is no real difference except that this is easier because
|
||||
// we already have a KcpHeader for reliable messages.
|
||||
// ping is only used to keep it alive, so latency doesn't matter.
|
||||
Ping = 0x02,
|
||||
Data = 0x03,
|
||||
Disconnect = 0x04
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91b5edac31224a49bd76f960ae018942
|
||||
timeCreated: 1610081248
|
||||
337
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
337
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/KcpServer.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
// kcp server logic abstracted into a class.
|
||||
// for use in Mirror, DOTSNET, testing, etc.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServer
|
||||
{
|
||||
// events
|
||||
public Action<int> OnConnected;
|
||||
public Action<int, ArraySegment<byte>> OnData;
|
||||
public Action<int> OnDisconnected;
|
||||
|
||||
// configuration
|
||||
// DualMode uses both IPv6 and IPv4. not all platforms support it.
|
||||
// (Nintendo Switch, etc.)
|
||||
public bool DualMode;
|
||||
// NoDelay is recommended to reduce latency. This also scales better
|
||||
// without buffers getting full.
|
||||
public bool NoDelay;
|
||||
// KCP internal update interval. 100ms is KCP default, but a lower
|
||||
// interval is recommended to minimize latency and to scale to more
|
||||
// networked entities.
|
||||
public uint Interval;
|
||||
// KCP fastresend parameter. Faster resend for the cost of higher
|
||||
// bandwidth.
|
||||
public int FastResend;
|
||||
// KCP 'NoCongestionWindow' is false by default. here we negate it for
|
||||
// ease of use. This can be disabled for high scale games if connections
|
||||
// choke regularly.
|
||||
public bool CongestionWindow;
|
||||
// KCP window size can be modified to support higher loads.
|
||||
// for example, Mirror Benchmark requires:
|
||||
// 128, 128 for 4k monsters
|
||||
// 512, 512 for 10k monsters
|
||||
// 8192, 8192 for 20k monsters
|
||||
public uint SendWindowSize;
|
||||
public uint ReceiveWindowSize;
|
||||
// timeout in milliseconds
|
||||
public int Timeout;
|
||||
|
||||
// state
|
||||
protected Socket socket;
|
||||
EndPoint newClientEP;
|
||||
|
||||
// IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even
|
||||
// if MaxMessageSize is larger. kcp always sends in MTU
|
||||
// segments and having a buffer smaller than MTU would
|
||||
// silently drop excess data.
|
||||
// => we need the mtu to fit channel + message!
|
||||
readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF];
|
||||
|
||||
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
|
||||
public Dictionary<int, KcpServerConnection> connections = new Dictionary<int, KcpServerConnection>();
|
||||
|
||||
public KcpServer(Action<int> OnConnected,
|
||||
Action<int, ArraySegment<byte>> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
bool DualMode,
|
||||
bool NoDelay,
|
||||
uint Interval,
|
||||
int FastResend = 0,
|
||||
bool CongestionWindow = true,
|
||||
uint SendWindowSize = Kcp.WND_SND,
|
||||
uint ReceiveWindowSize = Kcp.WND_RCV,
|
||||
int Timeout = KcpConnection.DEFAULT_TIMEOUT)
|
||||
{
|
||||
this.OnConnected = OnConnected;
|
||||
this.OnData = OnData;
|
||||
this.OnDisconnected = OnDisconnected;
|
||||
this.DualMode = DualMode;
|
||||
this.NoDelay = NoDelay;
|
||||
this.Interval = Interval;
|
||||
this.FastResend = FastResend;
|
||||
this.CongestionWindow = CongestionWindow;
|
||||
this.SendWindowSize = SendWindowSize;
|
||||
this.ReceiveWindowSize = ReceiveWindowSize;
|
||||
this.Timeout = Timeout;
|
||||
|
||||
// create newClientEP either IPv4 or IPv6
|
||||
newClientEP = DualMode
|
||||
? new IPEndPoint(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPoint(IPAddress.Any, 0);
|
||||
}
|
||||
|
||||
public bool IsActive() => socket != null;
|
||||
|
||||
public void Start(ushort port)
|
||||
{
|
||||
// only start once
|
||||
if (socket != null)
|
||||
{
|
||||
Log.Warning("KCP: server already started!");
|
||||
}
|
||||
|
||||
// listen
|
||||
if (DualMode)
|
||||
{
|
||||
// IPv6 socket with DualMode
|
||||
socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.DualMode = true;
|
||||
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
|
||||
}
|
||||
else
|
||||
{
|
||||
// IPv4 socket
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, port));
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.SendData(segment, channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
connection.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetClientAddress(int connectionId)
|
||||
{
|
||||
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
return (connection.GetRemoteEndPoint() as IPEndPoint).Address.ToString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// EndPoint & Receive functions can be overwritten for where-allocation:
|
||||
// https://github.com/vis2k/where-allocation
|
||||
protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash)
|
||||
{
|
||||
// NOTE: ReceiveFrom allocates.
|
||||
// we pass our IPEndPoint to ReceiveFrom.
|
||||
// receive from calls newClientEP.Create(socketAddr).
|
||||
// IPEndPoint.Create always returns a new IPEndPoint.
|
||||
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
|
||||
int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP);
|
||||
|
||||
// calculate connectionHash from endpoint
|
||||
// NOTE: IPEndPoint.GetHashCode() allocates.
|
||||
// it calls m_Address.GetHashCode().
|
||||
// m_Address is an IPAddress.
|
||||
// GetHashCode() allocates for IPv6:
|
||||
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
|
||||
//
|
||||
// => using only newClientEP.Port wouldn't work, because
|
||||
// different connections can have the same port.
|
||||
connectionHash = newClientEP.GetHashCode();
|
||||
return read;
|
||||
}
|
||||
|
||||
protected virtual KcpServerConnection CreateConnection() =>
|
||||
new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
|
||||
|
||||
// process incoming messages. should be called before updating the world.
|
||||
HashSet<int> connectionsToRemove = new HashSet<int>();
|
||||
public void TickIncoming()
|
||||
{
|
||||
while (socket != null && socket.Poll(0, SelectMode.SelectRead))
|
||||
{
|
||||
try
|
||||
{
|
||||
// receive
|
||||
int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId);
|
||||
//Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
|
||||
|
||||
// IMPORTANT: detect if buffer was too small for the received
|
||||
// msgLength. otherwise the excess data would be
|
||||
// silently lost.
|
||||
// (see ReceiveFrom documentation)
|
||||
if (msgLength <= rawReceiveBuffer.Length)
|
||||
{
|
||||
// is this a new connection?
|
||||
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
|
||||
{
|
||||
// create a new KcpConnection based on last received
|
||||
// EndPoint. can be overwritten for where-allocation.
|
||||
connection = CreateConnection();
|
||||
|
||||
// DO NOT add to connections yet. only if the first message
|
||||
// is actually the kcp handshake. otherwise it's either:
|
||||
// * random data from the internet
|
||||
// * or from a client connection that we just disconnected
|
||||
// but that hasn't realized it yet, still sending data
|
||||
// from last session that we should absolutely ignore.
|
||||
//
|
||||
//
|
||||
// TODO this allocates a new KcpConnection for each new
|
||||
// internet connection. not ideal, but C# UDP Receive
|
||||
// already allocated anyway.
|
||||
//
|
||||
// expecting a MAGIC byte[] would work, but sending the raw
|
||||
// UDP message without kcp's reliability will have low
|
||||
// probability of being received.
|
||||
//
|
||||
// for now, this is fine.
|
||||
|
||||
// setup authenticated event that also adds to connections
|
||||
connection.OnAuthenticated = () =>
|
||||
{
|
||||
// only send handshake to client AFTER we received his
|
||||
// handshake in OnAuthenticated.
|
||||
// we don't want to reply to random internet messages
|
||||
// with handshakes each time.
|
||||
connection.SendHandshake();
|
||||
|
||||
// add to connections dict after being authenticated.
|
||||
connections.Add(connectionId, connection);
|
||||
Log.Info($"KCP: server added connection({connectionId})");
|
||||
|
||||
// setup Data + Disconnected events only AFTER the
|
||||
// handshake. we don't want to fire OnServerDisconnected
|
||||
// every time we receive invalid random data from the
|
||||
// internet.
|
||||
|
||||
// setup data event
|
||||
connection.OnData = (message) =>
|
||||
{
|
||||
// call mirror event
|
||||
//Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})");
|
||||
OnData.Invoke(connectionId, message);
|
||||
};
|
||||
|
||||
// setup disconnected event
|
||||
connection.OnDisconnected = () =>
|
||||
{
|
||||
// flag for removal
|
||||
// (can't remove directly because connection is updated
|
||||
// and event is called while iterating all connections)
|
||||
connectionsToRemove.Add(connectionId);
|
||||
|
||||
// call mirror event
|
||||
Log.Info($"KCP: OnServerDisconnected({connectionId})");
|
||||
OnDisconnected.Invoke(connectionId);
|
||||
};
|
||||
|
||||
// finally, call mirror OnConnected event
|
||||
Log.Info($"KCP: OnServerConnected({connectionId})");
|
||||
OnConnected.Invoke(connectionId);
|
||||
};
|
||||
|
||||
// now input the message & process received ones
|
||||
// connected event was set up.
|
||||
// tick will process the first message and adds the
|
||||
// connection if it was the handshake.
|
||||
connection.RawInput(rawReceiveBuffer, msgLength);
|
||||
connection.TickIncoming();
|
||||
|
||||
// again, do not add to connections.
|
||||
// if the first message wasn't the kcp handshake then
|
||||
// connection will simply be garbage collected.
|
||||
}
|
||||
// existing connection: simply input the message into kcp
|
||||
else
|
||||
{
|
||||
connection.RawInput(rawReceiveBuffer, msgLength);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}.");
|
||||
Disconnect(connectionId);
|
||||
}
|
||||
}
|
||||
// this is fine, the socket might have been closed in the other end
|
||||
catch (SocketException) {}
|
||||
}
|
||||
|
||||
// process inputs for all server connections
|
||||
// (even if we didn't receive anything. need to tick ping etc.)
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickIncoming();
|
||||
}
|
||||
|
||||
// remove disconnected connections
|
||||
// (can't do it in connection.OnDisconnected because Tick is called
|
||||
// while iterating connections)
|
||||
foreach (int connectionId in connectionsToRemove)
|
||||
{
|
||||
connections.Remove(connectionId);
|
||||
}
|
||||
connectionsToRemove.Clear();
|
||||
}
|
||||
|
||||
// process outgoing messages. should be called after updating the world.
|
||||
public void TickOutgoing()
|
||||
{
|
||||
// flush all server connections
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
{
|
||||
connection.TickOutgoing();
|
||||
}
|
||||
}
|
||||
|
||||
// process incoming and outgoing for convenience.
|
||||
// => ideally call ProcessIncoming() before updating the world and
|
||||
// ProcessOutgoing() after updating the world for minimum latency
|
||||
public void Tick()
|
||||
{
|
||||
TickIncoming();
|
||||
TickOutgoing();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
socket?.Close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
// pause/unpause to safely support mirror scene handling and to
|
||||
// immediately pause the receive while loop if needed.
|
||||
public void Pause()
|
||||
{
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
connection.Pause();
|
||||
}
|
||||
|
||||
public void Unpause()
|
||||
{
|
||||
foreach (KcpServerConnection connection in connections.Values)
|
||||
connection.Unpause();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9759159c6589494a9037f5e130a867ed
|
||||
timeCreated: 1603787747
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerConnection : KcpConnection
|
||||
{
|
||||
// Constructor & Send functions can be overwritten for where-allocation:
|
||||
// https://github.com/vis2k/where-allocation
|
||||
public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
|
||||
{
|
||||
this.socket = socket;
|
||||
this.remoteEndPoint = remoteEndPoint;
|
||||
SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout);
|
||||
}
|
||||
|
||||
protected override void RawSend(byte[] data, int length)
|
||||
{
|
||||
socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
|
||||
timeCreated: 1602601483
|
||||
14
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
Normal file
14
Assets/Mirror/Runtime/Transport/KCP/kcp2k/highlevel/Log.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// A simple logger class that uses Console.WriteLine by default.
|
||||
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
|
||||
// (this way we don't have to depend on UnityEngine)
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
public static Action<string> Info = Console.WriteLine;
|
||||
public static Action<string> Warning = Console.WriteLine;
|
||||
public static Action<string> Error = Console.Error.WriteLine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b320ff06046474eae7bce7240ea478c
|
||||
timeCreated: 1626430641
|
||||
@@ -0,0 +1,24 @@
|
||||
// where-allocation version of KcpClientConnection.
|
||||
// may not be wanted on all platforms, so it's an extra optional class.
|
||||
using System.Net;
|
||||
using WhereAllocation;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClientConnectionNonAlloc : KcpClientConnection
|
||||
{
|
||||
IPEndPointNonAlloc reusableEP;
|
||||
|
||||
protected override void CreateRemoteEndPoint(IPAddress[] addresses, ushort port)
|
||||
{
|
||||
// create reusableEP with same address family as remoteEndPoint.
|
||||
// otherwise ReceiveFrom_NonAlloc couldn't use it.
|
||||
reusableEP = new IPEndPointNonAlloc(addresses[0], port);
|
||||
base.CreateRemoteEndPoint(addresses, port);
|
||||
}
|
||||
|
||||
// where-allocation nonalloc recv
|
||||
protected override int ReceiveFrom(byte[] buffer) =>
|
||||
socket.ReceiveFrom_NonAlloc(buffer, reusableEP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c1b235bbe054706bef6d092f361006e
|
||||
timeCreated: 1626430539
|
||||
@@ -0,0 +1,17 @@
|
||||
// where-allocation version of KcpClientConnectionNonAlloc.
|
||||
// may not be wanted on all platforms, so it's an extra optional class.
|
||||
using System;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpClientNonAlloc : KcpClient
|
||||
{
|
||||
public KcpClientNonAlloc(Action OnConnected, Action<ArraySegment<byte>> OnData, Action OnDisconnected)
|
||||
: base(OnConnected, OnData, OnDisconnected)
|
||||
{
|
||||
}
|
||||
|
||||
protected override KcpClientConnection CreateConnection() =>
|
||||
new KcpClientConnectionNonAlloc();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cf0ccf7d551480bb5af08fcbe169f84
|
||||
timeCreated: 1626435264
|
||||
@@ -0,0 +1,25 @@
|
||||
// where-allocation version of KcpServerConnection.
|
||||
// may not be wanted on all platforms, so it's an extra optional class.
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using WhereAllocation;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerConnectionNonAlloc : KcpServerConnection
|
||||
{
|
||||
IPEndPointNonAlloc reusableSendEndPoint;
|
||||
|
||||
public KcpServerConnectionNonAlloc(Socket socket, EndPoint remoteEndpoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT)
|
||||
: base(socket, remoteEndpoint, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout)
|
||||
{
|
||||
this.reusableSendEndPoint = reusableSendEndPoint;
|
||||
}
|
||||
|
||||
protected override void RawSend(byte[] data, int length)
|
||||
{
|
||||
// where-allocation nonalloc send
|
||||
socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e1b74cc224b4c83a0f6c8d8da9090ab
|
||||
timeCreated: 1626430608
|
||||
@@ -0,0 +1,51 @@
|
||||
// where-allocation version of KcpServer.
|
||||
// may not be wanted on all platforms, so it's an extra optional class.
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using WhereAllocation;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class KcpServerNonAlloc : KcpServer
|
||||
{
|
||||
IPEndPointNonAlloc reusableClientEP;
|
||||
|
||||
public KcpServerNonAlloc(Action<int> OnConnected, Action<int, ArraySegment<byte>> OnData, Action<int> OnDisconnected, bool DualMode, bool NoDelay, uint Interval, int FastResend = 0, bool CongestionWindow = true, uint SendWindowSize = Kcp.WND_SND, uint ReceiveWindowSize = Kcp.WND_RCV, int Timeout = KcpConnection.DEFAULT_TIMEOUT)
|
||||
: base(OnConnected, OnData, OnDisconnected, DualMode, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout)
|
||||
{
|
||||
// create reusableClientEP either IPv4 or IPv6
|
||||
reusableClientEP = DualMode
|
||||
? new IPEndPointNonAlloc(IPAddress.IPv6Any, 0)
|
||||
: new IPEndPointNonAlloc(IPAddress.Any, 0);
|
||||
}
|
||||
|
||||
protected override int ReceiveFrom(byte[] buffer, out int connectionHash)
|
||||
{
|
||||
// where-allocation nonalloc ReceiveFrom.
|
||||
int read = socket.ReceiveFrom_NonAlloc(buffer, 0, buffer.Length, SocketFlags.None, reusableClientEP);
|
||||
SocketAddress remoteAddress = reusableClientEP.temp;
|
||||
|
||||
// where-allocation nonalloc GetHashCode
|
||||
connectionHash = remoteAddress.GetHashCode();
|
||||
return read;
|
||||
}
|
||||
|
||||
protected override KcpServerConnection CreateConnection()
|
||||
{
|
||||
// IPEndPointNonAlloc is reused all the time.
|
||||
// we can't store that as the connection's endpoint.
|
||||
// we need a new copy!
|
||||
IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint();
|
||||
|
||||
// for allocation free sending, we also need another
|
||||
// IPEndPointNonAlloc...
|
||||
IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port);
|
||||
|
||||
// create a new KcpConnection NonAlloc version
|
||||
// -> where-allocation IPEndPointNonAlloc is reused.
|
||||
// need to create a new one from the temp address.
|
||||
return new KcpServerConnectionNonAlloc(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54b8398dcd544c8a93bcad846214cc40
|
||||
timeCreated: 1626432191
|
||||
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
8
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cafb8851a0084f3e94a580c207b3923
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("kcp2k.Tests")]
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aec6a15ac7bd43129317ea1f01f19782
|
||||
timeCreated: 1602665988
|
||||
1042
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
1042
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Kcp.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a59b1cae10a334faf807432ab472f212
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
Normal file
46
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// Pool to avoid allocations (from libuv2k & Mirror)
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public class Pool<T>
|
||||
{
|
||||
// Mirror is single threaded, no need for concurrent collections
|
||||
readonly Stack<T> objects = new Stack<T>();
|
||||
|
||||
// some types might need additional parameters in their constructor, so
|
||||
// we use a Func<T> generator
|
||||
readonly Func<T> objectGenerator;
|
||||
|
||||
// some types might need additional cleanup for returned objects
|
||||
readonly Action<T> objectResetter;
|
||||
|
||||
public Pool(Func<T> objectGenerator, Action<T> objectResetter, int initialCapacity)
|
||||
{
|
||||
this.objectGenerator = objectGenerator;
|
||||
this.objectResetter = objectResetter;
|
||||
|
||||
// allocate an initial pool so we have fewer (if any)
|
||||
// allocations in the first few frames (or seconds).
|
||||
for (int i = 0; i < initialCapacity; ++i)
|
||||
objects.Push(objectGenerator());
|
||||
}
|
||||
|
||||
// take an element from the pool, or create a new one if empty
|
||||
public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator();
|
||||
|
||||
// return an element to the pool
|
||||
public void Return(T item)
|
||||
{
|
||||
objectResetter(item);
|
||||
objects.Push(item);
|
||||
}
|
||||
|
||||
// clear the pool
|
||||
public void Clear() => objects.Clear();
|
||||
|
||||
// count to see how many objects are in the pool. useful for tests.
|
||||
public int Count => objects.Count;
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Pool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c07818fc4784bb4ba472c8e5029002
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
62
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal file
62
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Segment.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
// KCP Segment Definition
|
||||
internal class Segment
|
||||
{
|
||||
internal uint conv; // conversation
|
||||
internal uint cmd; // command, e.g. Kcp.CMD_ACK etc.
|
||||
internal uint frg; // fragment
|
||||
internal uint wnd; // window size that the receive can currently receive
|
||||
internal uint ts; // timestamp
|
||||
internal uint sn; // serial number
|
||||
internal uint una;
|
||||
internal uint resendts; // resend timestamp
|
||||
internal int rto;
|
||||
internal uint fastack;
|
||||
internal uint xmit;
|
||||
|
||||
// we need an auto scaling byte[] with a WriteBytes function.
|
||||
// MemoryStream does that perfectly, no need to reinvent the wheel.
|
||||
// note: no need to pool it, because Segment is already pooled.
|
||||
// -> MTU as initial capacity to avoid most runtime resizing/allocations
|
||||
internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF);
|
||||
|
||||
// ikcp_encode_seg
|
||||
// encode a segment into buffer
|
||||
internal int Encode(byte[] ptr, int offset)
|
||||
{
|
||||
int offset_ = offset;
|
||||
offset += Utils.Encode32U(ptr, offset, conv);
|
||||
offset += Utils.Encode8u(ptr, offset, (byte)cmd);
|
||||
offset += Utils.Encode8u(ptr, offset, (byte)frg);
|
||||
offset += Utils.Encode16U(ptr, offset, (ushort)wnd);
|
||||
offset += Utils.Encode32U(ptr, offset, ts);
|
||||
offset += Utils.Encode32U(ptr, offset, sn);
|
||||
offset += Utils.Encode32U(ptr, offset, una);
|
||||
offset += Utils.Encode32U(ptr, offset, (uint)data.Position);
|
||||
|
||||
return offset - offset_;
|
||||
}
|
||||
|
||||
// reset to return a fresh segment to the pool
|
||||
internal void Reset()
|
||||
{
|
||||
conv = 0;
|
||||
cmd = 0;
|
||||
frg = 0;
|
||||
wnd = 0;
|
||||
ts = 0;
|
||||
sn = 0;
|
||||
una = 0;
|
||||
rto = 0;
|
||||
xmit = 0;
|
||||
resendts = 0;
|
||||
fastack = 0;
|
||||
|
||||
// keep buffer for next pool usage, but reset length (= bytes written)
|
||||
data.SetLength(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc58706a05dd3442c8fde858d5266855
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
76
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal file
76
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
public static partial class Utils
|
||||
{
|
||||
// Clamp so we don't have to depend on UnityEngine
|
||||
public static int Clamp(int value, int min, int max)
|
||||
{
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
// encode 8 bits unsigned int
|
||||
public static int Encode8u(byte[] p, int offset, byte c)
|
||||
{
|
||||
p[0 + offset] = c;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// decode 8 bits unsigned int
|
||||
public static int Decode8u(byte[] p, int offset, ref byte c)
|
||||
{
|
||||
c = p[0 + offset];
|
||||
return 1;
|
||||
}
|
||||
|
||||
// encode 16 bits unsigned int (lsb)
|
||||
public static int Encode16U(byte[] p, int offset, ushort w)
|
||||
{
|
||||
p[0 + offset] = (byte)(w >> 0);
|
||||
p[1 + offset] = (byte)(w >> 8);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// decode 16 bits unsigned int (lsb)
|
||||
public static int Decode16U(byte[] p, int offset, ref ushort c)
|
||||
{
|
||||
ushort result = 0;
|
||||
result |= p[0 + offset];
|
||||
result |= (ushort)(p[1 + offset] << 8);
|
||||
c = result;
|
||||
return 2;
|
||||
}
|
||||
|
||||
// encode 32 bits unsigned int (lsb)
|
||||
public static int Encode32U(byte[] p, int offset, uint l)
|
||||
{
|
||||
p[0 + offset] = (byte)(l >> 0);
|
||||
p[1 + offset] = (byte)(l >> 8);
|
||||
p[2 + offset] = (byte)(l >> 16);
|
||||
p[3 + offset] = (byte)(l >> 24);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// decode 32 bits unsigned int (lsb)
|
||||
public static int Decode32U(byte[] p, int offset, ref uint c)
|
||||
{
|
||||
uint result = 0;
|
||||
result |= p[0 + offset];
|
||||
result |= (uint)(p[1 + offset] << 8);
|
||||
result |= (uint)(p[2 + offset] << 16);
|
||||
result |= (uint)(p[3 + offset] << 24);
|
||||
c = result;
|
||||
return 4;
|
||||
}
|
||||
|
||||
// timediff was a macro in original Kcp. let's inline it if possible.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int TimeDiff(uint later, uint earlier)
|
||||
{
|
||||
return (int)(later - earlier);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
11
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp/Utils.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef959eb716205bd48b050f010a9a35ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
15
Assets/Mirror/Runtime/Transport/KCP/kcp2k/kcp2k.asmdef
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "kcp2k",
|
||||
"references": [
|
||||
"GUID:63c380d6dae6946209ed0832388a657c"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user