diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Config.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Config.cs index 145f5c1..8e5b6ae 100644 --- a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Config.cs +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Config.cs @@ -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; diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Program/Program.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Program/Program.cs index a7a9152..da55ff6 100644 --- a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Program/Program.cs +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Program/Program.cs @@ -104,7 +104,7 @@ namespace LightReflectiveMirror _pendingNATPunches.Remove(clientID); }; - transport.ServerStart(); + transport.ServerStart(conf.TransportPort); WriteLogMessage("OK", ConsoleColor.Green); diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Transport.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Transport.cs index abcd0ec..7e259f2 100644 --- a/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Transport.cs +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/LRM/Transport.cs @@ -159,7 +159,7 @@ namespace Mirror /// /// Start listening for clients /// - public abstract void ServerStart(); + public abstract void ServerStart(ushort port); /// /// Send data to a client. diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Class1.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Class1.cs deleted file mode 100644 index 20978f2..0000000 --- a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace MultiCompiled -{ - public class Class1 - { - } -} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/MultiCompiled.csproj b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/MultiCompiled.csproj index f208d30..4b9a9ca 100644 --- a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/MultiCompiled.csproj +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/MultiCompiled.csproj @@ -4,4 +4,12 @@ net5.0 + + + + + + + + diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Client.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Client.cs new file mode 100644 index 0000000..73e775c --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Client.cs @@ -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> 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 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 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 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; + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Common.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Common.cs new file mode 100644 index 0000000..15265f9 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Common.cs @@ -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; + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ConnectionState.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ConnectionState.cs new file mode 100644 index 0000000..cdfe3c0 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ConnectionState.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/EventType.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/EventType.cs new file mode 100644 index 0000000..66bc3b4 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/EventType.cs @@ -0,0 +1,9 @@ +namespace Telepathy +{ + public enum EventType + { + Connected, + Data, + Disconnected + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/LICENSE b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/LICENSE new file mode 100644 index 0000000..680deef --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/LICENSE @@ -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. diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Log.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Log.cs new file mode 100644 index 0000000..2d50aa3 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Log.cs @@ -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 Info = Console.WriteLine; + public static Action Warning = Console.WriteLine; + public static Action Error = Console.Error.WriteLine; + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Logger.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Logger.cs new file mode 100644 index 0000000..4f7722a --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Logger.cs @@ -0,0 +1 @@ +// removed 2021-02-04 \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentReceivePipe.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentReceivePipe.cs new file mode 100644 index 0000000..2e10318 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentReceivePipe.cs @@ -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 data; + public Entry(int connectionId, EventType eventType, ArraySegment data) + { + this.connectionId = connectionId; + this.eventType = eventType; + this.data = data; + } + } + + // message queue + // ConcurrentQueue allocates. lock{} instead. + // + // IMPORTANT: lock{} all usages! + readonly Queue queue = new Queue(); + + // 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 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 queueCounter = new Dictionary(); + + // constructor + public MagnificentReceivePipe(int MaxMessageSize) + { + // initialize pool to create max message sized byte[]s each time + pool = new Pool(() => 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 message) + { + // pool & queue usage always needs to be locked + lock (this) + { + // does this message have a data array content? + ArraySegment 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(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 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(); + } + } + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentSendPipe.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentSendPipe.cs new file mode 100644 index 0000000..be456a0 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/MagnificentSendPipe.cs @@ -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> queue = new Queue>(); + + // 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 pool; + + // constructor + public MagnificentSendPipe(int MaxMessageSize) + { + // initialize pool to create max message sized byte[]s each time + pool = new Pool(() => 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 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 segment = new ArraySegment(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 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 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); + } + } + } + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Message.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Message.cs new file mode 100644 index 0000000..4f7722a --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Message.cs @@ -0,0 +1 @@ +// removed 2021-02-04 \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/NetworkStreamExtensions.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/NetworkStreamExtensions.cs new file mode 100644 index 0000000..bda4f9e --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/NetworkStreamExtensions.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Pool.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Pool.cs new file mode 100644 index 0000000..4ec4fd2 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Pool.cs @@ -0,0 +1,34 @@ +// pool to avoid allocations. originally from libuv2k. +using System; +using System.Collections.Generic; + +namespace Telepathy +{ + public class Pool + { + // objects + readonly Stack objects = new Stack(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + // constructor + public Pool(Func 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; + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/SafeQueue.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/SafeQueue.cs new file mode 100644 index 0000000..7899911 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/SafeQueue.cs @@ -0,0 +1 @@ +// removed 2021-02-04 diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Server.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Server.cs new file mode 100644 index 0000000..d044b50 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Server.cs @@ -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 OnConnected; + public Action> OnData; + public Action 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 + readonly ConcurrentDictionary clients = new ConcurrentDictionary(); + + // 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 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 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 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 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; + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadExtensions.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadExtensions.cs new file mode 100644 index 0000000..85dece4 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadExtensions.cs @@ -0,0 +1 @@ +// removed 2021-02-04 diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadFunctions.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadFunctions.cs new file mode 100644 index 0000000..6f026c9 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/ThreadFunctions.cs @@ -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 message structure + // this function is blocking sometimes! + // (e.g. if someone has high latency or wire was cut off) + // -> payload is of multiple < 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 , 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 message = new ArraySegment(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(); + } + } + } +} \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Utils.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Utils.cs new file mode 100644 index 0000000..8f04fe9 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/Utils.cs @@ -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]; + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/VERSION b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/VERSION new file mode 100644 index 0000000..b70eef2 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/Telepathy/VERSION @@ -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 \ No newline at end of file diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyConfig.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyConfig.cs new file mode 100644 index 0000000..ef4c29b --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyConfig.cs @@ -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; + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyTransport.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyTransport.cs new file mode 100644 index 0000000..ed46425 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/MultiCompiled/Telepathy/TelepathyTransport.cs @@ -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 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(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 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 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"; + } + } +} diff --git a/UnityProject/Assets/Mirror/Examples/Tanks/Scenes/Scene.unity b/UnityProject/Assets/Mirror/Examples/Tanks/Scenes/Scene.unity index cbbe18d..804c241 100644 --- a/UnityProject/Assets/Mirror/Examples/Tanks/Scenes/Scene.unity +++ b/UnityProject/Assets/Mirror/Examples/Tanks/Scenes/Scene.unity @@ -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 diff --git a/UnityProject/ProjectSettings/ProjectSettings.asset b/UnityProject/ProjectSettings/ProjectSettings.asset index 63e9fad..fd89bc2 100644 --- a/UnityProject/ProjectSettings/ProjectSettings.asset +++ b/UnityProject/ProjectSettings/ProjectSettings.asset @@ -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