Telepathy Transport
This commit is contained in:
parent
cf454ab043
commit
c892d2745a
27 changed files with 1928 additions and 533 deletions
|
|
@ -12,6 +12,7 @@ namespace LightReflectiveMirror
|
|||
public string TransportDLL = "MultiCompiled.dll";
|
||||
public string TransportClass = "Mirror.SimpleWebTransport";
|
||||
public string AuthenticationKey = "Secret Auth Key";
|
||||
public ushort TransportPort = 7777;
|
||||
public int UpdateLoopTime = 10;
|
||||
public int UpdateHeartbeatInterval = 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ namespace LightReflectiveMirror
|
|||
_pendingNATPunches.Remove(clientID);
|
||||
};
|
||||
|
||||
transport.ServerStart();
|
||||
transport.ServerStart(conf.TransportPort);
|
||||
|
||||
WriteLogMessage("OK", ConsoleColor.Green);
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ namespace Mirror
|
|||
/// <summary>
|
||||
/// Start listening for clients
|
||||
/// </summary>
|
||||
public abstract void ServerStart();
|
||||
public abstract void ServerStart(ushort port);
|
||||
|
||||
/// <summary>
|
||||
/// Send data to a client.
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace MultiCompiled
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -4,4 +4,12 @@
|
|||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LRM\LRM.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,362 @@
|
|||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
// ClientState OBJECT that can be handed to the ReceiveThread safely.
|
||||
// => allows us to create a NEW OBJECT every time we connect and start a
|
||||
// receive thread.
|
||||
// => perfectly protects us against data races. fixes all the flaky tests
|
||||
// where .Connecting or .client would still be used by a dieing thread
|
||||
// while attempting to use it for a new connection attempt etc.
|
||||
// => creating a fresh client state each time is the best solution against
|
||||
// data races here!
|
||||
class ClientConnectionState : ConnectionState
|
||||
{
|
||||
public Thread receiveThread;
|
||||
|
||||
// TcpClient.Connected doesn't check if socket != null, which
|
||||
// results in NullReferenceExceptions if connection was closed.
|
||||
// -> let's check it manually instead
|
||||
public bool Connected => client != null &&
|
||||
client.Client != null &&
|
||||
client.Client.Connected;
|
||||
|
||||
// TcpClient has no 'connecting' state to check. We need to keep track
|
||||
// of it manually.
|
||||
// -> checking 'thread.IsAlive && !Connected' is not enough because the
|
||||
// thread is alive and connected is false for a short moment after
|
||||
// disconnecting, so this would cause race conditions.
|
||||
// -> we use a threadsafe bool wrapper so that ThreadFunction can remain
|
||||
// static (it needs a common lock)
|
||||
// => Connecting is true from first Connect() call in here, through the
|
||||
// thread start, until TcpClient.Connect() returns. Simple and clear.
|
||||
// => bools are atomic according to
|
||||
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables
|
||||
// made volatile so the compiler does not reorder access to it
|
||||
public volatile bool Connecting;
|
||||
|
||||
// thread safe pipe for received messages
|
||||
// => inside client connection state so that we can create a new state
|
||||
// each time we connect
|
||||
// (unlike server which has one receive pipe for all connections)
|
||||
public readonly MagnificentReceivePipe receivePipe;
|
||||
|
||||
// constructor always creates new TcpClient for client connection!
|
||||
public ClientConnectionState(int MaxMessageSize) : base(new TcpClient(), MaxMessageSize)
|
||||
{
|
||||
// create receive pipe with max message size for pooling
|
||||
receivePipe = new MagnificentReceivePipe(MaxMessageSize);
|
||||
}
|
||||
|
||||
// dispose all the state safely
|
||||
public void Dispose()
|
||||
{
|
||||
// close client
|
||||
client.Close();
|
||||
|
||||
// wait until thread finished. this is the only way to guarantee
|
||||
// that we can call Connect() again immediately after Disconnect
|
||||
// -> calling .Join would sometimes wait forever, e.g. when
|
||||
// calling Disconnect while trying to connect to a dead end
|
||||
receiveThread?.Interrupt();
|
||||
|
||||
// we interrupted the receive Thread, so we can't guarantee that
|
||||
// connecting was reset. let's do it manually.
|
||||
Connecting = false;
|
||||
|
||||
// clear send pipe. no need to hold on to elements.
|
||||
// (unlike receiveQueue, which is still needed to process the
|
||||
// latest Disconnected message, etc.)
|
||||
sendPipe.Clear();
|
||||
|
||||
// IMPORTANT: DO NOT CLEAR RECEIVE PIPE.
|
||||
// we still want to process disconnect messages in Tick()!
|
||||
|
||||
// let go of this client completely. the thread ended, no one uses
|
||||
// it anymore and this way Connected is false again immediately.
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class Client : Common
|
||||
{
|
||||
// events to hook into
|
||||
// => OnData uses ArraySegment for allocation free receives later
|
||||
public Action OnConnected;
|
||||
public Action<ArraySegment<byte>> OnData;
|
||||
public Action OnDisconnected;
|
||||
|
||||
// disconnect if send queue gets too big.
|
||||
// -> avoids ever growing queue memory if network is slower than input
|
||||
// -> disconnecting is great for load balancing. better to disconnect
|
||||
// one connection than risking every connection / the whole server
|
||||
// -> huge queue would introduce multiple seconds of latency anyway
|
||||
//
|
||||
// Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size:
|
||||
// limit = 1,000 means 16 MB of memory/connection
|
||||
// limit = 10,000 means 160 MB of memory/connection
|
||||
public int SendQueueLimit = 10000;
|
||||
public int ReceiveQueueLimit = 10000;
|
||||
|
||||
// all client state wrapped into an object that is passed to ReceiveThread
|
||||
// => we create a new one each time we connect to avoid data races with
|
||||
// old dieing threads still using the previous object!
|
||||
ClientConnectionState state;
|
||||
|
||||
// Connected & Connecting
|
||||
public bool Connected => state != null && state.Connected;
|
||||
public bool Connecting => state != null && state.Connecting;
|
||||
|
||||
// pipe count, useful for debugging / benchmarks
|
||||
public int ReceivePipeCount => state != null ? state.receivePipe.TotalCount : 0;
|
||||
|
||||
// constructor
|
||||
public Client(int MaxMessageSize) : base(MaxMessageSize) {}
|
||||
|
||||
// the thread function
|
||||
// STATIC to avoid sharing state!
|
||||
// => pass ClientState object. a new one is created for each new thread!
|
||||
// => avoids data races where an old dieing thread might still modify
|
||||
// the current thread's state :/
|
||||
static void ReceiveThreadFunction(ClientConnectionState state, string ip, int port, int MaxMessageSize, bool NoDelay, int SendTimeout, int ReceiveTimeout, int ReceiveQueueLimit)
|
||||
|
||||
{
|
||||
Thread sendThread = null;
|
||||
|
||||
// absolutely must wrap with try/catch, otherwise thread
|
||||
// exceptions are silent
|
||||
try
|
||||
{
|
||||
// connect (blocking)
|
||||
state.client.Connect(ip, port);
|
||||
state.Connecting = false; // volatile!
|
||||
|
||||
// set socket options after the socket was created in Connect()
|
||||
// (not after the constructor because we clear the socket there)
|
||||
state.client.NoDelay = NoDelay;
|
||||
state.client.SendTimeout = SendTimeout;
|
||||
state.client.ReceiveTimeout = ReceiveTimeout;
|
||||
|
||||
// start send thread only after connected
|
||||
// IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS!
|
||||
sendThread = new Thread(() => { ThreadFunctions.SendLoop(0, state.client, state.sendPipe, state.sendPending); });
|
||||
sendThread.IsBackground = true;
|
||||
sendThread.Start();
|
||||
|
||||
// run the receive loop
|
||||
// (receive pipe is shared across all loops)
|
||||
ThreadFunctions.ReceiveLoop(0, state.client, MaxMessageSize, state.receivePipe, ReceiveQueueLimit);
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// this happens if (for example) the ip address is correct
|
||||
// but there is no server running on that ip/port
|
||||
Log.Info("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception);
|
||||
|
||||
// add 'Disconnected' event to receive pipe so that the caller
|
||||
// knows that the Connect failed. otherwise they will never know
|
||||
state.receivePipe.Enqueue(0, EventType.Disconnected, default);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
// expected if Disconnect() aborts it
|
||||
}
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
// expected if Disconnect() aborts it
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// expected if Disconnect() aborts it and disposed the client
|
||||
// while ReceiveThread is in a blocking Connect() call
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// something went wrong. probably important.
|
||||
Log.Error("Client Recv Exception: " + exception);
|
||||
}
|
||||
|
||||
// sendthread might be waiting on ManualResetEvent,
|
||||
// so let's make sure to end it if the connection
|
||||
// closed.
|
||||
// otherwise the send thread would only end if it's
|
||||
// actually sending data while the connection is
|
||||
// closed.
|
||||
sendThread?.Interrupt();
|
||||
|
||||
// Connect might have failed. thread might have been closed.
|
||||
// let's reset connecting state no matter what.
|
||||
state.Connecting = false;
|
||||
|
||||
// if we got here then we are done. ReceiveLoop cleans up already,
|
||||
// but we may never get there if connect fails. so let's clean up
|
||||
// here too.
|
||||
state.client?.Close();
|
||||
}
|
||||
|
||||
public void Connect(string ip, int port)
|
||||
{
|
||||
// not if already started
|
||||
if (Connecting || Connected)
|
||||
{
|
||||
Log.Warning("Telepathy Client can not create connection because an existing connection is connecting or connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// overwrite old thread's state object. create a new one to avoid
|
||||
// data races where an old dieing thread might still modify the
|
||||
// current state! fixes all the flaky tests!
|
||||
state = new ClientConnectionState(MaxMessageSize);
|
||||
|
||||
// We are connecting from now until Connect succeeds or fails
|
||||
state.Connecting = true;
|
||||
|
||||
// create a TcpClient with perfect IPv4, IPv6 and hostname resolving
|
||||
// support.
|
||||
//
|
||||
// * TcpClient(hostname, port): works but would connect (and block)
|
||||
// already
|
||||
// * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6
|
||||
// addresses but only connects to IPv6 servers (e.g. Telepathy).
|
||||
// does NOT connect to IPv4 servers (e.g. Mirror Booster), even
|
||||
// with DualMode enabled.
|
||||
// * TcpClient(): creates IPv4 socket internally, which would force
|
||||
// Connect() to only use IPv4 sockets.
|
||||
//
|
||||
// => the trick is to clear the internal IPv4 socket so that Connect
|
||||
// resolves the hostname and creates either an IPv4 or an IPv6
|
||||
// socket as needed (see TcpClient source)
|
||||
state.client.Client = null; // clear internal IPv4 socket until Connect()
|
||||
|
||||
// client.Connect(ip, port) is blocking. let's call it in the thread
|
||||
// and return immediately.
|
||||
// -> this way the application doesn't hang for 30s if connect takes
|
||||
// too long, which is especially good in games
|
||||
// -> this way we don't async client.BeginConnect, which seems to
|
||||
// fail sometimes if we connect too many clients too fast
|
||||
state.receiveThread = new Thread(() => {
|
||||
ReceiveThreadFunction(state, ip, port, MaxMessageSize, NoDelay, SendTimeout, ReceiveTimeout, ReceiveQueueLimit);
|
||||
});
|
||||
state.receiveThread.IsBackground = true;
|
||||
state.receiveThread.Start();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
// only if started
|
||||
if (Connecting || Connected)
|
||||
{
|
||||
// dispose all the state safely
|
||||
state.Dispose();
|
||||
|
||||
// IMPORTANT: DO NOT set state = null!
|
||||
// we still want to process the pipe's disconnect message etc.!
|
||||
}
|
||||
}
|
||||
|
||||
// send message to server using socket connection.
|
||||
// arraysegment for allocation free sends later.
|
||||
// -> the segment's array is only used until Send() returns!
|
||||
public bool Send(ArraySegment<byte> message)
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
// respect max message size to avoid allocation attacks.
|
||||
if (message.Count <= MaxMessageSize)
|
||||
{
|
||||
// check send pipe limit
|
||||
if (state.sendPipe.Count < SendQueueLimit)
|
||||
{
|
||||
// add to thread safe send pipe and return immediately.
|
||||
// calling Send here would be blocking (sometimes for long
|
||||
// times if other side lags or wire was disconnected)
|
||||
state.sendPipe.Enqueue(message);
|
||||
state.sendPending.Set(); // interrupt SendThread WaitOne()
|
||||
return true;
|
||||
}
|
||||
// disconnect if send queue gets too big.
|
||||
// -> avoids ever growing queue memory if network is slower
|
||||
// than input
|
||||
// -> avoids ever growing latency as well
|
||||
//
|
||||
// note: while SendThread always grabs the WHOLE send queue
|
||||
// immediately, it's still possible that the sending
|
||||
// blocks for so long that the send queue just gets
|
||||
// way too big. have a limit - better safe than sorry.
|
||||
else
|
||||
{
|
||||
// log the reason
|
||||
Log.Warning($"Client.Send: sendPipe reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting to avoid ever growing memory & latency.");
|
||||
|
||||
// just close it. send thread will take care of the rest.
|
||||
state.client.Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Log.Error("Client.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize);
|
||||
return false;
|
||||
}
|
||||
Log.Warning("Client.Send: not connected!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// tick: processes up to 'limit' messages
|
||||
// => limit parameter to avoid deadlocks / too long freezes if server or
|
||||
// client is too slow to process network load
|
||||
// => Mirror & DOTSNET need to have a process limit anyway.
|
||||
// might as well do it here and make life easier.
|
||||
// => returns amount of remaining messages to process, so the caller
|
||||
// can call tick again as many times as needed (or up to a limit)
|
||||
//
|
||||
// Tick() may process multiple messages, but Mirror needs a way to stop
|
||||
// processing immediately if a scene change messages arrives. Mirror
|
||||
// can't process any other messages during a scene change.
|
||||
// (could be useful for others too)
|
||||
// => make sure to allocate the lambda only once in transports
|
||||
public int Tick(int processLimit, Func<bool> checkEnabled = null)
|
||||
{
|
||||
// only if state was created yet (after connect())
|
||||
// note: we don't check 'only if connected' because we want to still
|
||||
// process Disconnect messages afterwards too!
|
||||
if (state == null)
|
||||
return 0;
|
||||
|
||||
// process up to 'processLimit' messages
|
||||
for (int i = 0; i < processLimit; ++i)
|
||||
{
|
||||
// check enabled in case a Mirror scene message arrived
|
||||
if (checkEnabled != null && !checkEnabled())
|
||||
break;
|
||||
|
||||
// peek first. allows us to process the first queued entry while
|
||||
// still keeping the pooled byte[] alive by not removing anything.
|
||||
if (state.receivePipe.TryPeek(out int _, out EventType eventType, out ArraySegment<byte> message))
|
||||
{
|
||||
switch (eventType)
|
||||
{
|
||||
case EventType.Connected:
|
||||
OnConnected?.Invoke();
|
||||
break;
|
||||
case EventType.Data:
|
||||
OnData?.Invoke(message);
|
||||
break;
|
||||
case EventType.Disconnected:
|
||||
OnDisconnected?.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
// IMPORTANT: now dequeue and return it to pool AFTER we are
|
||||
// done processing the event.
|
||||
state.receivePipe.TryDequeue();
|
||||
}
|
||||
// no more messages. stop the loop.
|
||||
else break;
|
||||
}
|
||||
|
||||
// return what's left to process for next time
|
||||
return state.receivePipe.TotalCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// common code used by server and client
|
||||
namespace Telepathy
|
||||
{
|
||||
public abstract class Common
|
||||
{
|
||||
// IMPORTANT: DO NOT SHARE STATE ACROSS SEND/RECV LOOPS (DATA RACES)
|
||||
// (except receive pipe which is used for all threads)
|
||||
|
||||
// NoDelay disables nagle algorithm. lowers CPU% and latency but
|
||||
// increases bandwidth
|
||||
public bool NoDelay = true;
|
||||
|
||||
// Prevent allocation attacks. Each packet is prefixed with a length
|
||||
// header, so an attacker could send a fake packet with length=2GB,
|
||||
// causing the server to allocate 2GB and run out of memory quickly.
|
||||
// -> simply increase max packet size if you want to send around bigger
|
||||
// files!
|
||||
// -> 16KB per message should be more than enough.
|
||||
public readonly int MaxMessageSize;
|
||||
|
||||
// Send would stall forever if the network is cut off during a send, so
|
||||
// we need a timeout (in milliseconds)
|
||||
public int SendTimeout = 5000;
|
||||
|
||||
// Default TCP receive time out can be huge (minutes).
|
||||
// That's way too much for games, let's make it configurable.
|
||||
// we need a timeout (in milliseconds)
|
||||
// => '0' means disabled
|
||||
// => disabled by default because some people might use Telepathy
|
||||
// without Mirror and without sending pings, so timeouts are likely
|
||||
public int ReceiveTimeout = 0;
|
||||
|
||||
// constructor
|
||||
protected Common(int MaxMessageSize)
|
||||
{
|
||||
this.MaxMessageSize = MaxMessageSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// both server and client need a connection state object.
|
||||
// -> server needs it to keep track of multiple connections
|
||||
// -> client needs it to safely create a new connection state on every new
|
||||
// connect in order to avoid data races where a dieing thread might still
|
||||
// modify the current state. can't happen if we create a new state each time!
|
||||
// (fixes all the flaky tests)
|
||||
//
|
||||
// ... besides, it also allows us to share code!
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public class ConnectionState
|
||||
{
|
||||
public TcpClient client;
|
||||
|
||||
// thread safe pipe to send messages from main thread to send thread
|
||||
public readonly MagnificentSendPipe sendPipe;
|
||||
|
||||
// ManualResetEvent to wake up the send thread. better than Thread.Sleep
|
||||
// -> call Set() if everything was sent
|
||||
// -> call Reset() if there is something to send again
|
||||
// -> call WaitOne() to block until Reset was called
|
||||
public ManualResetEvent sendPending = new ManualResetEvent(false);
|
||||
|
||||
public ConnectionState(TcpClient client, int MaxMessageSize)
|
||||
{
|
||||
this.client = client;
|
||||
|
||||
// create send pipe with max message size for pooling
|
||||
sendPipe = new MagnificentSendPipe(MaxMessageSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Telepathy
|
||||
{
|
||||
public enum EventType
|
||||
{
|
||||
Connected,
|
||||
Data,
|
||||
Disconnected
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018, 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.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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.DLL and don't need a
|
||||
// different version for every UnityEngine version here)
|
||||
using System;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
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 @@
|
|||
// removed 2021-02-04
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
// a magnificent receive pipe to shield us from all of life's complexities.
|
||||
// safely sends messages from receive thread to main thread.
|
||||
// -> thread safety built in
|
||||
// -> byte[] pooling coming in the future
|
||||
//
|
||||
// => hides all the complexity from telepathy
|
||||
// => easy to switch between stack/queue/concurrentqueue/etc.
|
||||
// => easy to test
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public class MagnificentReceivePipe
|
||||
{
|
||||
// queue entry message. only used in here.
|
||||
// -> byte arrays are always of 4 + MaxMessageSize
|
||||
// -> ArraySegment indicates the actual message content
|
||||
struct Entry
|
||||
{
|
||||
public int connectionId;
|
||||
public EventType eventType;
|
||||
public ArraySegment<byte> data;
|
||||
public Entry(int connectionId, EventType eventType, ArraySegment<byte> data)
|
||||
{
|
||||
this.connectionId = connectionId;
|
||||
this.eventType = eventType;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// message queue
|
||||
// ConcurrentQueue allocates. lock{} instead.
|
||||
//
|
||||
// IMPORTANT: lock{} all usages!
|
||||
readonly Queue<Entry> queue = new Queue<Entry>();
|
||||
|
||||
// byte[] pool to avoid allocations
|
||||
// Take & Return is beautifully encapsulated in the pipe.
|
||||
// the outside does not need to worry about anything.
|
||||
// and it can be tested easily.
|
||||
//
|
||||
// IMPORTANT: lock{} all usages!
|
||||
Pool<byte[]> pool;
|
||||
|
||||
// unfortunately having one receive pipe per connetionId is way slower
|
||||
// in CCU tests. right now we have one pipe for all connections.
|
||||
// => we still need to limit queued messages per connection to avoid one
|
||||
// spamming connection being able to slow down everyone else since
|
||||
// the queue would be full of just this connection's messages forever
|
||||
// => let's use a simpler per-connectionId counter for now
|
||||
Dictionary<int, int> queueCounter = new Dictionary<int, int>();
|
||||
|
||||
// constructor
|
||||
public MagnificentReceivePipe(int MaxMessageSize)
|
||||
{
|
||||
// initialize pool to create max message sized byte[]s each time
|
||||
pool = new Pool<byte[]>(() => new byte[MaxMessageSize]);
|
||||
}
|
||||
|
||||
// return amount of queued messages for this connectionId.
|
||||
// for statistics. don't call Count and assume that it's the same after
|
||||
// the call.
|
||||
public int Count(int connectionId)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return queueCounter.TryGetValue(connectionId, out int count)
|
||||
? count
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// total count
|
||||
public int TotalCount
|
||||
{
|
||||
get { lock (this) { return queue.Count; } }
|
||||
}
|
||||
|
||||
// pool count for testing
|
||||
public int PoolCount
|
||||
{
|
||||
get { lock (this) { return pool.Count(); } }
|
||||
}
|
||||
|
||||
// enqueue a message
|
||||
// -> ArraySegment to avoid allocations later
|
||||
// -> parameters passed directly so it's more obvious that we don't just
|
||||
// queue a passed 'Message', instead we copy the ArraySegment into
|
||||
// a byte[] and store it internally, etc.)
|
||||
public void Enqueue(int connectionId, EventType eventType, ArraySegment<byte> message)
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
// does this message have a data array content?
|
||||
ArraySegment<byte> segment = default;
|
||||
if (message != default)
|
||||
{
|
||||
// ArraySegment is only valid until returning.
|
||||
// copy it into a byte[] that we can store.
|
||||
// ArraySegment array is only valid until returning, so copy
|
||||
// it into a byte[] that we can queue safely.
|
||||
|
||||
// get one from the pool first to avoid allocations
|
||||
byte[] bytes = pool.Take();
|
||||
|
||||
// copy into it
|
||||
Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count);
|
||||
|
||||
// indicate which part is the message
|
||||
segment = new ArraySegment<byte>(bytes, 0, message.Count);
|
||||
}
|
||||
|
||||
// enqueue it
|
||||
// IMPORTANT: pass the segment around pool byte[],
|
||||
// NOT the 'message' that is only valid until returning!
|
||||
Entry entry = new Entry(connectionId, eventType, segment);
|
||||
queue.Enqueue(entry);
|
||||
|
||||
// increase counter for this connectionId
|
||||
int oldCount = Count(connectionId);
|
||||
queueCounter[connectionId] = oldCount + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// peek the next message
|
||||
// -> allows the caller to process it while pipe still holds on to the
|
||||
// byte[]
|
||||
// -> TryDequeue should be called after processing, so that the message
|
||||
// is actually dequeued and the byte[] is returned to pool!
|
||||
// => see TryDequeue comments!
|
||||
//
|
||||
// IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD!
|
||||
public bool TryPeek(out int connectionId, out EventType eventType, out ArraySegment<byte> data)
|
||||
{
|
||||
connectionId = 0;
|
||||
eventType = EventType.Disconnected;
|
||||
data = default;
|
||||
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
if (queue.Count > 0)
|
||||
{
|
||||
Entry entry = queue.Peek();
|
||||
connectionId = entry.connectionId;
|
||||
eventType = entry.eventType;
|
||||
data = entry.data;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// dequeue the next message
|
||||
// -> simply dequeues and returns the byte[] to pool (if any)
|
||||
// -> use Peek to actually process the first element while the pipe
|
||||
// still holds on to the byte[]
|
||||
// -> doesn't return the element because the byte[] needs to be returned
|
||||
// to the pool in dequeue. caller can't be allowed to work with a
|
||||
// byte[] that is already returned to pool.
|
||||
// => Peek & Dequeue is the most simple, clean solution for receive
|
||||
// pipe pooling to avoid allocations!
|
||||
//
|
||||
// IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD!
|
||||
public bool TryDequeue()
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
if (queue.Count > 0)
|
||||
{
|
||||
// dequeue from queue
|
||||
Entry entry = queue.Dequeue();
|
||||
|
||||
// return byte[] to pool (if any).
|
||||
// not all message types have byte[] contents.
|
||||
if (entry.data != default)
|
||||
{
|
||||
pool.Return(entry.data.Array);
|
||||
}
|
||||
|
||||
// decrease counter for this connectionId
|
||||
queueCounter[entry.connectionId]--;
|
||||
|
||||
// remove if zero. don't want to keep old connectionIds in
|
||||
// there forever, it would cause slowly growing memory.
|
||||
if (queueCounter[entry.connectionId] == 0)
|
||||
queueCounter.Remove(entry.connectionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
// clear queue, but via dequeue to return each byte[] to pool
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
// dequeue
|
||||
Entry entry = queue.Dequeue();
|
||||
|
||||
// return byte[] to pool (if any).
|
||||
// not all message types have byte[] contents.
|
||||
if (entry.data != default)
|
||||
{
|
||||
pool.Return(entry.data.Array);
|
||||
}
|
||||
}
|
||||
|
||||
// clear counter too
|
||||
queueCounter.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
// a magnificent send pipe to shield us from all of life's complexities.
|
||||
// safely sends messages from main thread to send thread.
|
||||
// -> thread safety built in
|
||||
// -> byte[] pooling coming in the future
|
||||
//
|
||||
// => hides all the complexity from telepathy
|
||||
// => easy to switch between stack/queue/concurrentqueue/etc.
|
||||
// => easy to test
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public class MagnificentSendPipe
|
||||
{
|
||||
// message queue
|
||||
// ConcurrentQueue allocates. lock{} instead.
|
||||
// -> byte arrays are always of MaxMessageSize
|
||||
// -> ArraySegment indicates the actual message content
|
||||
//
|
||||
// IMPORTANT: lock{} all usages!
|
||||
readonly Queue<ArraySegment<byte>> queue = new Queue<ArraySegment<byte>>();
|
||||
|
||||
// byte[] pool to avoid allocations
|
||||
// Take & Return is beautifully encapsulated in the pipe.
|
||||
// the outside does not need to worry about anything.
|
||||
// and it can be tested easily.
|
||||
//
|
||||
// IMPORTANT: lock{} all usages!
|
||||
Pool<byte[]> pool;
|
||||
|
||||
// constructor
|
||||
public MagnificentSendPipe(int MaxMessageSize)
|
||||
{
|
||||
// initialize pool to create max message sized byte[]s each time
|
||||
pool = new Pool<byte[]>(() => new byte[MaxMessageSize]);
|
||||
}
|
||||
|
||||
// for statistics. don't call Count and assume that it's the same after
|
||||
// the call.
|
||||
public int Count
|
||||
{
|
||||
get { lock (this) { return queue.Count; } }
|
||||
}
|
||||
|
||||
// pool count for testing
|
||||
public int PoolCount
|
||||
{
|
||||
get { lock (this) { return pool.Count(); } }
|
||||
}
|
||||
|
||||
// enqueue a message
|
||||
// arraysegment for allocation free sends later.
|
||||
// -> the segment's array is only used until Enqueue() returns!
|
||||
public void Enqueue(ArraySegment<byte> message)
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
// ArraySegment array is only valid until returning, so copy
|
||||
// it into a byte[] that we can queue safely.
|
||||
|
||||
// get one from the pool first to avoid allocations
|
||||
byte[] bytes = pool.Take();
|
||||
|
||||
// copy into it
|
||||
Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count);
|
||||
|
||||
// indicate which part is the message
|
||||
ArraySegment<byte> segment = new ArraySegment<byte>(bytes, 0, message.Count);
|
||||
|
||||
// now enqueue it
|
||||
queue.Enqueue(segment);
|
||||
}
|
||||
}
|
||||
|
||||
// send threads need to dequeue each byte[] and write it into the socket
|
||||
// -> dequeueing one byte[] after another works, but it's WAY slower
|
||||
// than dequeueing all immediately (locks only once)
|
||||
// lock{} & DequeueAll is WAY faster than ConcurrentQueue & dequeue
|
||||
// one after another:
|
||||
//
|
||||
// uMMORPG 450 CCU
|
||||
// SafeQueue: 900-1440ms latency
|
||||
// ConcurrentQueue: 2000ms latency
|
||||
//
|
||||
// -> the most obvious solution is to just return a list with all byte[]
|
||||
// (which allocates) and then write each one into the socket
|
||||
// -> a faster solution is to serialize each one into one payload buffer
|
||||
// and pass that to the socket only once. fewer socket calls always
|
||||
// give WAY better CPU performance(!)
|
||||
// -> to avoid allocating a new list of entries each time, we simply
|
||||
// serialize all entries into the payload here already
|
||||
// => having all this complexity built into the pipe makes testing and
|
||||
// modifying the algorithm super easy!
|
||||
//
|
||||
// IMPORTANT: serializing in here will allow us to return the byte[]
|
||||
// entries back to a pool later to completely avoid
|
||||
// allocations!
|
||||
public bool DequeueAndSerializeAll(ref byte[] payload, out int packetSize)
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
// do nothing if empty
|
||||
packetSize = 0;
|
||||
if (queue.Count == 0)
|
||||
return false;
|
||||
|
||||
// we might have multiple pending messages. merge into one
|
||||
// packet to avoid TCP overheads and improve performance.
|
||||
//
|
||||
// IMPORTANT: Mirror & DOTSNET already batch into MaxMessageSize
|
||||
// chunks, but we STILL pack all pending messages
|
||||
// into one large payload so we only give it to TCP
|
||||
// ONCE. This is HUGE for performance so we keep it!
|
||||
packetSize = 0;
|
||||
foreach (ArraySegment<byte> message in queue)
|
||||
packetSize += 4 + message.Count; // header + content
|
||||
|
||||
// create payload buffer if not created yet or previous one is
|
||||
// too small
|
||||
// IMPORTANT: payload.Length might be > packetSize! don't use it!
|
||||
if (payload == null || payload.Length < packetSize)
|
||||
payload = new byte[packetSize];
|
||||
|
||||
// dequeue all byte[] messages and serialize into the packet
|
||||
int position = 0;
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
// dequeue
|
||||
ArraySegment<byte> message = queue.Dequeue();
|
||||
|
||||
// write header (size) into buffer at position
|
||||
Utils.IntToBytesBigEndianNonAlloc(message.Count, payload, position);
|
||||
position += 4;
|
||||
|
||||
// copy message into payload at position
|
||||
Buffer.BlockCopy(message.Array, message.Offset, payload, position, message.Count);
|
||||
position += message.Count;
|
||||
|
||||
// return to pool so it can be reused (avoids allocations!)
|
||||
pool.Return(message.Array);
|
||||
}
|
||||
|
||||
// we did serialize something
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
// pool & queue usage always needs to be locked
|
||||
lock (this)
|
||||
{
|
||||
// clear queue, but via dequeue to return each byte[] to pool
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
pool.Return(queue.Dequeue().Array);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// removed 2021-02-04
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public static class NetworkStreamExtensions
|
||||
{
|
||||
// .Read returns '0' if remote closed the connection but throws an
|
||||
// IOException if we voluntarily closed our own connection.
|
||||
//
|
||||
// let's add a ReadSafely method that returns '0' in both cases so we don't
|
||||
// have to worry about exceptions, since a disconnect is a disconnect...
|
||||
public static int ReadSafely(this NetworkStream stream, byte[] buffer, int offset, int size)
|
||||
{
|
||||
try
|
||||
{
|
||||
return stream.Read(buffer, offset, size);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to read EXACTLY 'n' bytes
|
||||
// -> default .Read reads up to 'n' bytes. this function reads exactly
|
||||
// 'n' bytes
|
||||
// -> this is blocking until 'n' bytes were received
|
||||
// -> immediately returns false in case of disconnects
|
||||
public static bool ReadExactly(this NetworkStream stream, byte[] buffer, int amount)
|
||||
{
|
||||
// there might not be enough bytes in the TCP buffer for .Read to read
|
||||
// the whole amount at once, so we need to keep trying until we have all
|
||||
// the bytes (blocking)
|
||||
//
|
||||
// note: this just is a faster version of reading one after another:
|
||||
// for (int i = 0; i < amount; ++i)
|
||||
// if (stream.Read(buffer, i, 1) == 0)
|
||||
// return false;
|
||||
// return true;
|
||||
int bytesRead = 0;
|
||||
while (bytesRead < amount)
|
||||
{
|
||||
// read up to 'remaining' bytes with the 'safe' read extension
|
||||
int remaining = amount - bytesRead;
|
||||
int result = stream.ReadSafely(buffer, bytesRead, remaining);
|
||||
|
||||
// .Read returns 0 if disconnected
|
||||
if (result == 0)
|
||||
return false;
|
||||
|
||||
// otherwise add to bytes read
|
||||
bytesRead += result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// pool to avoid allocations. originally from libuv2k.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public class Pool<T>
|
||||
{
|
||||
// objects
|
||||
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;
|
||||
|
||||
// constructor
|
||||
public Pool(Func<T> objectGenerator)
|
||||
{
|
||||
this.objectGenerator = 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) => objects.Push(item);
|
||||
|
||||
// clear the pool with the disposer function applied to each object
|
||||
public void Clear() => objects.Clear();
|
||||
|
||||
// count to see how many objects are in the pool. useful for tests.
|
||||
public int Count() => objects.Count;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// removed 2021-02-04
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public class Server : Common
|
||||
{
|
||||
// events to hook into
|
||||
// => OnData uses ArraySegment for allocation free receives later
|
||||
public Action<int> OnConnected;
|
||||
public Action<int, ArraySegment<byte>> OnData;
|
||||
public Action<int> OnDisconnected;
|
||||
|
||||
// listener
|
||||
public TcpListener listener;
|
||||
Thread listenerThread;
|
||||
|
||||
// disconnect if send queue gets too big.
|
||||
// -> avoids ever growing queue memory if network is slower than input
|
||||
// -> disconnecting is great for load balancing. better to disconnect
|
||||
// one connection than risking every connection / the whole server
|
||||
// -> huge queue would introduce multiple seconds of latency anyway
|
||||
//
|
||||
// Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size:
|
||||
// limit = 1,000 means 16 MB of memory/connection
|
||||
// limit = 10,000 means 160 MB of memory/connection
|
||||
public int SendQueueLimit = 10000;
|
||||
public int ReceiveQueueLimit = 10000;
|
||||
|
||||
// thread safe pipe for received messages
|
||||
// IMPORTANT: unfortunately using one pipe per connection is way slower
|
||||
// when testing 150 CCU. we need to use one pipe for all
|
||||
// connections. this scales beautifully.
|
||||
protected MagnificentReceivePipe receivePipe;
|
||||
|
||||
// pipe count, useful for debugging / benchmarks
|
||||
public int ReceivePipeTotalCount => receivePipe.TotalCount;
|
||||
|
||||
// clients with <connectionId, ConnectionState>
|
||||
readonly ConcurrentDictionary<int, ConnectionState> clients = new ConcurrentDictionary<int, ConnectionState>();
|
||||
|
||||
// connectionId counter
|
||||
int counter;
|
||||
|
||||
// public next id function in case someone needs to reserve an id
|
||||
// (e.g. if hostMode should always have 0 connection and external
|
||||
// connections should start at 1, etc.)
|
||||
public int NextConnectionId()
|
||||
{
|
||||
int id = Interlocked.Increment(ref counter);
|
||||
|
||||
// it's very unlikely that we reach the uint limit of 2 billion.
|
||||
// even with 1 new connection per second, this would take 68 years.
|
||||
// -> but if it happens, then we should throw an exception because
|
||||
// the caller probably should stop accepting clients.
|
||||
// -> it's hardly worth using 'bool Next(out id)' for that case
|
||||
// because it's just so unlikely.
|
||||
if (id == int.MaxValue)
|
||||
{
|
||||
throw new Exception("connection id limit reached: " + id);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// check if the server is running
|
||||
public bool Active => listenerThread != null && listenerThread.IsAlive;
|
||||
|
||||
// constructor
|
||||
public Server(int MaxMessageSize) : base(MaxMessageSize) {}
|
||||
|
||||
// the listener thread's listen function
|
||||
// note: no maxConnections parameter. high level API should handle that.
|
||||
// (Transport can't send a 'too full' message anyway)
|
||||
void Listen(int port)
|
||||
{
|
||||
// absolutely must wrap with try/catch, otherwise thread
|
||||
// exceptions are silent
|
||||
try
|
||||
{
|
||||
// start listener on all IPv4 and IPv6 address via .Create
|
||||
listener = TcpListener.Create(port);
|
||||
listener.Server.NoDelay = NoDelay;
|
||||
listener.Server.SendTimeout = SendTimeout;
|
||||
listener.Server.ReceiveTimeout = ReceiveTimeout;
|
||||
listener.Start();
|
||||
Log.Info("Server: listening port=" + port);
|
||||
|
||||
// keep accepting new clients
|
||||
while (true)
|
||||
{
|
||||
// wait and accept new client
|
||||
// note: 'using' sucks here because it will try to
|
||||
// dispose after thread was started but we still need it
|
||||
// in the thread
|
||||
TcpClient client = listener.AcceptTcpClient();
|
||||
|
||||
// set socket options
|
||||
client.NoDelay = NoDelay;
|
||||
client.SendTimeout = SendTimeout;
|
||||
client.ReceiveTimeout = ReceiveTimeout;
|
||||
|
||||
// generate the next connection id (thread safely)
|
||||
int connectionId = NextConnectionId();
|
||||
|
||||
// add to dict immediately
|
||||
ConnectionState connection = new ConnectionState(client, MaxMessageSize);
|
||||
clients[connectionId] = connection;
|
||||
|
||||
// spawn a send thread for each client
|
||||
Thread sendThread = new Thread(() =>
|
||||
{
|
||||
// wrap in try-catch, otherwise Thread exceptions
|
||||
// are silent
|
||||
try
|
||||
{
|
||||
// run the send loop
|
||||
// IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS!
|
||||
ThreadFunctions.SendLoop(connectionId, client, connection.sendPipe, connection.sendPending);
|
||||
}
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
// happens on stop. don't log anything.
|
||||
// (we catch it in SendLoop too, but it still gets
|
||||
// through to here when aborting. don't show an
|
||||
// error.)
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("Server send thread exception: " + exception);
|
||||
}
|
||||
});
|
||||
sendThread.IsBackground = true;
|
||||
sendThread.Start();
|
||||
|
||||
// spawn a receive thread for each client
|
||||
Thread receiveThread = new Thread(() =>
|
||||
{
|
||||
// wrap in try-catch, otherwise Thread exceptions
|
||||
// are silent
|
||||
try
|
||||
{
|
||||
// run the receive loop
|
||||
// (receive pipe is shared across all loops)
|
||||
ThreadFunctions.ReceiveLoop(connectionId, client, MaxMessageSize, receivePipe, ReceiveQueueLimit);
|
||||
|
||||
// IMPORTANT: do NOT remove from clients after the
|
||||
// thread ends. need to do it in Tick() so that the
|
||||
// disconnect event in the pipe is still processed.
|
||||
// (removing client immediately would mean that the
|
||||
// pipe is lost and the disconnect event is never
|
||||
// processed)
|
||||
|
||||
// sendthread might be waiting on ManualResetEvent,
|
||||
// so let's make sure to end it if the connection
|
||||
// closed.
|
||||
// otherwise the send thread would only end if it's
|
||||
// actually sending data while the connection is
|
||||
// closed.
|
||||
sendThread.Interrupt();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("Server client thread exception: " + exception);
|
||||
}
|
||||
});
|
||||
receiveThread.IsBackground = true;
|
||||
receiveThread.Start();
|
||||
}
|
||||
}
|
||||
catch (ThreadAbortException exception)
|
||||
{
|
||||
// UnityEditor causes AbortException if thread is still
|
||||
// running when we press Play again next time. that's okay.
|
||||
Log.Info("Server thread aborted. That's okay. " + exception);
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
// calling StopServer will interrupt this thread with a
|
||||
// 'SocketException: interrupted'. that's okay.
|
||||
Log.Info("Server Thread stopped. That's okay. " + exception);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// something went wrong. probably important.
|
||||
Log.Error("Server Exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
// start listening for new connections in a background thread and spawn
|
||||
// a new thread for each one.
|
||||
public bool Start(int port)
|
||||
{
|
||||
// not if already started
|
||||
if (Active) return false;
|
||||
|
||||
// create receive pipe with max message size for pooling
|
||||
// => create new pipes every time!
|
||||
// if an old receive thread is still finishing up, it might still
|
||||
// be using the old pipes. we don't want to risk any old data for
|
||||
// our new start here.
|
||||
receivePipe = new MagnificentReceivePipe(MaxMessageSize);
|
||||
|
||||
// start the listener thread
|
||||
// (on low priority. if main thread is too busy then there is not
|
||||
// much value in accepting even more clients)
|
||||
Log.Info("Server: Start port=" + port);
|
||||
listenerThread = new Thread(() => { Listen(port); });
|
||||
listenerThread.IsBackground = true;
|
||||
listenerThread.Priority = ThreadPriority.BelowNormal;
|
||||
listenerThread.Start();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
// only if started
|
||||
if (!Active) return;
|
||||
|
||||
Log.Info("Server: stopping...");
|
||||
|
||||
// stop listening to connections so that no one can connect while we
|
||||
// close the client connections
|
||||
// (might be null if we call Stop so quickly after Start that the
|
||||
// thread was interrupted before even creating the listener)
|
||||
listener?.Stop();
|
||||
|
||||
// kill listener thread at all costs. only way to guarantee that
|
||||
// .Active is immediately false after Stop.
|
||||
// -> calling .Join would sometimes wait forever
|
||||
listenerThread?.Interrupt();
|
||||
listenerThread = null;
|
||||
|
||||
// close all client connections
|
||||
foreach (KeyValuePair<int, ConnectionState> kvp in clients)
|
||||
{
|
||||
TcpClient client = kvp.Value.client;
|
||||
// close the stream if not closed yet. it may have been closed
|
||||
// by a disconnect already, so use try/catch
|
||||
try { client.GetStream().Close(); } catch {}
|
||||
client.Close();
|
||||
}
|
||||
|
||||
// clear clients list
|
||||
clients.Clear();
|
||||
|
||||
// reset the counter in case we start up again so
|
||||
// clients get connection ID's starting from 1
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
// send message to client using socket connection.
|
||||
// arraysegment for allocation free sends later.
|
||||
// -> the segment's array is only used until Send() returns!
|
||||
public bool Send(int connectionId, ArraySegment<byte> message)
|
||||
{
|
||||
// respect max message size to avoid allocation attacks.
|
||||
if (message.Count <= MaxMessageSize)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out ConnectionState connection))
|
||||
{
|
||||
// check send pipe limit
|
||||
if (connection.sendPipe.Count < SendQueueLimit)
|
||||
{
|
||||
// add to thread safe send pipe and return immediately.
|
||||
// calling Send here would be blocking (sometimes for long
|
||||
// times if other side lags or wire was disconnected)
|
||||
connection.sendPipe.Enqueue(message);
|
||||
connection.sendPending.Set(); // interrupt SendThread WaitOne()
|
||||
return true;
|
||||
}
|
||||
// disconnect if send queue gets too big.
|
||||
// -> avoids ever growing queue memory if network is slower
|
||||
// than input
|
||||
// -> disconnecting is great for load balancing. better to
|
||||
// disconnect one connection than risking every
|
||||
// connection / the whole server
|
||||
//
|
||||
// note: while SendThread always grabs the WHOLE send queue
|
||||
// immediately, it's still possible that the sending
|
||||
// blocks for so long that the send queue just gets
|
||||
// way too big. have a limit - better safe than sorry.
|
||||
else
|
||||
{
|
||||
// log the reason
|
||||
Log.Warning($"Server.Send: sendPipe for connection {connectionId} reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting this connection for load balancing.");
|
||||
|
||||
// just close it. send thread will take care of the rest.
|
||||
connection.client.Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// sending to an invalid connectionId is expected sometimes.
|
||||
// for example, if a client disconnects, the server might still
|
||||
// try to send for one frame before it calls GetNextMessages
|
||||
// again and realizes that a disconnect happened.
|
||||
// so let's not spam the console with log messages.
|
||||
//Logger.Log("Server.Send: invalid connectionId: " + connectionId);
|
||||
return false;
|
||||
}
|
||||
Log.Error("Server.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// client's ip is sometimes needed by the server, e.g. for bans
|
||||
public string GetClientAddress(int connectionId)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out ConnectionState connection))
|
||||
{
|
||||
return ((IPEndPoint)connection.client.Client.RemoteEndPoint).Address.ToString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// disconnect (kick) a client
|
||||
public bool Disconnect(int connectionId)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out ConnectionState connection))
|
||||
{
|
||||
// just close it. send thread will take care of the rest.
|
||||
connection.client.Close();
|
||||
Log.Info("Server.Disconnect connectionId:" + connectionId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// tick: processes up to 'limit' messages for each connection
|
||||
// => limit parameter to avoid deadlocks / too long freezes if server or
|
||||
// client is too slow to process network load
|
||||
// => Mirror & DOTSNET need to have a process limit anyway.
|
||||
// might as well do it here and make life easier.
|
||||
// => returns amount of remaining messages to process, so the caller
|
||||
// can call tick again as many times as needed (or up to a limit)
|
||||
//
|
||||
// Tick() may process multiple messages, but Mirror needs a way to stop
|
||||
// processing immediately if a scene change messages arrives. Mirror
|
||||
// can't process any other messages during a scene change.
|
||||
// (could be useful for others too)
|
||||
// => make sure to allocate the lambda only once in transports
|
||||
public int Tick(int processLimit, Func<bool> checkEnabled = null)
|
||||
{
|
||||
// only if pipe was created yet (after start())
|
||||
if (receivePipe == null)
|
||||
return 0;
|
||||
|
||||
// process up to 'processLimit' messages for this connection
|
||||
for (int i = 0; i < processLimit; ++i)
|
||||
{
|
||||
// check enabled in case a Mirror scene message arrived
|
||||
if (checkEnabled != null && !checkEnabled())
|
||||
break;
|
||||
|
||||
// peek first. allows us to process the first queued entry while
|
||||
// still keeping the pooled byte[] alive by not removing anything.
|
||||
if (receivePipe.TryPeek(out int connectionId, out EventType eventType, out ArraySegment<byte> message))
|
||||
{
|
||||
switch (eventType)
|
||||
{
|
||||
case EventType.Connected:
|
||||
OnConnected?.Invoke(connectionId);
|
||||
break;
|
||||
case EventType.Data:
|
||||
OnData?.Invoke(connectionId, message);
|
||||
break;
|
||||
case EventType.Disconnected:
|
||||
OnDisconnected?.Invoke(connectionId);
|
||||
// remove disconnected connection now that the final
|
||||
// disconnected message was processed.
|
||||
clients.TryRemove(connectionId, out ConnectionState _);
|
||||
break;
|
||||
}
|
||||
|
||||
// IMPORTANT: now dequeue and return it to pool AFTER we are
|
||||
// done processing the event.
|
||||
receivePipe.TryDequeue();
|
||||
}
|
||||
// no more messages. stop the loop.
|
||||
else break;
|
||||
}
|
||||
|
||||
// return what's left to process for next time
|
||||
return receivePipe.TotalCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// removed 2021-02-04
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
// IMPORTANT
|
||||
// force all thread functions to be STATIC.
|
||||
// => Common.Send/ReceiveLoop is EXTREMELY DANGEROUS because it's too easy to
|
||||
// accidentally share Common state between threads.
|
||||
// => header buffer, payload etc. were accidentally shared once after changing
|
||||
// the thread functions from static to non static
|
||||
// => C# does not automatically detect data races. best we can do is move all of
|
||||
// our thread code into static functions and pass all state into them
|
||||
//
|
||||
// let's even keep them in a STATIC CLASS so it's 100% obvious that this should
|
||||
// NOT EVER be changed to non static!
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Telepathy
|
||||
{
|
||||
public static class ThreadFunctions
|
||||
{
|
||||
// send message (via stream) with the <size,content> message structure
|
||||
// this function is blocking sometimes!
|
||||
// (e.g. if someone has high latency or wire was cut off)
|
||||
// -> payload is of multiple <<size, content, size, content, ...> parts
|
||||
public static bool SendMessagesBlocking(NetworkStream stream, byte[] payload, int packetSize)
|
||||
{
|
||||
// stream.Write throws exceptions if client sends with high
|
||||
// frequency and the server stops
|
||||
try
|
||||
{
|
||||
// write the whole thing
|
||||
stream.Write(payload, 0, packetSize);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// log as regular message because servers do shut down sometimes
|
||||
Log.Info("Send: stream.Write exception: " + exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// read message (via stream) blocking.
|
||||
// writes into byte[] and returns bytes written to avoid allocations.
|
||||
public static bool ReadMessageBlocking(NetworkStream stream, int MaxMessageSize, byte[] headerBuffer, byte[] payloadBuffer, out int size)
|
||||
{
|
||||
size = 0;
|
||||
|
||||
// buffer needs to be of Header + MaxMessageSize
|
||||
if (payloadBuffer.Length != 4 + MaxMessageSize)
|
||||
{
|
||||
Log.Error($"ReadMessageBlocking: payloadBuffer needs to be of size 4 + MaxMessageSize = {4 + MaxMessageSize} instead of {payloadBuffer.Length}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// read exactly 4 bytes for header (blocking)
|
||||
if (!stream.ReadExactly(headerBuffer, 4))
|
||||
return false;
|
||||
|
||||
// convert to int
|
||||
size = Utils.BytesToIntBigEndian(headerBuffer);
|
||||
|
||||
// protect against allocation attacks. an attacker might send
|
||||
// multiple fake '2GB header' packets in a row, causing the server
|
||||
// to allocate multiple 2GB byte arrays and run out of memory.
|
||||
//
|
||||
// also protect against size <= 0 which would cause issues
|
||||
if (size > 0 && size <= MaxMessageSize)
|
||||
{
|
||||
// read exactly 'size' bytes for content (blocking)
|
||||
return stream.ReadExactly(payloadBuffer, size);
|
||||
}
|
||||
Log.Warning("ReadMessageBlocking: possible header attack with a header of: " + size + " bytes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// thread receive function is the same for client and server's clients
|
||||
public static void ReceiveLoop(int connectionId, TcpClient client, int MaxMessageSize, MagnificentReceivePipe receivePipe, int QueueLimit)
|
||||
{
|
||||
// get NetworkStream from client
|
||||
NetworkStream stream = client.GetStream();
|
||||
|
||||
// every receive loop needs it's own receive buffer of
|
||||
// HeaderSize + MaxMessageSize
|
||||
// to avoid runtime allocations.
|
||||
//
|
||||
// IMPORTANT: DO NOT make this a member, otherwise every connection
|
||||
// on the server would use the same buffer simulatenously
|
||||
byte[] receiveBuffer = new byte[4 + MaxMessageSize];
|
||||
|
||||
// avoid header[4] allocations
|
||||
//
|
||||
// IMPORTANT: DO NOT make this a member, otherwise every connection
|
||||
// on the server would use the same buffer simulatenously
|
||||
byte[] headerBuffer = new byte[4];
|
||||
|
||||
// absolutely must wrap with try/catch, otherwise thread exceptions
|
||||
// are silent
|
||||
try
|
||||
{
|
||||
// add connected event to pipe
|
||||
receivePipe.Enqueue(connectionId, EventType.Connected, default);
|
||||
|
||||
// let's talk about reading data.
|
||||
// -> normally we would read as much as possible and then
|
||||
// extract as many <size,content>,<size,content> messages
|
||||
// as we received this time. this is really complicated
|
||||
// and expensive to do though
|
||||
// -> instead we use a trick:
|
||||
// Read(2) -> size
|
||||
// Read(size) -> content
|
||||
// repeat
|
||||
// Read is blocking, but it doesn't matter since the
|
||||
// best thing to do until the full message arrives,
|
||||
// is to wait.
|
||||
// => this is the most elegant AND fast solution.
|
||||
// + no resizing
|
||||
// + no extra allocations, just one for the content
|
||||
// + no crazy extraction logic
|
||||
while (true)
|
||||
{
|
||||
// read the next message (blocking) or stop if stream closed
|
||||
if (!ReadMessageBlocking(stream, MaxMessageSize, headerBuffer, receiveBuffer, out int size))
|
||||
// break instead of return so stream close still happens!
|
||||
break;
|
||||
|
||||
// create arraysegment for the read message
|
||||
ArraySegment<byte> message = new ArraySegment<byte>(receiveBuffer, 0, size);
|
||||
|
||||
// send to main thread via pipe
|
||||
// -> it'll copy the message internally so we can reuse the
|
||||
// receive buffer for next read!
|
||||
receivePipe.Enqueue(connectionId, EventType.Data, message);
|
||||
|
||||
// disconnect if receive pipe gets too big for this connectionId.
|
||||
// -> avoids ever growing queue memory if network is slower
|
||||
// than input
|
||||
// -> disconnecting is great for load balancing. better to
|
||||
// disconnect one connection than risking every
|
||||
// connection / the whole server
|
||||
if (receivePipe.Count(connectionId) >= QueueLimit)
|
||||
{
|
||||
// log the reason
|
||||
Log.Warning($"receivePipe reached limit of {QueueLimit} for connectionId {connectionId}. This can happen if network messages come in way faster than we manage to process them. Disconnecting this connection for load balancing.");
|
||||
|
||||
// IMPORTANT: do NOT clear the whole queue. we use one
|
||||
// queue for all connections.
|
||||
//receivePipe.Clear();
|
||||
|
||||
// just break. the finally{} will close everything.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// something went wrong. the thread was interrupted or the
|
||||
// connection closed or we closed our own connection or ...
|
||||
// -> either way we should stop gracefully
|
||||
Log.Info("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// clean up no matter what
|
||||
stream.Close();
|
||||
client.Close();
|
||||
|
||||
// add 'Disconnected' message after disconnecting properly.
|
||||
// -> always AFTER closing the streams to avoid a race condition
|
||||
// where Disconnected -> Reconnect wouldn't work because
|
||||
// Connected is still true for a short moment before the stream
|
||||
// would be closed.
|
||||
receivePipe.Enqueue(connectionId, EventType.Disconnected, default);
|
||||
}
|
||||
}
|
||||
// thread send function
|
||||
// note: we really do need one per connection, so that if one connection
|
||||
// blocks, the rest will still continue to get sends
|
||||
public static void SendLoop(int connectionId, TcpClient client, MagnificentSendPipe sendPipe, ManualResetEvent sendPending)
|
||||
{
|
||||
// get NetworkStream from client
|
||||
NetworkStream stream = client.GetStream();
|
||||
|
||||
// avoid payload[packetSize] allocations. size increases dynamically as
|
||||
// needed for batching.
|
||||
//
|
||||
// IMPORTANT: DO NOT make this a member, otherwise every connection
|
||||
// on the server would use the same buffer simulatenously
|
||||
byte[] payload = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (client.Connected) // try this. client will get closed eventually.
|
||||
{
|
||||
// reset ManualResetEvent before we do anything else. this
|
||||
// way there is no race condition. if Send() is called again
|
||||
// while in here then it will be properly detected next time
|
||||
// -> otherwise Send might be called right after dequeue but
|
||||
// before .Reset, which would completely ignore it until
|
||||
// the next Send call.
|
||||
sendPending.Reset(); // WaitOne() blocks until .Set() again
|
||||
|
||||
// dequeue & serialize all
|
||||
// a locked{} TryDequeueAll is twice as fast as
|
||||
// ConcurrentQueue, see SafeQueue.cs!
|
||||
if (sendPipe.DequeueAndSerializeAll(ref payload, out int packetSize))
|
||||
{
|
||||
// send messages (blocking) or stop if stream is closed
|
||||
if (!SendMessagesBlocking(stream, payload, packetSize))
|
||||
// break instead of return so stream close still happens!
|
||||
break;
|
||||
}
|
||||
|
||||
// don't choke up the CPU: wait until queue not empty anymore
|
||||
sendPending.WaitOne();
|
||||
}
|
||||
}
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
// happens on stop. don't log anything.
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
// happens if receive thread interrupts send thread.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// something went wrong. the thread was interrupted or the
|
||||
// connection closed or we closed our own connection or ...
|
||||
// -> either way we should stop gracefully
|
||||
Log.Info("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// clean up no matter what
|
||||
// we might get SocketExceptions when sending if the 'host has
|
||||
// failed to respond' - in which case we should close the connection
|
||||
// which causes the ReceiveLoop to end and fire the Disconnected
|
||||
// message. otherwise the connection would stay alive forever even
|
||||
// though we can't send anymore.
|
||||
stream.Close();
|
||||
client.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
namespace Telepathy
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
// IntToBytes version that doesn't allocate a new byte[4] each time.
|
||||
// -> important for MMO scale networking performance.
|
||||
public static void IntToBytesBigEndianNonAlloc(int value, byte[] bytes, int offset = 0)
|
||||
{
|
||||
bytes[offset + 0] = (byte)(value >> 24);
|
||||
bytes[offset + 1] = (byte)(value >> 16);
|
||||
bytes[offset + 2] = (byte)(value >> 8);
|
||||
bytes[offset + 3] = (byte)value;
|
||||
}
|
||||
|
||||
public static int BytesToIntBigEndian(byte[] bytes)
|
||||
{
|
||||
return (bytes[0] << 24) |
|
||||
(bytes[1] << 16) |
|
||||
(bytes[2] << 8) |
|
||||
bytes[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
V1.7 [2021-02-20]
|
||||
- ReceiveTimeout: disabled by default for cases where people use Telepathy by
|
||||
itself without pings etc.
|
||||
|
||||
V1.6 [2021-02-10]
|
||||
- configurable ReceiveTimeout to avoid TCPs high default timeout
|
||||
- Server/Client receive queue limit now disconnects instead of showing a
|
||||
warning. this is necessary for load balancing to avoid situations where one
|
||||
spamming connection might fill the queue and slow down everyone else.
|
||||
|
||||
V1.5 [2021-02-05]
|
||||
- fix: client data races & flaky tests fixed by creating a new client state
|
||||
object every time we connect. fixes data race where an old dieing thread
|
||||
might still try to modify the current state
|
||||
- fix: Client.ReceiveThreadFunction catches and ignores ObjectDisposedException
|
||||
which can happen if Disconnect() closes and disposes the client, while the
|
||||
ReceiveThread just starts up and still uses the client.
|
||||
- Server/Client Tick() optional enabled check for Mirror scene changing
|
||||
|
||||
V1.4 [2021-02-03]
|
||||
- Server/Client.Tick: limit parameter added to process up to 'limit' messages.
|
||||
makes Mirror & DOTSNET transports easier to implement
|
||||
- stability: Server/Client send queue limit disconnects instead of showing a
|
||||
warning. allows for load balancing. better to kick one connection and keep
|
||||
the server running than slowing everything down for everyone.
|
||||
|
||||
V1.3 [2021-02-02]
|
||||
- perf: ReceivePipe: byte[] pool for allocation free receives (╯°□°)╯︵ ┻━┻
|
||||
- fix: header buffer, payload buffer data races because they were made non
|
||||
static earlier. server threads would all access the same ones.
|
||||
=> all threaded code was moved into a static ThreadFunctions class to make it
|
||||
100% obvious that there should be no shared state in the future
|
||||
|
||||
V1.2 [2021-02-02]
|
||||
- Client/Server Tick & OnConnected/OnData/OnDisconnected events instead of
|
||||
having the outside process messages via GetNextMessage. That's easier for
|
||||
Mirror/DOTSNET and allows for allocation free data message processing later.
|
||||
- MagnificientSend/RecvPipe to shield Telepathy from all the complexity
|
||||
- perf: SendPipe: byte[] pool for allocation free sends (╯°□°)╯︵ ┻━┻
|
||||
|
||||
V1.1 [2021-02-01]
|
||||
- stability: added more tests
|
||||
- breaking: Server/Client.Send: ArraySegment parameter and copy internally so
|
||||
that Transports don't need to worry about it
|
||||
- perf: Buffer.BlockCopy instead of Array.Copy
|
||||
- perf: SendMessageBlocking puts message header directly into payload now
|
||||
- perf: receiveQueues use SafeQueue instead of ConcurrentQueue to avoid
|
||||
allocations
|
||||
- Common: removed static state
|
||||
- perf: SafeQueue.TryDequeueAll: avoid queue.ToArray() allocations. copy into a
|
||||
list instead.
|
||||
- Logger.Log/LogWarning/LogError renamed to Log.Info/Warning/Error
|
||||
- MaxMessageSize is now specified in constructor to prepare for pooling
|
||||
- flaky tests are ignored for now
|
||||
- smaller improvements
|
||||
|
||||
V1.0
|
||||
- first stable release
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
class TelepathyConfig
|
||||
{
|
||||
public bool NoDelay = true;
|
||||
|
||||
public int SendTimeout = 5000;
|
||||
|
||||
public int ReceiveTimeout = 30000;
|
||||
|
||||
public int serverMaxMessageSize = 16 * 1024;
|
||||
|
||||
public int serverMaxReceivesPerTick = 10000;
|
||||
|
||||
public int serverSendQueueLimitPerConnection = 10000;
|
||||
|
||||
public int serverReceiveQueueLimitPerConnection = 10000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
// wraps Telepathy for use as HLAPI TransportLayer
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
// Replaced by Kcp November 2020
|
||||
namespace Mirror
|
||||
{
|
||||
public class TelepathyTransport : Transport
|
||||
{
|
||||
// scheme used by this transport
|
||||
// "tcp4" means tcp with 4 bytes header, network byte order
|
||||
public const string Scheme = "tcp4";
|
||||
|
||||
public bool NoDelay = true;
|
||||
|
||||
public int SendTimeout = 5000;
|
||||
|
||||
public int ReceiveTimeout = 30000;
|
||||
|
||||
public int serverMaxMessageSize = 16 * 1024;
|
||||
|
||||
public int serverMaxReceivesPerTick = 10000;
|
||||
|
||||
public int serverSendQueueLimitPerConnection = 10000;
|
||||
|
||||
public int serverReceiveQueueLimitPerConnection = 10000;
|
||||
|
||||
public int clientMaxMessageSize = 16 * 1024;
|
||||
|
||||
public int clientMaxReceivesPerTick = 1000;
|
||||
|
||||
public int clientSendQueueLimit = 10000;
|
||||
|
||||
public int clientReceiveQueueLimit = 10000;
|
||||
|
||||
Telepathy.Client client;
|
||||
Telepathy.Server server;
|
||||
|
||||
// scene change message needs to halt message processing immediately
|
||||
// Telepathy.Tick() has a enabledCheck parameter that we can use, but
|
||||
// let's only allocate it once.
|
||||
Func<bool> enabledCheck;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
TelepathyConfig conf = new TelepathyConfig();
|
||||
if (!File.Exists("TelepathyConfig.json"))
|
||||
{
|
||||
File.WriteAllText("TelepathyConfig.json", JsonConvert.SerializeObject(conf, Formatting.Indented));
|
||||
}
|
||||
else
|
||||
{
|
||||
conf = JsonConvert.DeserializeObject<TelepathyConfig>(File.ReadAllText("TelepathyConfig.json"));
|
||||
}
|
||||
|
||||
NoDelay = conf.NoDelay;
|
||||
SendTimeout = conf.SendTimeout;
|
||||
ReceiveTimeout = conf.ReceiveTimeout;
|
||||
serverMaxMessageSize = conf.serverMaxMessageSize;
|
||||
serverMaxReceivesPerTick = conf.serverMaxReceivesPerTick;
|
||||
serverSendQueueLimitPerConnection = conf.serverSendQueueLimitPerConnection;
|
||||
serverReceiveQueueLimitPerConnection = conf.serverReceiveQueueLimitPerConnection;
|
||||
|
||||
// create client & server
|
||||
client = new Telepathy.Client(clientMaxMessageSize);
|
||||
server = new Telepathy.Server(serverMaxMessageSize);
|
||||
|
||||
// tell Telepathy to use Unity's Debug.Log
|
||||
Telepathy.Log.Info = Console.WriteLine;
|
||||
Telepathy.Log.Warning = Console.WriteLine;
|
||||
Telepathy.Log.Error = Console.WriteLine;
|
||||
|
||||
// client hooks
|
||||
// other systems hook into transport events in OnCreate or
|
||||
// OnStartRunning in no particular order. the only way to avoid
|
||||
// race conditions where telepathy uses OnConnected before another
|
||||
// system's hook (e.g. statistics OnData) was added is to wrap
|
||||
// them all in a lambda and always call the latest hook.
|
||||
// (= lazy call)
|
||||
client.OnConnected = () => OnClientConnected.Invoke();
|
||||
client.OnData = (segment) => OnClientDataReceived.Invoke(segment, 0);
|
||||
client.OnDisconnected = () => OnClientDisconnected.Invoke();
|
||||
|
||||
// client configuration
|
||||
client.NoDelay = NoDelay;
|
||||
client.SendTimeout = SendTimeout;
|
||||
client.ReceiveTimeout = ReceiveTimeout;
|
||||
client.SendQueueLimit = clientSendQueueLimit;
|
||||
client.ReceiveQueueLimit = clientReceiveQueueLimit;
|
||||
|
||||
// server hooks
|
||||
// other systems hook into transport events in OnCreate or
|
||||
// OnStartRunning in no particular order. the only way to avoid
|
||||
// race conditions where telepathy uses OnConnected before another
|
||||
// system's hook (e.g. statistics OnData) was added is to wrap
|
||||
// them all in a lambda and always call the latest hook.
|
||||
// (= lazy call)
|
||||
server.OnConnected = (connectionId) => OnServerConnected.Invoke(connectionId);
|
||||
server.OnData = (connectionId, segment) => OnServerDataReceived.Invoke(connectionId, segment, 0);
|
||||
server.OnDisconnected = (connectionId) => OnServerDisconnected.Invoke(connectionId);
|
||||
|
||||
// server configuration
|
||||
server.NoDelay = NoDelay;
|
||||
server.SendTimeout = SendTimeout;
|
||||
server.ReceiveTimeout = ReceiveTimeout;
|
||||
server.SendQueueLimit = serverSendQueueLimitPerConnection;
|
||||
server.ReceiveQueueLimit = serverReceiveQueueLimitPerConnection;
|
||||
|
||||
// allocate enabled check only once
|
||||
enabledCheck = () => true;
|
||||
|
||||
Console.WriteLine("TelepathyTransport initialized!");
|
||||
}
|
||||
|
||||
public override bool Available()
|
||||
{
|
||||
// C#'s built in TCP sockets run everywhere except on WebGL
|
||||
return true;
|
||||
}
|
||||
|
||||
// client
|
||||
public override bool ClientConnected() => client.Connected;
|
||||
public override void ClientConnect(string address) { }
|
||||
public override void ClientConnect(Uri uri) { }
|
||||
public override void ClientSend(int channelId, ArraySegment<byte> segment) => client.Send(segment);
|
||||
public override void ClientDisconnect() => client.Disconnect();
|
||||
// messages should always be processed in early update
|
||||
|
||||
// server
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
return builder.Uri;
|
||||
}
|
||||
public override bool ServerActive() => server.Active;
|
||||
public override void ServerStart(ushort requestedPort) => server.Start(requestedPort);
|
||||
public override void ServerSend(int connectionId, int channelId, ArraySegment<byte> segment) => server.Send(connectionId, segment);
|
||||
public override bool ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return server.GetClientAddress(connectionId);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// using server.listener.LocalEndpoint causes an Exception
|
||||
// in UWP + Unity 2019:
|
||||
// Exception thrown at 0x00007FF9755DA388 in UWF.exe:
|
||||
// Microsoft C++ exception: Il2CppExceptionWrapper at memory
|
||||
// location 0x000000E15A0FCDD0. SocketException: An address
|
||||
// incompatible with the requested protocol was used at
|
||||
// System.Net.Sockets.Socket.get_LocalEndPoint ()
|
||||
// so let's at least catch it and recover
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
public override void ServerStop() => server.Stop();
|
||||
// messages should always be processed in early update
|
||||
public void LateUpdate()
|
||||
{
|
||||
// note: we need to check enabled in case we set it to false
|
||||
// when LateUpdate already started.
|
||||
// (https://github.com/vis2k/Mirror/pull/379)
|
||||
|
||||
// process a maximum amount of server messages per tick
|
||||
// IMPORTANT: check .enabled to stop processing immediately after a
|
||||
// scene change message arrives!
|
||||
server.Tick(serverMaxReceivesPerTick, enabledCheck);
|
||||
}
|
||||
|
||||
// common
|
||||
public override void Shutdown()
|
||||
{
|
||||
Console.WriteLine("TelepathyTransport Shutdown()");
|
||||
client.Disconnect();
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
public override int GetMaxPacketSize(int channelId)
|
||||
{
|
||||
return serverMaxMessageSize;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Telepathy";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ LightmapSettings:
|
|||
m_EnableBakedLightmaps: 0
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
serializedVersion: 10
|
||||
m_Resolution: 2
|
||||
m_BakeResolution: 40
|
||||
m_AtlasSize: 1024
|
||||
|
|
@ -62,7 +62,6 @@ LightmapSettings:
|
|||
m_AOMaxDistance: 1
|
||||
m_CompAOExponent: 1
|
||||
m_CompAOExponentDirect: 0
|
||||
m_ExtractAmbientOcclusion: 0
|
||||
m_Padding: 2
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_LightmapsBakeMode: 1
|
||||
|
|
@ -77,16 +76,10 @@ LightmapSettings:
|
|||
m_PVRDirectSampleCount: 32
|
||||
m_PVRSampleCount: 500
|
||||
m_PVRBounces: 2
|
||||
m_PVREnvironmentSampleCount: 500
|
||||
m_PVREnvironmentReferencePointCount: 2048
|
||||
m_PVRFilteringMode: 2
|
||||
m_PVRDenoiserTypeDirect: 0
|
||||
m_PVRDenoiserTypeIndirect: 0
|
||||
m_PVRDenoiserTypeAO: 0
|
||||
m_PVRFilterTypeDirect: 0
|
||||
m_PVRFilterTypeIndirect: 0
|
||||
m_PVRFilterTypeAO: 0
|
||||
m_PVREnvironmentMIS: 0
|
||||
m_PVRFilteringMode: 1
|
||||
m_PVRCulling: 1
|
||||
m_PVRFilteringGaussRadiusDirect: 1
|
||||
m_PVRFilteringGaussRadiusIndirect: 5
|
||||
|
|
@ -94,9 +87,7 @@ LightmapSettings:
|
|||
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||
m_ExportTrainingData: 0
|
||||
m_TrainingDataDestination: TrainingData
|
||||
m_LightProbeSampleCountMultiplier: 4
|
||||
m_ShowResolutionOverlay: 1
|
||||
m_LightingDataAsset: {fileID: 0}
|
||||
m_UseShadowmask: 1
|
||||
--- !u!196 &4
|
||||
|
|
@ -150,10 +141,9 @@ Camera:
|
|||
m_ClearFlags: 1
|
||||
m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0}
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_GateFitMode: 2
|
||||
m_FocalLength: 50
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
|
|
@ -195,136 +185,6 @@ Transform:
|
|||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 45, y: 180, z: 0}
|
||||
--- !u!1 &171810013
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 171810014}
|
||||
- component: {fileID: 171810017}
|
||||
- component: {fileID: 171810016}
|
||||
- component: {fileID: 171810015}
|
||||
m_Layer: 5
|
||||
m_Name: Button
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &171810014
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 171810013}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 2007463783}
|
||||
m_Father: {fileID: 1933302372}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 160, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &171810015
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 171810013}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Navigation:
|
||||
m_Mode: 3
|
||||
m_SelectOnUp: {fileID: 0}
|
||||
m_SelectOnDown: {fileID: 0}
|
||||
m_SelectOnLeft: {fileID: 0}
|
||||
m_SelectOnRight: {fileID: 0}
|
||||
m_Transition: 1
|
||||
m_Colors:
|
||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
||||
m_ColorMultiplier: 1
|
||||
m_FadeDuration: 0.1
|
||||
m_SpriteState:
|
||||
m_HighlightedSprite: {fileID: 0}
|
||||
m_PressedSprite: {fileID: 0}
|
||||
m_SelectedSprite: {fileID: 0}
|
||||
m_DisabledSprite: {fileID: 0}
|
||||
m_AnimationTriggers:
|
||||
m_NormalTrigger: Normal
|
||||
m_HighlightedTrigger: Highlighted
|
||||
m_PressedTrigger: Pressed
|
||||
m_SelectedTrigger: Selected
|
||||
m_DisabledTrigger: Disabled
|
||||
m_Interactable: 1
|
||||
m_TargetGraphic: {fileID: 171810016}
|
||||
m_OnClick:
|
||||
m_PersistentCalls:
|
||||
m_Calls:
|
||||
- m_Target: {fileID: 1282001523}
|
||||
m_MethodName: RequestServerList
|
||||
m_Mode: 1
|
||||
m_Arguments:
|
||||
m_ObjectArgument: {fileID: 0}
|
||||
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
|
||||
m_IntArgument: 0
|
||||
m_FloatArgument: 0
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!114 &171810016
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 171810013}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!222 &171810017
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 171810013}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &251893064
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -411,59 +271,6 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &769288735
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 769288737}
|
||||
- component: {fileID: 769288736}
|
||||
m_Layer: 0
|
||||
m_Name: Puncher
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &769288736
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 769288735}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Port: 1111
|
||||
NoDelay: 1
|
||||
Interval: 10
|
||||
FastResend: 2
|
||||
CongestionWindow: 0
|
||||
SendWindowSize: 4096
|
||||
ReceiveWindowSize: 4096
|
||||
debugLog: 1
|
||||
statisticsGUI: 1
|
||||
statisticsLog: 0
|
||||
--- !u!4 &769288737
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 769288735}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1282001518}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1107091652
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -497,7 +304,6 @@ MeshRenderer:
|
|||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RenderingLayerMask: 4294967295
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
|
|
@ -509,7 +315,6 @@ MeshRenderer:
|
|||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 1
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
|
|
@ -532,9 +337,9 @@ MeshCollider:
|
|||
m_Material: {fileID: 0}
|
||||
m_IsTrigger: 0
|
||||
m_Enabled: 1
|
||||
serializedVersion: 4
|
||||
serializedVersion: 3
|
||||
m_Convex: 0
|
||||
m_CookingOptions: 30
|
||||
m_CookingOptions: 14
|
||||
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!33 &1107091655
|
||||
MeshFilter:
|
||||
|
|
@ -558,72 +363,6 @@ Transform:
|
|||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1143775647
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1143775650}
|
||||
- component: {fileID: 1143775649}
|
||||
- component: {fileID: 1143775648}
|
||||
m_Layer: 0
|
||||
m_Name: EventSystem
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1143775648
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1143775647}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalAxis: Horizontal
|
||||
m_VerticalAxis: Vertical
|
||||
m_SubmitButton: Submit
|
||||
m_CancelButton: Cancel
|
||||
m_InputActionsPerSecond: 10
|
||||
m_RepeatDelay: 0.5
|
||||
m_ForceModuleActive: 0
|
||||
--- !u!114 &1143775649
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1143775647}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_FirstSelected: {fileID: 0}
|
||||
m_sendNavigationEvents: 1
|
||||
m_DragThreshold: 10
|
||||
--- !u!4 &1143775650
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1143775647}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 9
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1282001517
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -636,8 +375,6 @@ GameObject:
|
|||
- component: {fileID: 1282001520}
|
||||
- component: {fileID: 1282001519}
|
||||
- component: {fileID: 1282001521}
|
||||
- component: {fileID: 1282001523}
|
||||
- component: {fileID: 1282001522}
|
||||
m_Layer: 0
|
||||
m_Name: NetworkManager
|
||||
m_TagString: Untagged
|
||||
|
|
@ -655,8 +392,7 @@ Transform:
|
|||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 769288737}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 3
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
|
|
@ -688,15 +424,15 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
dontDestroyOnLoad: 1
|
||||
PersistNetworkManagerToOfflineScene: 0
|
||||
runInBackground: 1
|
||||
autoStartServerBuild: 1
|
||||
showDebugMessages: 0
|
||||
serverTickRate: 30
|
||||
serverBatching: 0
|
||||
serverBatchInterval: 0
|
||||
offlineScene:
|
||||
onlineScene:
|
||||
transport: {fileID: 1282001523}
|
||||
transport: {fileID: 1282001521}
|
||||
networkAddress: localhost
|
||||
maxConnections: 100
|
||||
disconnectInactiveConnections: 0
|
||||
|
|
@ -730,51 +466,6 @@ MonoBehaviour:
|
|||
debugLog: 0
|
||||
statisticsGUI: 0
|
||||
statisticsLog: 0
|
||||
--- !u!114 &1282001522
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1282001517}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9c4cbff877abc42448dd829920c6c233, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
directConnectTransport: {fileID: 769288736}
|
||||
showDebugLogs: 1
|
||||
--- !u!114 &1282001523
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1282001517}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7064b1b1d0671194baf55fa8d5f564d6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
clientToServerTransport: {fileID: 1282001521}
|
||||
serverIP: 172.105.109.117
|
||||
endpointServerPort: 8080
|
||||
heartBeatInterval: 3
|
||||
connectOnAwake: 1
|
||||
authenticationKey: Secret Auth Key
|
||||
diconnectedFromRelay:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
useNATPunch: 1
|
||||
NATPunchtroughPort: 7776
|
||||
serverName: My awesome server!
|
||||
extraServerData: Map 1
|
||||
maxServerPlayers: 10
|
||||
isPublicServer: 1
|
||||
serverListUpdated:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
serverId: -1
|
||||
--- !u!1 &1458789072
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -861,183 +552,6 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &1933302368
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1933302372}
|
||||
- component: {fileID: 1933302371}
|
||||
- component: {fileID: 1933302370}
|
||||
- component: {fileID: 1933302369}
|
||||
m_Layer: 5
|
||||
m_Name: Canvas
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1933302369
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1933302368}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_IgnoreReversedGraphics: 1
|
||||
m_BlockingObjects: 0
|
||||
m_BlockingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
--- !u!114 &1933302370
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1933302368}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_UiScaleMode: 0
|
||||
m_ReferencePixelsPerUnit: 100
|
||||
m_ScaleFactor: 1
|
||||
m_ReferenceResolution: {x: 800, y: 600}
|
||||
m_ScreenMatchMode: 0
|
||||
m_MatchWidthOrHeight: 0
|
||||
m_PhysicalUnit: 3
|
||||
m_FallbackScreenDPI: 96
|
||||
m_DefaultSpriteDPI: 96
|
||||
m_DynamicPixelsPerUnit: 1
|
||||
--- !u!223 &1933302371
|
||||
Canvas:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1933302368}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 3
|
||||
m_RenderMode: 0
|
||||
m_Camera: {fileID: 0}
|
||||
m_PlaneDistance: 100
|
||||
m_PixelPerfect: 0
|
||||
m_ReceivesEvents: 1
|
||||
m_OverrideSorting: 0
|
||||
m_OverridePixelPerfect: 0
|
||||
m_SortingBucketNormalizedSize: 0
|
||||
m_AdditionalShaderChannelsFlag: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingOrder: 0
|
||||
m_TargetDisplay: 0
|
||||
--- !u!224 &1933302372
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1933302368}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 0, y: 0, z: 0}
|
||||
m_Children:
|
||||
- {fileID: 171810014}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 8
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0, y: 0}
|
||||
--- !u!1 &2007463782
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2007463783}
|
||||
- component: {fileID: 2007463785}
|
||||
- component: {fileID: 2007463784}
|
||||
m_Layer: 5
|
||||
m_Name: Text
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2007463783
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2007463782}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 171810014}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2007463784
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2007463782}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_FontSize: 14
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 10
|
||||
m_MaxSize: 40
|
||||
m_Alignment: 4
|
||||
m_AlignByGeometry: 0
|
||||
m_RichText: 1
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: Button
|
||||
--- !u!222 &2007463785
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2007463782}
|
||||
m_CullTransparentMesh: 0
|
||||
--- !u!1 &2054208274
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -1063,14 +577,12 @@ Light:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2054208274}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 10
|
||||
serializedVersion: 8
|
||||
m_Type: 1
|
||||
m_Shape: 0
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Intensity: 1
|
||||
m_Range: 10
|
||||
m_SpotAngle: 30
|
||||
m_InnerSpotAngle: 21.80208
|
||||
m_CookieSize: 10
|
||||
m_Shadows:
|
||||
m_Type: 2
|
||||
|
|
@ -1080,24 +592,6 @@ Light:
|
|||
m_Bias: 0.05
|
||||
m_NormalBias: 0.4
|
||||
m_NearPlane: 0.2
|
||||
m_CullingMatrixOverride:
|
||||
e00: 1
|
||||
e01: 0
|
||||
e02: 0
|
||||
e03: 0
|
||||
e10: 0
|
||||
e11: 1
|
||||
e12: 0
|
||||
e13: 0
|
||||
e20: 0
|
||||
e21: 0
|
||||
e22: 1
|
||||
e23: 0
|
||||
e30: 0
|
||||
e31: 0
|
||||
e32: 0
|
||||
e33: 1
|
||||
m_UseCullingMatrixOverride: 0
|
||||
m_Cookie: {fileID: 0}
|
||||
m_DrawHalo: 0
|
||||
m_Flare: {fileID: 0}
|
||||
|
|
@ -1105,15 +599,12 @@ Light:
|
|||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingLayerMask: 1
|
||||
m_Lightmapping: 4
|
||||
m_LightShadowCasterMode: 0
|
||||
m_AreaSize: {x: 1, y: 1}
|
||||
m_BounceIntensity: 1
|
||||
m_ColorTemperature: 6570
|
||||
m_UseColorTemperature: 0
|
||||
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_UseBoundingSphereOverride: 0
|
||||
m_ShadowRadius: 0
|
||||
m_ShadowAngle: 0
|
||||
--- !u!4 &2054208276
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ PlayerSettings:
|
|||
androidBlitType: 0
|
||||
defaultIsNativeResolution: 1
|
||||
macRetinaSupport: 1
|
||||
runInBackground: 0
|
||||
runInBackground: 1
|
||||
captureSingleScreen: 0
|
||||
muteOtherAudioSources: 0
|
||||
Prepare IOS For Recording: 0
|
||||
|
|
@ -163,7 +163,7 @@ PlayerSettings:
|
|||
useHDRDisplay: 0
|
||||
D3DHDRBitDepth: 0
|
||||
m_ColorGamuts: 00000000
|
||||
targetPixelDensity: 0
|
||||
targetPixelDensity: 30
|
||||
resolutionScalingMode: 0
|
||||
androidSupportedAspectRatio: 1
|
||||
androidMaxAspectRatio: 2.1
|
||||
|
|
@ -185,10 +185,10 @@ PlayerSettings:
|
|||
StripUnusedMeshComponents: 0
|
||||
VertexChannelCompressionMask: 4054
|
||||
iPhoneSdkVersion: 988
|
||||
iOSTargetOSVersionString:
|
||||
iOSTargetOSVersionString: 10.0
|
||||
tvOSSdkVersion: 0
|
||||
tvOSRequireExtendedGameController: 0
|
||||
tvOSTargetOSVersionString:
|
||||
tvOSTargetOSVersionString: 10.0
|
||||
uIPrerenderedIcon: 0
|
||||
uIRequiresPersistentWiFi: 0
|
||||
uIRequiresFullScreen: 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue