From 432b6d5270bc8e2c086af94ee60282916603117c Mon Sep 17 00:00:00 2001 From: cxxpxr <60411087+cxxpxr@users.noreply.github.com> Date: Sat, 3 Apr 2021 22:47:16 -0400 Subject: [PATCH] Add files via upload --- BiDictionary.cs | 51 ++++++++ Compression.cs | 60 +++++++++ Config.cs | 20 +++ DataHandler.cs | 120 +++++++++++++++++ Endpoint.cs | 101 ++++++++++++++ Program.cs | 295 +++++++++++++++++++++++++++++++++++++++++ RelayHandler.cs | 340 ++++++++++++++++++++++++++++++++++++++++++++++++ Room.cs | 28 ++++ Transport.cs | 244 ++++++++++++++++++++++++++++++++++ 9 files changed, 1259 insertions(+) create mode 100644 BiDictionary.cs create mode 100644 Compression.cs create mode 100644 Config.cs create mode 100644 DataHandler.cs create mode 100644 Endpoint.cs create mode 100644 Program.cs create mode 100644 RelayHandler.cs create mode 100644 Room.cs create mode 100644 Transport.cs 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