diff --git a/BiDictionary.cs b/BiDictionary.cs new file mode 100644 index 0000000..1bdd4b8 --- /dev/null +++ b/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/Compression.cs b/Compression.cs new file mode 100644 index 0000000..ce43d6b --- /dev/null +++ b/Compression.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace LightReflectiveMirror.Compression +{ + internal static class StringCompressor + { + /// + /// Compresses the string. + /// + /// The text. + /// + public static string Compress(this string text) + { + byte[] buffer = Encoding.UTF8.GetBytes(text); + var memoryStream = new MemoryStream(); + using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, true)) + { + gZipStream.Write(buffer, 0, buffer.Length); + } + + memoryStream.Position = 0; + + var compressedData = new byte[memoryStream.Length]; + memoryStream.Read(compressedData, 0, compressedData.Length); + + var gZipBuffer = new byte[compressedData.Length + 4]; + Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length); + Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4); + return Convert.ToBase64String(gZipBuffer); + } + + /// + /// Decompresses the string. + /// + /// The compressed text. + /// + public static string Decompress(this string compressedText) + { + byte[] gZipBuffer = Convert.FromBase64String(compressedText); + using (var memoryStream = new MemoryStream()) + { + int dataLength = BitConverter.ToInt32(gZipBuffer, 0); + memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4); + + var buffer = new byte[dataLength]; + + memoryStream.Position = 0; + using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + { + gZipStream.Read(buffer, 0, buffer.Length); + } + + return Encoding.UTF8.GetString(buffer); + } + } + } +} diff --git a/Config.cs b/Config.cs new file mode 100644 index 0000000..6e07a41 --- /dev/null +++ b/Config.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LightReflectiveMirror +{ + class Config + { + public string TransportDLL = "MultiCompiled.dll"; + public string TransportClass = "Mirror.SimpleWebTransport"; + public string AuthenticationKey = "Secret Auth Key"; + public int UpdateLoopTime = 10; + public int UpdateHeartbeatInterval = 100; + public bool UseEndpoint = true; + public ushort EndpointPort = 8080; + public bool EndpointServerList = true; + public bool EnableNATPunchtroughServer = true; + public ushort NATPunchtroughPort = 7776; + } +} diff --git a/DataHandler.cs b/DataHandler.cs new file mode 100644 index 0000000..3efa752 --- /dev/null +++ b/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/Endpoint.cs b/Endpoint.cs new file mode 100644 index 0000000..e434d53 --- /dev/null +++ b/Endpoint.cs @@ -0,0 +1,101 @@ +using Grapevine; +using LightReflectiveMirror.Compression; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LightReflectiveMirror.Endpoints +{ + [Serializable] + struct RelayStats + { + public int ConnectedClients; + public int RoomCount; + public int PublicRoomCount; + public TimeSpan Uptime; + } + + [RestResource] + public class Endpoint + { + private List _rooms { get => Program.instance.GetRooms(); } + + private RelayStats _stats { get => new RelayStats + { + ConnectedClients = Program.instance.GetConnections(), + RoomCount = Program.instance.GetRooms().Count, + PublicRoomCount = Program.instance.GetPublicRoomCount(), + Uptime = Program.instance.GetUptime() + }; } + + [RestRoute("Get", "/api/stats")] + public async Task Stats(IHttpContext context) + { + string json = JsonConvert.SerializeObject(_stats, Formatting.Indented); + await context.Response.SendResponseAsync(json); + } + + [RestRoute("Get", "/api/servers")] + public async Task ServerList(IHttpContext context) + { + if (Program.conf.EndpointServerList) + { + string json = JsonConvert.SerializeObject(_rooms, Formatting.Indented); + await context.Response.SendResponseAsync(json); + } + else + await context.Response.SendResponseAsync(HttpStatusCode.Forbidden); + } + + [RestRoute("Get", "/api/compressed/servers")] + public async Task ServerListCompressed(IHttpContext context) + { + if (Program.conf.EndpointServerList) + { + string json = JsonConvert.SerializeObject(_rooms); + await context.Response.SendResponseAsync(json.Compress()); + } + else + await context.Response.SendResponseAsync(HttpStatusCode.Forbidden); + } + } + + public class EndpointServer + { + public bool Start(ushort port = 8080) + { + try + { + var config = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); + + var server = new RestServerBuilder(new ServiceCollection(), config, + (services) => + { + services.AddLogging(configure => configure.AddConsole()); + services.Configure(options => options.MinLevel = LogLevel.None); + }, (server) => + { + server.Prefixes.Add($"http://*:{port}/"); + }).Build(); + + server.Router.Options.SendExceptionMessages = false; + server.Start(); + + return true; + } + catch + { + return false; + } + + } + } +} + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..66c26e4 --- /dev/null +++ b/Program.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using LightReflectiveMirror.Endpoints; +using Mirror; +using Newtonsoft.Json; + +namespace LightReflectiveMirror +{ + class Program + { + public static Transport transport; + public static Program instance; + public static Config conf; + + private RelayHandler _relay; + private MethodInfo _awakeMethod; + private MethodInfo _startMethod; + private MethodInfo _updateMethod; + private MethodInfo _lateUpdateMethod; + + private DateTime _startupTime; + + private List _currentConnections = new List(); + public Dictionary NATConnections = new Dictionary(); + private BiDictionary _pendingNATPunches = new BiDictionary(); + private int _currentHeartbeatTimer = 0; + + private byte[] _NATRequest = new byte[500]; + private int _NATRequestPosition = 0; + + private UdpClient _punchServer; + + private const string CONFIG_PATH = "config.json"; + + public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public int GetConnections() => _currentConnections.Count; + public TimeSpan GetUptime() => DateTime.Now - _startupTime; + public int GetPublicRoomCount() => _relay.rooms.Where(x => x.isPublic).Count(); + public List GetRooms() => _relay.rooms; + + public async Task MainAsync() + { + WriteTitle(); + instance = this; + _startupTime = DateTime.Now; + + if (!File.Exists(CONFIG_PATH)) + { + File.WriteAllText(CONFIG_PATH, 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_PATH)); + WriteLogMessage("Loading Assembly... ", ConsoleColor.White, true); + try + { + var asm = Assembly.LoadFile(Directory.GetCurrentDirectory() + @"\" + conf.TransportDLL); + WriteLogMessage($"OK", ConsoleColor.Green); + + WriteLogMessage("\nLoading Transport Class... ", ConsoleColor.White, true); + + transport = asm.CreateInstance(conf.TransportClass) as Transport; + + if (transport != null) + { + var transportClass = asm.GetType(conf.TransportClass); + WriteLogMessage("OK", ConsoleColor.Green); + + WriteLogMessage("\nLoading Transport Methods... ", ConsoleColor.White, true); + CheckMethods(transportClass); + WriteLogMessage("OK", ConsoleColor.Green); + + WriteLogMessage("\nInvoking Transport Methods..."); + + if (_awakeMethod != null) + _awakeMethod.Invoke(transport, null); + + if (_startMethod != null) + _startMethod.Invoke(transport, null); + + WriteLogMessage("\nStarting Transport... ", ConsoleColor.White, true); + + 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); + + if (conf.EnableNATPunchtroughServer) + { + string natID = Guid.NewGuid().ToString(); + _pendingNATPunches.Add(clientID, natID); + _NATRequestPosition = 0; + _NATRequest.WriteByte(ref _NATRequestPosition, (byte)OpCodes.RequestNATConnection); + _NATRequest.WriteString(ref _NATRequestPosition, natID); + transport.ServerSend(clientID, 0, new ArraySegment(_NATRequest, 0, _NATRequestPosition)); + } + }; + + _relay = new RelayHandler(transport.GetMaxPacketSize(0)); + + transport.OnServerDataReceived = _relay.HandleMessage; + transport.OnServerDisconnected = (clientID) => + { + _currentConnections.Remove(clientID); + _relay.HandleDisconnect(clientID); + + if(NATConnections.ContainsKey(clientID)) + NATConnections.Remove(clientID); + + if(_pendingNATPunches.TryGetByFirst(clientID, out _)) + _pendingNATPunches.Remove(clientID); + }; + + transport.ServerStart(); + + WriteLogMessage("OK", ConsoleColor.Green); + + if (conf.UseEndpoint) + { + WriteLogMessage("\nStarting Endpoint Service... ", ConsoleColor.White, true); + var endpoint = new EndpointServer(); + + if (endpoint.Start(conf.EndpointPort)) + { + WriteLogMessage("OK", ConsoleColor.Green); + } + else + { + WriteLogMessage("FAILED\nPlease run as administrator or check if port is in use.", ConsoleColor.DarkRed); + } + } + + if (conf.EnableNATPunchtroughServer) + { + WriteLogMessage("\nStarting NatPunchthrough Socket... ", ConsoleColor.White, true); + + try + { + _punchServer = new UdpClient(conf.NATPunchtroughPort); + + WriteLogMessage("OK\n", ConsoleColor.Green, true); + + WriteLogMessage("\nStarting NatPunchthrough Thread... ", ConsoleColor.White, true); + var natThread = new Thread(new ThreadStart(RunNATPunchLoop)); + + try + { + natThread.Start(); + } + catch(Exception e) + { + WriteLogMessage("FAILED\n" + e, ConsoleColor.DarkRed); + } + } + catch(Exception e) + { + WriteLogMessage("FAILED\nCheck if port is in use.", ConsoleColor.DarkRed, true); + Console.WriteLine(e); + } + } + } + else + { + WriteLogMessage("FAILED\nClass not found, make sure to included namespaces!", ConsoleColor.DarkRed); + Console.ReadKey(); + Environment.Exit(0); + } + } + catch(Exception e) + { + WriteLogMessage("FAILED\nException: " + e, ConsoleColor.DarkRed); + 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 })); + + GC.Collect(); + } + + await Task.Delay(conf.UpdateLoopTime); + } + } + + void RunNATPunchLoop() + { + WriteLogMessage("OK\n", ConsoleColor.Green); + IPEndPoint remoteEndpoint = new IPEndPoint(IPAddress.Any, conf.NATPunchtroughPort); + + // Stock Data server sends to everyone: + var serverResponse = new byte[1] { 1 }; + + byte[] readData; + bool isConnectionEstablishment; + int pos; + string connectionID; + + while (true) + { + readData = _punchServer.Receive(ref remoteEndpoint); + pos = 0; + try + { + isConnectionEstablishment = readData.ReadBool(ref pos); + + if (isConnectionEstablishment) + { + connectionID = readData.ReadString(ref pos); + + if (_pendingNATPunches.TryGetBySecond(connectionID, out pos)) + { + NATConnections.Add(pos, new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port)); + _pendingNATPunches.Remove(pos); + Console.WriteLine("Client Successfully Established Puncher Connection. " + remoteEndpoint.ToString()); + } + } + + _punchServer.Send(serverResponse, 1, remoteEndpoint); + } + catch + { + // ignore, packet got fucked up or something. + } + } + } + + static void WriteLogMessage(string message, ConsoleColor color = ConsoleColor.White, bool oneLine = false) + { + Console.ForegroundColor = color; + if (oneLine) + Console.Write(message); + else + 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); + } + + void WriteTitle() + { + string t = @" + w c(..)o ( + _ _____ __ __ \__(-) __) + | | | __ \ | \/ | /\ ( + | | | |__) || \ / | /(_)___) + | | | _ / | |\/| | w /| + | |____ | | \ \ | | | | | \ + |______||_| \_\|_| |_| m m copyright monkesoft 2021 + +"; + + string load = $"Chimp Event Listener Initializing... OK" + + "\nHarambe Memorial Initializing... OK" + + "\nBananas Initializing... OK\n"; + + WriteLogMessage(t, ConsoleColor.Green); + WriteLogMessage(load, ConsoleColor.Cyan); + } + } +} diff --git a/RelayHandler.cs b/RelayHandler.cs new file mode 100644 index 0000000..b216194 --- /dev/null +++ b/RelayHandler.cs @@ -0,0 +1,340 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace LightReflectiveMirror +{ + public class RelayHandler + { + public List rooms = new List(); + private List _pendingAuthentication = new List(); + private ArrayPool _sendBuffers; + private 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 = segmentData.Offset; + + 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), data.ReadBool(ref pos), data.ReadString(ref pos), data.ReadBool(ref pos), data.ReadInt(ref pos)); + break; + case OpCodes.RequestID: + SendClientID(clientId); + break; + case OpCodes.LeaveRoom: + LeaveRoom(clientId); + break; + case OpCodes.JoinServer: + JoinRoom(clientId, data.ReadInt(ref pos), data.ReadBool(ref pos), data.ReadString(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.UpdateRoomData: + var plyRoom = GetRoomForPlayer(clientId); + + if (plyRoom == null) + return; + + 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 ProcessData(int clientId, byte[] clientData, int channel, int sendTo = -1) + { + Room playersRoom = GetRoomForPlayer(clientId); + + if(playersRoom != null) + { + Room room = playersRoom; + + 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, bool canDirectConnect, string localIP) + { + LeaveRoom(clientId); + + for(int i = 0; i < rooms.Count; i++) + { + if(rooms[i].serverId == serverId) + { + if(rooms[i].clients.Count < rooms[i].maxPlayers) + { + rooms[i].clients.Add(clientId); + + int sendJoinPos = 0; + byte[] sendJoinBuffer = _sendBuffers.Rent(500); + + if (canDirectConnect && Program.instance.NATConnections.ContainsKey(clientId)) + { + sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP); + + if (Program.instance.NATConnections[clientId].Address.Equals(rooms[i].hostIP.Address)) + sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostLocalIP == localIP ? "127.0.0.1" : rooms[i].hostLocalIP); + else + sendJoinBuffer.WriteString(ref sendJoinPos, rooms[i].hostIP.Address.ToString()); + + sendJoinBuffer.WriteInt(ref sendJoinPos, rooms[i].useNATPunch ? rooms[i].hostIP.Port : rooms[i].port); + + Program.transport.ServerSend(clientId, 0, new ArraySegment(sendJoinBuffer, 0, sendJoinPos)); + + sendJoinPos = 0; + sendJoinBuffer.WriteByte(ref sendJoinPos, (byte)OpCodes.DirectConnectIP); + Console.WriteLine(Program.instance.NATConnections[clientId].Address.ToString()); + sendJoinBuffer.WriteString(ref sendJoinPos, Program.instance.NATConnections[clientId].Address.ToString()); + sendJoinBuffer.WriteInt(ref sendJoinPos, Program.instance.NATConnections[clientId].Port); + + Program.transport.ServerSend(rooms[i].hostId, 0, new ArraySegment(sendJoinBuffer, 0, sendJoinPos)); + + _sendBuffers.Return(sendJoinBuffer); + + return; + } + else + { + + 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(rooms[i].hostId, 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, bool useDirectConnect, string hostLocalIP, bool useNatPunch, int port) + { + LeaveRoom(clientId); + + IPEndPoint hostIP = null; + Program.instance.NATConnections.TryGetValue(clientId, out hostIP); + + Room room = new Room + { + hostId = clientId, + maxPlayers = maxPlayers, + serverName = serverName, + isPublic = isPublic, + serverData = serverData, + clients = new List(), + serverId = GetRandomServerID(), + hostIP = hostIP, + hostLocalIP = hostLocalIP, + supportsDirectConnect = hostIP == null ? false : useDirectConnect, + port = port, + useNATPunch = useNatPunch + }; + + 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); + } + + int GetRandomServerID() + { + Random rand = new Random(); + int temp = rand.Next(int.MinValue, int.MaxValue); + + while (DoesServerIdExist(temp)) + temp = rand.Next(int.MinValue, int.MaxValue); + + return temp; + } + + bool DoesServerIdExist(int id) + { + for (int i = 0; i < rooms.Count; i++) + if (rooms[i].serverId == id) + return true; + + return false; + } + } + + 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, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20, + DirectConnectIP = 21 + } +} diff --git a/Room.cs b/Room.cs new file mode 100644 index 0000000..e2d4973 --- /dev/null +++ b/Room.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net; + +namespace LightReflectiveMirror +{ + [JsonObject(MemberSerialization.OptOut)] + public class Room + { + public int serverId; + public int hostId; + public string serverName; + public string serverData; + public bool isPublic; + public int maxPlayers; + public List clients; + [JsonIgnore] + public bool supportsDirectConnect = false; + [JsonIgnore] + public IPEndPoint hostIP; + [JsonIgnore] + public string hostLocalIP; + [JsonIgnore] + public bool useNATPunch = false; + [JsonIgnore] + public int port; + } +} diff --git a/Transport.cs b/Transport.cs new file mode 100644 index 0000000..abcd0ec --- /dev/null +++ b/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