diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/Config.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/Config.cs new file mode 100644 index 0000000..c49964b --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/Config.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LightReflectiveMirror +{ + class Config + { + public string TransportDLL = "SimpleWebSocketTransportCompiled.dll"; + public string TransportClass = "Mirror.SimpleWeb.SimpleWebTransport"; + public string AuthenticationKey = "Secret Auth Key"; + public int UpdateLoopTime = 50; + public int UpdateHeartbeatInterval = 20; + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/DataHandler.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/DataHandler.cs new file mode 100644 index 0000000..3efa752 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/DataHandler.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LightReflectiveMirror +{ + public static class DataHandler + { + public static void WriteByte(this byte[] data, ref int position, byte value) + { + data[position] = value; + position += 1; + } + + public static byte ReadByte(this byte[] data, ref int position) + { + byte value = data[position]; + position += 1; + return value; + } + + public static void WriteBool(this byte[] data, ref int position, bool value) + { + unsafe + { + fixed(byte* dataPtr = &data[position]) + { + bool* valuePtr = (bool*)dataPtr; + *valuePtr = value; + position += 1; + } + } + } + + public static bool ReadBool(this byte[] data, ref int position) + { + bool value = BitConverter.ToBoolean(data, position); + position += 1; + return value; + } + + public static void WriteString(this byte[] data, ref int position, string value) + { + data.WriteInt(ref position, value.Length); + for (int i = 0; i < value.Length; i++) + data.WriteChar(ref position, value[i]); + } + + public static string ReadString(this byte[] data, ref int position) + { + string value = default; + + int stringSize = data.ReadInt(ref position); + + for (int i = 0; i < stringSize; i++) + value += data.ReadChar(ref position); + + return value; + } + + public static void WriteBytes(this byte[] data, ref int position, byte[] value) + { + data.WriteInt(ref position, value.Length); + for (int i = 0; i < value.Length; i++) + data.WriteByte(ref position, value[i]); + } + + public static byte[] ReadBytes(this byte[] data, ref int position) + { + int byteSize = data.ReadInt(ref position); + + byte[] value = new byte[byteSize]; + + for (int i = 0; i < byteSize; i++) + value[i] = data.ReadByte(ref position); + + return value; + } + + public static void WriteChar(this byte[] data, ref int position, char value) + { + unsafe + { + fixed (byte* dataPtr = &data[position]) + { + char* valuePtr = (char*)dataPtr; + *valuePtr = value; + position += 2; + } + } + } + + public static char ReadChar(this byte[] data, ref int position) + { + char value = BitConverter.ToChar(data, position); + position += 2; + return value; + } + + public static void WriteInt(this byte[] data, ref int position, int value) + { + unsafe + { + fixed (byte* dataPtr = &data[position]) + { + int* valuePtr = (int*)dataPtr; + *valuePtr = value; + position += 4; + } + } + } + + public static int ReadInt(this byte[] data, ref int position) + { + int value = BitConverter.ToInt32(data, position); + position += 4; + return value; + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/Program.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/Program.cs new file mode 100644 index 0000000..166470c --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/Program.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Mirror; +using Newtonsoft.Json; + +namespace LightReflectiveMirror +{ + class Program + { + public static Config conf; + RelayHandler relay; + public static Transport transport; + MethodInfo awakeMethod; + MethodInfo startMethod; + MethodInfo updateMethod; + MethodInfo lateUpdateMethod; + List _currentConnections = new List(); + int _currentHeartbeatTimer = 0; + + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + + if (!File.Exists("config.json")) + { + File.WriteAllText("config.json", JsonConvert.SerializeObject(new Config(), Formatting.Indented)); + WriteLogMessage("A config.json file was generated. Please configure it to the proper settings and re-run!", ConsoleColor.Yellow); + Console.ReadKey(); + Environment.Exit(0); + } + else + { + conf = JsonConvert.DeserializeObject(File.ReadAllText("config.json")); + try + { + Console.WriteLine(Directory.GetCurrentDirectory()); + var asm = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + conf.TransportDLL); + WriteLogMessage($"Loaded Assembly: {asm.FullName}", ConsoleColor.Green); + transport = (Transport)asm.CreateInstance(conf.TransportClass); + + if (transport != null) + { + WriteLogMessage($"Loaded Transport: {asm.GetType(conf.TransportClass).Name}! Loading Methods...", ConsoleColor.Green); + CheckMethods(asm.GetType(conf.TransportClass)); + + if (awakeMethod != null) + { + awakeMethod.Invoke(transport, null); + WriteLogMessage("Called Awake on transport.", ConsoleColor.Yellow); + } + + if (startMethod != null) + { + awakeMethod.Invoke(transport, null); + WriteLogMessage("Called Start on transport.", ConsoleColor.Yellow); + } + + WriteLogMessage("Starting Transport...", ConsoleColor.Green); + + transport.OnServerError = (clientID, error) => { + WriteLogMessage($"Transport Error, Client: {clientID}, Error: {error}", ConsoleColor.Red); + }; + + transport.OnServerConnected = (clientID) => + { + WriteLogMessage($"Transport Connected, Client: {clientID}", ConsoleColor.Cyan); + _currentConnections.Add(clientID); + relay.ClientConnected(clientID); + }; + + relay = new RelayHandler(transport.GetMaxPacketSize(0)); + + transport.OnServerDataReceived = relay.HandleMessage; + transport.OnServerDisconnected = (clientID) => + { + _currentConnections.Remove(clientID); + relay.HandleDisconnect(clientID); + }; + + transport.ServerStart(); + + WriteLogMessage("Transport Started!", ConsoleColor.Green); + } + else + { + WriteLogMessage("Transport Class not found! Please make sure to include namespaces.", ConsoleColor.Red); + Console.ReadKey(); + Environment.Exit(0); + } + } + catch(Exception e) + { + WriteLogMessage("Exception: " + e, ConsoleColor.Red); + Console.ReadKey(); + Environment.Exit(0); + } + } + + while (true) + { + if (updateMethod != null) + updateMethod.Invoke(transport, null); + + if (lateUpdateMethod != null) + lateUpdateMethod.Invoke(transport, null); + + _currentHeartbeatTimer++; + + if(_currentHeartbeatTimer >= conf.UpdateHeartbeatInterval) + { + _currentHeartbeatTimer = 0; + + for(int i = 0; i < _currentConnections.Count; i++) + { + transport.ServerSend(_currentConnections[i], 0, new ArraySegment(new byte[] { 200 })); + } + } + + await Task.Delay(conf.UpdateLoopTime); + } + } + + static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White) + { + Console.ForegroundColor = color; + Console.WriteLine(message); + } + + void CheckMethods(Type type) + { + awakeMethod = type.GetMethod("Awake", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + startMethod = type.GetMethod("Start", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + updateMethod = type.GetMethod("Update", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + lateUpdateMethod = type.GetMethod("LateUpdate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (awakeMethod != null) + WriteLogMessage("'Awake' Loaded!", ConsoleColor.Yellow); + + if (startMethod != null) + WriteLogMessage("'Start' Loaded!", ConsoleColor.Yellow); + + if (updateMethod != null) + WriteLogMessage("'Update' Loaded!", ConsoleColor.Yellow); + + if (lateUpdateMethod != null) + WriteLogMessage("'LateUpdate' Loaded!", ConsoleColor.Yellow); + } + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/RelayHandler.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/RelayHandler.cs new file mode 100644 index 0000000..80b2a01 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/RelayHandler.cs @@ -0,0 +1,307 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace LightReflectiveMirror +{ + public class RelayHandler + { + List rooms = new List(); + List pendingAuthentication = new List(); + ArrayPool sendBuffers; + int maxPacketSize = 0; + + public RelayHandler(int maxPacketSize) + { + this.maxPacketSize = maxPacketSize; + sendBuffers = ArrayPool.Create(maxPacketSize, 50); + } + + public void ClientConnected(int clientId) + { + pendingAuthentication.Add(clientId); + var buffer = sendBuffers.Rent(1); + int pos = 0; + buffer.WriteByte(ref pos, (byte)OpCodes.AuthenticationRequest); + Program.transport.ServerSend(clientId, 0, new ArraySegment(buffer, 0, pos)); + sendBuffers.Return(buffer); + } + + public void HandleMessage(int clientId, ArraySegment segmentData, int channel) + { + try + { + var data = segmentData.Array; + int pos = 0; + + OpCodes opcode = (OpCodes)data.ReadByte(ref pos); + + if (pendingAuthentication.Contains(clientId)) + { + if (opcode == OpCodes.AuthenticationResponse) + { + string authResponse = data.ReadString(ref pos); + if (authResponse == Program.conf.AuthenticationKey) + { + pendingAuthentication.Remove(clientId); + int writePos = 0; + var sendBuffer = sendBuffers.Rent(1); + sendBuffer.WriteByte(ref writePos, (byte)OpCodes.Authenticated); + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendBuffer, 0, writePos)); + } + } + return; + } + + switch (opcode) + { + case OpCodes.CreateRoom: + CreateRoom(clientId, data.ReadInt(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadString(ref pos)); + break; + case OpCodes.RequestID: + SendClientID(clientId); + break; + case OpCodes.LeaveRoom: + LeaveRoom(clientId); + break; + case OpCodes.JoinServer: + JoinRoom(clientId, data.ReadInt(ref pos)); + break; + case OpCodes.KickPlayer: + LeaveRoom(data.ReadInt(ref pos), clientId); + break; + case OpCodes.SendData: + ProcessData(clientId, data.ReadBytes(ref pos), channel, data.ReadInt(ref pos)); + break; + case OpCodes.RequestServers: + SendServerList(clientId); + break; + case OpCodes.UpdateRoomData: + var room = GetRoomForPlayer(clientId); + if (room == null) + return; + + var plyRoom = room.Value; + + bool newName = data.ReadBool(ref pos); + if (newName) + plyRoom.serverName = data.ReadString(ref pos); + + bool newData = data.ReadBool(ref pos); + if (newData) + plyRoom.serverData = data.ReadString(ref pos); + + bool newPublicStatus = data.ReadBool(ref pos); + if (newPublicStatus) + plyRoom.isPublic = data.ReadBool(ref pos); + + bool newPlayerCap = data.ReadBool(ref pos); + if (newPlayerCap) + plyRoom.maxPlayers = data.ReadInt(ref pos); + + break; + } + } + catch + { + // Do Nothing. Client probably sent some invalid data. + } + } + + public void HandleDisconnect(int clientId) + { + LeaveRoom(clientId); + } + + void SendServerList(int clientId) + { + int pos = 0; + var buffer = sendBuffers.Rent(500); + buffer.WriteByte(ref pos, (byte)OpCodes.ServerListReponse); + for(int i = 0; i < rooms.Count; i++) + { + if (rooms[i].isPublic) + { + buffer.WriteBool(ref pos, true); + buffer.WriteString(ref pos, rooms[i].serverName); + buffer.WriteString(ref pos, rooms[i].serverData); + buffer.WriteInt(ref pos, rooms[i].hostId); + buffer.WriteInt(ref pos, rooms[i].maxPlayers); + buffer.WriteInt(ref pos, rooms[i].clients.Count + 1); + } + } + buffer.WriteBool(ref pos, false); + Program.transport.ServerSend(clientId, 0, new ArraySegment(buffer, 0, pos)); + sendBuffers.Return(buffer); + } + + void ProcessData(int clientId, byte[] clientData, int channel, int sendTo = -1) + { + Room? playersRoom = GetRoomForPlayer(clientId); + + if(playersRoom != null) + { + Room room = playersRoom.Value; + + if(room.hostId == clientId) + { + if (room.clients.Contains(sendTo)) + { + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(maxPacketSize); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData); + sendBuffer.WriteBytes(ref pos, clientData); + + Program.transport.ServerSend(sendTo, channel, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + } + else + { + // We are not the host, so send the data to the host. + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(maxPacketSize); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetData); + sendBuffer.WriteBytes(ref pos, clientData); + sendBuffer.WriteInt(ref pos, clientId); + + Program.transport.ServerSend(room.hostId, channel, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + } + } + + Room? GetRoomForPlayer(int clientId) + { + for(int i = 0; i < rooms.Count; i++) + { + if (rooms[i].hostId == clientId) + return rooms[i]; + + if (rooms[i].clients.Contains(clientId)) + return rooms[i]; + } + + return null; + } + + void JoinRoom(int clientId, int serverId) + { + LeaveRoom(clientId); + + for(int i = 0; i < rooms.Count; i++) + { + if(rooms[i].hostId == serverId) + { + if(rooms[i].clients.Count < rooms[i].maxPlayers) + { + rooms[i].clients.Add(clientId); + + int sendJoinPos = 0; + byte[] sendJoinBuffer = sendBuffers.Rent(5); + + sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.ServerJoined); + sendJoinBuffer.WriteInt(ref sendJoinPos, clientId); + + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendJoinBuffer, 0, sendJoinPos)); + Program.transport.ServerSend(serverId, 0, new ArraySegment(sendJoinBuffer, 0, sendJoinPos)); + sendBuffers.Return(sendJoinBuffer); + return; + } + } + } + + // If it got to here, then the server was not found, or full. Tell the client. + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(1); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft); + + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + + void CreateRoom(int clientId, int maxPlayers, string serverName, bool isPublic, string serverData) + { + LeaveRoom(clientId); + + Room room = new Room(); + room.hostId = clientId; + room.maxPlayers = maxPlayers; + room.serverName = serverName; + room.isPublic = isPublic; + room.serverData = serverData; + room.clients = new List(); + + rooms.Add(room); + + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(5); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.RoomCreated); + sendBuffer.WriteInt(ref pos, clientId); + + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + + void LeaveRoom(int clientId, int requiredHostId = -1) + { + for(int i = 0; i < rooms.Count; i++) + { + if(rooms[i].hostId == clientId) + { + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(1); + sendBuffer.WriteByte(ref pos, (byte)OpCodes.ServerLeft); + + for(int x = 0; x < rooms[i].clients.Count; x++) + Program.transport.ServerSend(rooms[i].clients[x], 0, new ArraySegment(sendBuffer, 0, pos)); + + sendBuffers.Return(sendBuffer); + rooms[i].clients.Clear(); + rooms.RemoveAt(i); + return; + } + else + { + if (requiredHostId >= 0 && rooms[i].hostId != requiredHostId) + continue; + + if(rooms[i].clients.RemoveAll(x => x == clientId) > 0) + { + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(5); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.PlayerDisconnected); + sendBuffer.WriteInt(ref pos, clientId); + + Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + } + } + } + + void SendClientID(int clientId) + { + int pos = 0; + byte[] sendBuffer = sendBuffers.Rent(5); + + sendBuffer.WriteByte(ref pos, (byte)OpCodes.GetID); + sendBuffer.WriteInt(ref pos, clientId); + + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendBuffer, 0, pos)); + sendBuffers.Return(sendBuffer); + } + } + + public enum OpCodes + { + Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10, + LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, RequestServers = 15, ServerListReponse = 16, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19 + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/Room.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/Room.cs new file mode 100644 index 0000000..d77ae68 --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/Room.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LightReflectiveMirror +{ + struct Room + { + public int hostId; + public string serverName; + public string serverData; + public bool isPublic; + public int maxPlayers; + public List clients; + } +} diff --git a/ServerProject-DONT-IMPORT-INTO-UNITY/Transport.cs b/ServerProject-DONT-IMPORT-INTO-UNITY/Transport.cs new file mode 100644 index 0000000..abcd0ec --- /dev/null +++ b/ServerProject-DONT-IMPORT-INTO-UNITY/Transport.cs @@ -0,0 +1,244 @@ +using System; + +namespace Mirror +{ + /// + /// Abstract transport layer component + /// + /// + ///

+ /// Transport Rules + ///

+ /// + /// + /// All transports should follow these rules so that they work correctly with mirror + /// + /// + /// When Monobehaviour is disabled the Transport should not invoke callbacks + /// + /// + /// Callbacks should be invoked on main thread. It is best to do this from LateUpdate + /// + /// + /// Callbacks can be invoked after or as been called + /// + /// + /// or can be called by mirror multiple times + /// + /// + /// should check the platform and 32 vs 64 bit if the transport only works on some of them + /// + /// + /// should return size even if transport is not running + /// + /// + /// Default channel should be reliable + /// + /// + ///
+ public abstract class Transport + { + /// + /// The current transport used by Mirror. + /// + public static Transport activeTransport; + + /// + /// Is this transport available in the current platform? + /// Some transports might only be available in mobile + /// Many will not work in webgl + /// Example usage: return Application.platform == RuntimePlatform.WebGLPlayer + /// + /// True if this transport works in the current platform + public abstract bool Available(); + + #region Client + /// + /// Notify subscribers when when this client establish a successful connection to the server + /// callback() + /// + public Action OnClientConnected = () => Console.WriteLine("OnClientConnected called with no handler"); + + /// + /// Notify subscribers when this client receive data from the server + /// callback(ArraySegment<byte> data, int channel) + /// + public Action, int> OnClientDataReceived = (data, channel) => Console.WriteLine("OnClientDataReceived called with no handler"); + + /// + /// Notify subscribers when this client encounters an error communicating with the server + /// callback(Exception e) + /// + public Action OnClientError = (error) => Console.WriteLine("OnClientError called with no handler"); + + /// + /// Notify subscribers when this client disconnects from the server + /// callback() + /// + public Action OnClientDisconnected = () => Console.WriteLine("OnClientDisconnected called with no handler"); + + /// + /// Determines if we are currently connected to the server + /// + /// True if a connection has been established to the server + public abstract bool ClientConnected(); + + /// + /// Establish a connection to a server + /// + /// The IP address or FQDN of the server we are trying to connect to + public abstract void ClientConnect(string address); + + /// + /// Establish a connection to a server + /// + /// The address of the server we are trying to connect to + public virtual void ClientConnect(Uri uri) + { + // By default, to keep backwards compatibility, just connect to the host + // in the uri + ClientConnect(uri.Host); + } + + /// + /// Send data to the server + /// + /// The channel to use. 0 is the default channel, + /// but some transports might want to provide unreliable, encrypted, compressed, or any other feature + /// as new channels + /// The data to send to the server. Will be recycled after returning, so either use it directly or copy it internally. This allows for allocation-free sends! + public abstract void ClientSend(int channelId, ArraySegment segment); + + /// + /// Disconnect this client from the server + /// + public abstract void ClientDisconnect(); + + #endregion + + #region Server + + + /// + /// Retrieves the address of this server. + /// Useful for network discovery + /// + /// the url at which this server can be reached + public abstract Uri ServerUri(); + + /// + /// Notify subscribers when a client connects to this server + /// callback(int connId) + /// + public Action OnServerConnected = (connId) => Console.WriteLine("OnServerConnected called with no handler"); + + /// + /// Notify subscribers when this server receives data from the client + /// callback(int connId, ArraySegment<byte> data, int channel) + /// + public Action, int> OnServerDataReceived = (connId, data, channel) => Console.WriteLine("OnServerDataReceived called with no handler"); + + /// + /// Notify subscribers when this server has some problem communicating with the client + /// callback(int connId, Exception e) + /// + public Action OnServerError = (connId, error) => Console.WriteLine("OnServerError called with no handler"); + + /// + /// Notify subscribers when a client disconnects from this server + /// callback(int connId) + /// + public Action OnServerDisconnected = (connId) => Console.WriteLine("OnServerDisconnected called with no handler"); + + /// + /// Determines if the server is up and running + /// + /// true if the transport is ready for connections from clients + public abstract bool ServerActive(); + + /// + /// Start listening for clients + /// + public abstract void ServerStart(); + + /// + /// Send data to a client. + /// + /// The client connection id to send the data to + /// The channel to be used. Transports can use channels to implement + /// other features such as unreliable, encryption, compression, etc... + /// + public abstract void ServerSend(int connectionId, int channelId, ArraySegment segment); + + /// + /// Disconnect a client from this server. Useful to kick people out. + /// + /// the id of the client to disconnect + /// true if the client was kicked + public abstract bool ServerDisconnect(int connectionId); + + /// + /// Get the client address + /// + /// id of the client + /// address of the client + public abstract string ServerGetClientAddress(int connectionId); + + /// + /// Stop listening for clients and disconnect all existing clients + /// + public abstract void ServerStop(); + + #endregion + + /// + /// The maximum packet size for a given channel. Unreliable transports + /// usually can only deliver small packets. Reliable fragmented channels + /// can usually deliver large ones. + /// + /// GetMaxPacketSize needs to return a value at all times. Even if the + /// Transport isn't running, or isn't Available(). This is because + /// Fallback and Multiplex transports need to find the smallest possible + /// packet size at runtime. + /// + /// channel id + /// the size in bytes that can be sent via the provided channel + public abstract int GetMaxPacketSize(int channelId = 0); + + /// + /// Shut down the transport, both as client and server + /// + public abstract void Shutdown(); + + // block Update() to force Transports to use LateUpdate to avoid race + // conditions. messages should be processed after all the game state + // was processed in Update. + // -> in other words: use LateUpdate! + // -> uMMORPG 480 CCU stress test: when bot machine stops, it causes + // 'Observer not ready for ...' log messages when using Update + // -> occupying a public Update() function will cause Warnings if a + // transport uses Update. + // + // IMPORTANT: set script execution order to >1000 to call Transport's + // LateUpdate after all others. Fixes race condition where + // e.g. in uSurvival Transport would apply Cmds before + // ShoulderRotation.LateUpdate, resulting in projectile + // spawns at the point before shoulder rotation. +#pragma warning disable UNT0001 // Empty Unity message + public void Update() { } +#pragma warning restore UNT0001 // Empty Unity message + + /// + /// called when quitting the application by closing the window / pressing stop in the editor + /// virtual so that inheriting classes' OnApplicationQuit() can call base.OnApplicationQuit() too + /// + public virtual void OnApplicationQuit() + { + // stop transport (e.g. to shut down threads) + // (when pressing Stop in the Editor, Unity keeps threads alive + // until we press Start again. so if Transports use threads, we + // really want them to end now and not after next start) + Shutdown(); + } + } +} \ No newline at end of file diff --git a/UnityTransport/BiDictionary.cs b/UnityTransport/BiDictionary.cs new file mode 100644 index 0000000..1bdd4b8 --- /dev/null +++ b/UnityTransport/BiDictionary.cs @@ -0,0 +1,51 @@ +// +// Source: https://stackoverflow.com/questions/255341/getting-multiple-keys-of-specified-value-of-a-generic-dictionary#255630 +// +using System; +using System.Collections.Generic; + +namespace LightReflectiveMirror +{ + class BiDictionary + { + IDictionary firstToSecond = new Dictionary(); + IDictionary secondToFirst = new Dictionary(); + + public void Add(TFirst first, TSecond second) + { + if (firstToSecond.ContainsKey(first) || + secondToFirst.ContainsKey(second)) + { + throw new ArgumentException("Duplicate first or second"); + } + firstToSecond.Add(first, second); + secondToFirst.Add(second, first); + } + + public bool TryGetByFirst(TFirst first, out TSecond second) + { + return firstToSecond.TryGetValue(first, out second); + } + + public void Remove(TFirst first) + { + secondToFirst.Remove(firstToSecond[first]); + firstToSecond.Remove(first); + } + + public bool TryGetBySecond(TSecond second, out TFirst first) + { + return secondToFirst.TryGetValue(second, out first); + } + + public TSecond GetByFirst(TFirst first) + { + return firstToSecond[first]; + } + + public TFirst GetBySecond(TSecond second) + { + return secondToFirst[second]; + } + } +} \ No newline at end of file diff --git a/UnityTransport/LRMTools.cs b/UnityTransport/LRMTools.cs new file mode 100644 index 0000000..08802eb --- /dev/null +++ b/UnityTransport/LRMTools.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace LightReflectiveMirror +{ + public static class LRMTools + { + public static void WriteByte(this byte[] data, ref int position, byte value) + { + data[position] = value; + position += 1; + } + + public static byte ReadByte(this byte[] data, ref int position) + { + byte value = data[position]; + position += 1; + return value; + } + + public static void WriteBool(this byte[] data, ref int position, bool value) + { + unsafe + { + fixed (byte* dataPtr = &data[position]) + { + bool* valuePtr = (bool*)dataPtr; + *valuePtr = value; + position += 1; + } + } + } + + public static bool ReadBool(this byte[] data, ref int position) + { + bool value = BitConverter.ToBoolean(data, position); + position += 1; + return value; + } + + public static void WriteString(this byte[] data, ref int position, string value) + { + data.WriteInt(ref position, value.Length); + for (int i = 0; i < value.Length; i++) + data.WriteChar(ref position, value[i]); + } + + public static string ReadString(this byte[] data, ref int position) + { + string value = default; + + int stringSize = data.ReadInt(ref position); + + for (int i = 0; i < stringSize; i++) + value += data.ReadChar(ref position); + + return value; + } + + public static void WriteBytes(this byte[] data, ref int position, byte[] value) + { + data.WriteInt(ref position, value.Length); + for (int i = 0; i < value.Length; i++) + data.WriteByte(ref position, value[i]); + } + + public static byte[] ReadBytes(this byte[] data, ref int position) + { + int byteSize = data.ReadInt(ref position); + + byte[] value = new byte[byteSize]; + + for (int i = 0; i < byteSize; i++) + value[i] = data.ReadByte(ref position); + + return value; + } + + public static void WriteChar(this byte[] data, ref int position, char value) + { + unsafe + { + fixed (byte* dataPtr = &data[position]) + { + char* valuePtr = (char*)dataPtr; + *valuePtr = value; + position += 2; + } + } + } + + public static char ReadChar(this byte[] data, ref int position) + { + char value = BitConverter.ToChar(data, position); + position += 2; + return value; + } + + public static void WriteInt(this byte[] data, ref int position, int value) + { + unsafe + { + fixed (byte* dataPtr = &data[position]) + { + int* valuePtr = (int*)dataPtr; + *valuePtr = value; + position += 4; + } + } + } + + public static int ReadInt(this byte[] data, ref int position) + { + int value = BitConverter.ToInt32(data, position); + position += 4; + return value; + } + } +} \ No newline at end of file diff --git a/UnityTransport/LightReflectiveMirrorTransport.cs b/UnityTransport/LightReflectiveMirrorTransport.cs new file mode 100644 index 0000000..53d2f21 --- /dev/null +++ b/UnityTransport/LightReflectiveMirrorTransport.cs @@ -0,0 +1,371 @@ +using Mirror; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Events; + +namespace LightReflectiveMirror +{ + public class LightReflectiveMirrorTransport : Transport + { + [Header("Connection Variables")] + public Transport clientToServerTransport; + public string serverIP; + public float heartBeatInterval = 3; + public bool connectOnAwake = true; + public string authenticationKey = "Secret Auth Key"; + [Header("Server Hosting Data")] + public string serverName = "My awesome server!"; + public string extraServerData = "Map 1"; + public int maxServerPlayers = 10; + public bool isPublicServer = true; + [Header("Server List")] + public UnityEvent serverListUpdated; + public List relayServerList { private set; get; } = new List(); + [Header("Server Information")] + public int serverId = -1; + + private byte[] _clientSendBuffer; + private bool _connectedToRelay = false; + private bool _isClient = false; + private bool _isServer = false; + private bool _isAuthenticated = false; + private int _currentMemberId; + private BiDictionary _connectedRelayClients = new BiDictionary(); + public bool IsAuthenticated() => _isAuthenticated; + + private void Awake() + { + if (clientToServerTransport is LightReflectiveMirrorTransport) + { + throw new Exception("Haha real funny... Use a different transport."); + } + + if (connectOnAwake) + ConnectToRelay(); + + InvokeRepeating(nameof(SendHeartbeat), heartBeatInterval, heartBeatInterval); + } + + public void ConnectToRelay() + { + if (!_connectedToRelay) + { + clientToServerTransport.OnClientConnected = ConnectedToRelay; + clientToServerTransport.OnClientDataReceived = DataReceived; + _clientSendBuffer = new byte[clientToServerTransport.GetMaxPacketSize()]; + + clientToServerTransport.ClientConnect(serverIP); + } + } + + void SendHeartbeat() + { + if (_connectedToRelay) + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, 200); + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + } + + void ConnectedToRelay() + { + _connectedToRelay = true; + } + + public void RequestServerList() + { + if (_isAuthenticated) + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.RequestServers); + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + } + + void DataReceived(ArraySegment segmentData, int channel) + { + try + { + var data = segmentData.Array; + int pos = 0; + + OpCodes opcode = (OpCodes)data.ReadByte(ref pos); + + switch (opcode) + { + case OpCodes.Authenticated: + _isAuthenticated = true; + break; + case OpCodes.AuthenticationRequest: + SendAuthKey(); + break; + case OpCodes.GetData: + var recvData = data.ReadBytes(ref pos); + + if (_isServer) + OnServerDataReceived?.Invoke(_connectedRelayClients.GetByFirst(data.ReadInt(ref pos)), new ArraySegment(recvData), channel); + + if (_isClient) + OnClientDataReceived?.Invoke(new ArraySegment(recvData), channel); + break; + case OpCodes.ServerLeft: + if (_isClient) + { + _isClient = false; + OnClientDisconnected?.Invoke(); + } + break; + case OpCodes.PlayerDisconnected: + if (_isServer) + { + int user = data.ReadInt(ref pos); + OnServerDisconnected?.Invoke(_connectedRelayClients.GetByFirst(user)); + _connectedRelayClients.Remove(user); + } + break; + case OpCodes.RoomCreated: + serverId = data.ReadInt(ref pos); + break; + case OpCodes.ServerJoined: + int clientId = data.ReadInt(ref pos); + if (_isClient) + { + OnClientConnected?.Invoke(); + } + if (_isServer) + { + _connectedRelayClients.Add(clientId, _currentMemberId); + OnServerConnected?.Invoke(_currentMemberId); + _currentMemberId++; + } + break; + case OpCodes.ServerListReponse: + relayServerList.Clear(); + while(data.ReadBool(ref pos)) + { + relayServerList.Add(new RelayServerInfo() + { + serverName = data.ReadString(ref pos), + serverData = data.ReadString(ref pos), + serverId = data.ReadInt(ref pos), + maxPlayers = data.ReadInt(ref pos), + currentPlayers = data.ReadInt(ref pos) + }); + } + serverListUpdated?.Invoke(); + break; + } + } + catch { } + } + + public void UpdateRoomInfo(string newServerName = null, string newServerData = null, bool? newServerIsPublic = null, int? newPlayerCap = null) + { + if (_isServer) + { + int pos = 0; + if (!string.IsNullOrEmpty(newServerName)) + { + _clientSendBuffer.WriteBool(ref pos, true); + _clientSendBuffer.WriteString(ref pos,newServerName); + } + + if (!string.IsNullOrEmpty(newServerData)) + { + _clientSendBuffer.WriteBool(ref pos, true); + _clientSendBuffer.WriteString(ref pos, newServerData); + } + + if (newServerIsPublic != null) + { + _clientSendBuffer.WriteBool(ref pos, true); + _clientSendBuffer.WriteBool(ref pos, newServerIsPublic.Value); + } + + if (newPlayerCap != null) + { + _clientSendBuffer.WriteBool(ref pos, true); + _clientSendBuffer.WriteInt(ref pos, newPlayerCap.Value); + } + + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + } + + void SendAuthKey() + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.AuthenticationResponse); + _clientSendBuffer.WriteString(ref pos, authenticationKey); + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override bool Available() => _connectedToRelay; + + public override void ClientConnect(string address) + { + int hostId = 0; + if (!Available() || !int.TryParse(address, out hostId)) + { + Debug.Log("Not connected to relay or invalid server id!"); + OnClientDisconnected?.Invoke(); + return; + } + + if (_isClient || _isServer) + { + throw new Exception("Cannot connect while hosting/already connected!"); + } + + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.JoinServer); + _clientSendBuffer.WriteInt(ref pos, hostId); + + _isClient = true; + + clientToServerTransport.ClientSend(0, new System.ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override void ClientConnect(Uri uri) + { + ClientConnect(uri.Host); + } + + public override bool ClientConnected() => _isClient; + + public override void ClientDisconnect() + { + _isClient = false; + + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom); + + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override void ClientSend(int channelId, ArraySegment segment) + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); + _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); + _clientSendBuffer.WriteInt(ref pos, 0); + + clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override int GetMaxPacketSize(int channelId = 0) + { + return clientToServerTransport.GetMaxPacketSize(channelId); + } + + public override bool ServerActive() => _isServer; + + public override bool ServerDisconnect(int connectionId) + { + int relayId; + + if(_connectedRelayClients.TryGetBySecond(connectionId, out relayId)) + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.KickPlayer); + _clientSendBuffer.WriteInt(ref pos, relayId); + return true; + } + + return false; + } + + public override string ServerGetClientAddress(int connectionId) + { + return _connectedRelayClients.GetBySecond(connectionId).ToString(); + } + + public override void ServerSend(int connectionId, int channelId, ArraySegment segment) + { + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.SendData); + _clientSendBuffer.WriteBytes(ref pos, segment.Array.Take(segment.Count).ToArray()); + _clientSendBuffer.WriteInt(ref pos, _connectedRelayClients.GetBySecond(connectionId)); + + clientToServerTransport.ClientSend(channelId, new ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override void ServerStart() + { + if (!Available()) + { + Debug.Log("Not connected to relay! Server failed to start."); + return; + } + + if(_isClient || _isServer) + { + Debug.Log("Cannot host while already hosting or connected!"); + return; + } + + _isServer = true; + _connectedRelayClients = new BiDictionary(); + _currentMemberId = 1; + + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.CreateRoom); + _clientSendBuffer.WriteInt(ref pos, maxServerPlayers); + _clientSendBuffer.WriteString(ref pos, serverName); + _clientSendBuffer.WriteBool(ref pos, isPublicServer); + _clientSendBuffer.WriteString(ref pos, extraServerData); + + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + + public override void ServerStop() + { + if (_isServer) + { + _isServer = false; + int pos = 0; + _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.LeaveRoom); + + clientToServerTransport.ClientSend(0, new ArraySegment(_clientSendBuffer, 0, pos)); + } + } + + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder(); + builder.Scheme = "LRM"; + builder.Host = serverId.ToString(); + return builder.Uri; + } + + public override void Shutdown() + { + _isAuthenticated = false; + _isClient = false; + _isServer = false; + _connectedToRelay = false; + clientToServerTransport.Shutdown(); + } + + + public enum OpCodes + { + Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10, + LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, RequestServers = 15, ServerListReponse = 16, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19 + } + } + + [Serializable] + public struct RelayServerInfo + { + public string serverName; + public int currentPlayers; + public int maxPlayers; + public int serverId; + public string serverData; + } +} \ No newline at end of file