Merged master

This commit is contained in:
MrGadget1024 2023-11-27 09:39:22 -05:00
commit 79309c6a05
13 changed files with 221 additions and 70 deletions

View File

@ -0,0 +1,30 @@
using UnityEngine;
namespace Mirror
{
public class NetworkDiagnosticsDebugger : MonoBehaviour
{
public bool logInMessages = true;
public bool logOutMessages = true;
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
{
if (logInMessages)
Debug.Log(msgInfo);
}
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
{
if (logOutMessages)
Debug.Log(msgInfo);
}
void OnEnable()
{
NetworkDiagnostics.InMessageEvent += OnInMessage;
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
}
void OnDisable()
{
NetworkDiagnostics.InMessageEvent -= OnInMessage;
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bc9f0a0fe4124424b8f9d4927795ee01
timeCreated: 1700945893

View File

@ -1703,7 +1703,7 @@ public static void OnGUI()
// only if in world // only if in world
if (!ready) return; if (!ready) return;
GUILayout.BeginArea(new Rect(10, 5, 1000, 50)); GUILayout.BeginArea(new Rect(10, 5, 1020, 50));
GUILayout.BeginHorizontal("Box"); GUILayout.BeginHorizontal("Box");
GUILayout.Label("Snapshot Interp.:"); GUILayout.Label("Snapshot Interp.:");

View File

@ -9,6 +9,7 @@ namespace Mirror
{ {
public enum PlayerSpawnMethod { Random, RoundRobin } public enum PlayerSpawnMethod { Random, RoundRobin }
public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host }
public enum HeadlessStartOptions { DoNothing, AutoStartServer, AutoStartClient }
[DisallowMultipleComponent] [DisallowMultipleComponent]
[AddComponentMenu("Network/Network Manager")] [AddComponentMenu("Network/Network Manager")]
@ -29,18 +30,31 @@ public class NetworkManager : MonoBehaviour
/// <summary>Should the server auto-start when 'Server Build' is checked in build settings</summary> /// <summary>Should the server auto-start when 'Server Build' is checked in build settings</summary>
[Header("Headless Builds")] [Header("Headless Builds")]
[Tooltip("Should the server auto-start when 'Dedicated Server' platform is selected, or 'Server Build' is checked in build settings.")]
[FormerlySerializedAs("startOnHeadless")]
public bool autoStartServerBuild = true;
[Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")] [Tooltip("Choose whether Server or Client should auto-start in headless builds")]
public bool autoConnectClientBuild; public HeadlessStartOptions headlessStartMode = HeadlessStartOptions.DoNothing;
/// <summary>Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary> /// <summary>Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
[Tooltip("Server & Client send rate per second. Use 60-100Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] [Tooltip("Server & Client send rate per second. Use 60-100Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
[FormerlySerializedAs("serverTickRate")] [FormerlySerializedAs("serverTickRate")]
public int sendRate = 60; public int sendRate = 60;
// Deprecated 2023-11-25
// Using SerializeField and HideInInspector to self-correct for being
// replaced by headlessStartMode. This can be removed in the future.
// See OnValidate() for how we handle this.
[Obsolete("Deprecated - Use headlessStartMode instead.")]
[FormerlySerializedAs("autoStartServerBuild"), SerializeField, HideInInspector]
public bool autoStartServerBuild = true;
// Deprecated 2023-11-25
// Using SerializeField and HideInInspector to self-correct for being
// replaced by headlessStartMode. This can be removed in the future.
// See OnValidate() for how we handle this.
[Obsolete("Deprecated - Use headlessStartMode instead.")]
[FormerlySerializedAs("autoConnectClientBuild"), SerializeField, HideInInspector]
public bool autoConnectClientBuild;
// client send rate follows server send rate to avoid errors for now // client send rate follows server send rate to avoid errors for now
/// <summary>Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary> /// <summary>Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
// [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
@ -157,6 +171,24 @@ public class NetworkManager : MonoBehaviour
// virtual so that inheriting classes' OnValidate() can call base.OnValidate() too // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too
public virtual void OnValidate() public virtual void OnValidate()
{ {
#pragma warning disable 618
// autoStartServerBuild and autoConnectClientBuild are now obsolete, but to avoid
// a breaking change we'll set headlessStartMode to what the user had set before.
//
// headlessStartMode defaults to DoNothing, so if the user had neither of these
// set, then it will remain as DoNothing, and if they set headlessStartMode to
// any selection in the inspector it won't get changed back.
if (autoStartServerBuild)
headlessStartMode = HeadlessStartOptions.AutoStartServer;
else if (autoConnectClientBuild)
headlessStartMode = HeadlessStartOptions.AutoStartClient;
// Setting both to false here prevents this code from fighting with user
// selection in the inspector, and they're both SerialisedField's.
autoStartServerBuild = false;
autoConnectClientBuild = false;
#pragma warning restore 618
// always >= 0 // always >= 0
maxConnections = Mathf.Max(maxConnections, 0); maxConnections = Mathf.Max(maxConnections, 0);
@ -215,25 +247,24 @@ public virtual void Awake()
// virtual so that inheriting classes' Start() can call base.Start() too // virtual so that inheriting classes' Start() can call base.Start() too
public virtual void Start() public virtual void Start()
{ {
// headless mode? then start the server // Auto-start headless server or client.
// can't do this in Awake because Awake is for initialization.
// some transports might not be ready until Start.
// //
// (tick rate is applied in StartServer!) // We can't do this in Awake because Awake is for initialization
// and some transports might not be ready until Start.
// //
// don't auto start in editor where we have a UI, only in builds. // don't auto start in editor where we have a UI, only in builds.
// otherwise if we switch to 'Dedicated Server' target and press // otherwise if we switch to 'Dedicated Server' target and press
// Play, it would auto start the server every time. // Play, it would auto start the server every time.
if (Utils.IsHeadless() && !Application.isEditor) if (Utils.IsHeadless())
{ {
if (autoStartServerBuild) switch (headlessStartMode)
{ {
StartServer(); case HeadlessStartOptions.AutoStartServer:
} StartServer();
// only start server or client, never both break;
else if (autoConnectClientBuild) case HeadlessStartOptions.AutoStartClient:
{ StartClient();
StartClient(); break;
} }
} }
} }
@ -398,6 +429,8 @@ void SetupClient()
/// <summary>Starts the client, connects it to the server with networkAddress.</summary> /// <summary>Starts the client, connects it to the server with networkAddress.</summary>
public void StartClient() public void StartClient()
{ {
// Do checks and short circuits before setting anything up.
// If / when we retry, we won't have conflict issues.
if (NetworkClient.active) if (NetworkClient.active)
{ {
Debug.LogWarning("Client already started."); Debug.LogWarning("Client already started.");

View File

@ -378,6 +378,30 @@ async void BuildAndPushServer()
string imageName = _containerImageRepo; string imageName = _containerImageRepo;
string tag = _containerImageTag; string tag = _containerImageTag;
// MIRROR CHANGE ///////////////////////////////////////////////
// registry, repository and tag can not contain whitespaces.
// otherwise the docker command will throw an error:
// "ERROR: "docker buildx build" requires exactly 1 argument."
// catch this early and notify the user immediately.
if (registry.Contains(" "))
{
onError($"Container Registry is not allowed to contain whitespace: '{registry}'");
return;
}
if (imageName.Contains(" "))
{
onError($"Image Repository is not allowed to contain whitespace: '{imageName}'");
return;
}
if (tag.Contains(" "))
{
onError($"Tag is not allowed to contain whitespace: '{tag}'");
return;
}
// END MIRROR CHANGE ///////////////////////////////////////////
// increment tag for quicker iteration // increment tag for quicker iteration
if (_autoIncrementTag) if (_autoIncrementTag)
{ {
@ -615,9 +639,16 @@ void SyncObjectWithForm()
_appName = _appNameInput.value; _appName = _appNameInput.value;
_appVersionName = _appVersionNameInput.value; _appVersionName = _appVersionNameInput.value;
_containerRegistry = _containerRegistryInput.value; // MIRROR CHANGE ///////////////////////////////////////////////////
_containerImageTag = _containerImageTagInput.value; // registry, repository and tag can not contain whitespaces.
_containerImageRepo = _containerImageRepoInput.value; // otherwise it'll throw an error:
// "ERROR: "docker buildx build" requires exactly 1 argument."
// trim whitespace in case users accidentally added some.
_containerRegistry = _containerRegistryInput.value.Trim();
_containerImageTag = _containerImageTagInput.value.Trim();
_containerImageRepo = _containerImageRepoInput.value.Trim();
// END MIRROR CHANGE ///////////////////////////////////////////////
_autoIncrementTag = _autoIncrementTagInput.value; _autoIncrementTag = _autoIncrementTagInput.value;
} }

View File

@ -133,7 +133,7 @@ protected virtual void Awake()
if (statisticsLog) if (statisticsLog)
InvokeRepeating(nameof(OnLogStatistics), 1, 1); InvokeRepeating(nameof(OnLogStatistics), 1, 1);
Debug.Log("KcpTransport initialized!"); Log.Info("KcpTransport initialized!");
} }
protected virtual void OnValidate() protected virtual void OnValidate()
@ -344,7 +344,7 @@ protected virtual void OnLogStatistics()
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
log += $" SendBuffer: {GetTotalSendBuffer()}\n"; log += $" SendBuffer: {GetTotalSendBuffer()}\n";
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
Debug.Log(log); Log.Info(log);
} }
if (ClientConnected()) if (ClientConnected())
@ -356,7 +356,7 @@ protected virtual void OnLogStatistics()
log += $" ReceiveQueue: {client.ReceiveQueueCount}\n"; log += $" ReceiveQueue: {client.ReceiveQueueCount}\n";
log += $" SendBuffer: {client.SendBufferCount}\n"; log += $" SendBuffer: {client.SendBufferCount}\n";
log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n"; log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n";
Debug.Log(log); Log.Info(log);
} }
} }

View File

@ -122,7 +122,7 @@ protected override void Awake()
// it'll be used by the created thread immediately. // it'll be used by the created thread immediately.
base.Awake(); base.Awake();
Debug.Log("ThreadedKcpTransport initialized!"); Log.Info("ThreadedKcpTransport initialized!");
} }
protected virtual void OnValidate() protected virtual void OnValidate()
@ -300,7 +300,7 @@ protected virtual void OnLogStatistics()
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
log += $" SendBuffer: {GetTotalSendBuffer()}\n"; log += $" SendBuffer: {GetTotalSendBuffer()}\n";
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
Debug.Log(log); Log.Info(log);
} }
if (ClientConnected()) if (ClientConnected())
@ -312,7 +312,7 @@ protected virtual void OnLogStatistics()
log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n"; log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n";
log += $" SendBuffer: {client.peer.SendBufferCount}\n"; log += $" SendBuffer: {client.peer.SendBufferCount}\n";
log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n"; log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n";
Debug.Log(log); Log.Info(log);
} }
*/ */
} }

View File

@ -337,9 +337,10 @@ public override void Shutdown()
public override string ToString() public override string ToString()
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.Append("Multiplexer:");
foreach (Transport transport in transports) foreach (Transport transport in transports)
builder.AppendLine(transport.ToString()); builder.Append($" {transport}");
return builder.ToString().Trim(); return builder.ToString().Trim();
} }

View File

@ -29,8 +29,6 @@ internal class ServerSslHelper
public ServerSslHelper(SslConfig sslConfig) public ServerSslHelper(SslConfig sslConfig)
{ {
Console.Clear();
config = sslConfig; config = sslConfig;
if (config.enabled) if (config.enabled)
{ {

View File

@ -37,7 +37,7 @@ public void Listen(int port)
listener = TcpListener.Create(port); listener = TcpListener.Create(port);
listener.Start(); listener.Start();
Log.Info($"[SWT-WebSocketServer]: Server Started on {port}!", ConsoleColor.Green); Log.Verbose($"[SWT-WebSocketServer]: Server Started on {port}");
acceptThread = new Thread(acceptLoop); acceptThread = new Thread(acceptLoop);
acceptThread.IsBackground = true; acceptThread.IsBackground = true;
@ -53,7 +53,7 @@ public void Stop()
listener?.Stop(); listener?.Stop();
acceptThread = null; acceptThread = null;
Log.Info($"[SWT-WebSocketServer]: Server stopped...closing all connections."); Log.Verbose($"[SWT-WebSocketServer]: Server stopped...closing all connections.");
// make copy so that foreach doesn't break if values are removed // make copy so that foreach doesn't break if values are removed
Connection[] connectionsCopy = connections.Values.ToArray(); Connection[] connectionsCopy = connections.Values.ToArray();

View File

@ -52,7 +52,31 @@ public class SimpleWebTransport : Transport, PortTransport
[Tooltip("Port to use for server")] [Tooltip("Port to use for server")]
public ushort port = 7778; public ushort port = 7778;
public ushort Port { get => port; set => port = value; } public ushort Port
{
get
{
#if UNITY_WEBGL
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
return clientWebsocketSettings.CustomClientPort;
else
return port;
#else
return port;
#endif
}
set
{
#if UNITY_WEBGL
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
clientWebsocketSettings.CustomClientPort = value;
else
port = value;
#else
port = value;
#endif
}
}
[Tooltip("Groups messages in queue before calling Stream.Send")] [Tooltip("Groups messages in queue before calling Stream.Send")]
public bool batchSend = true; public bool batchSend = true;
@ -144,7 +168,7 @@ public override void ClientConnect(string hostname)
// https://github.com/MirrorNetworking/Mirror/pull/3477 // https://github.com/MirrorNetworking/Mirror/pull/3477
break; break;
default: // default case handles ClientWebsocketPortOption.DefaultSameAsServerPort default: // default case handles ClientWebsocketPortOption.DefaultSameAsServerPort
builder.Port = Port; builder.Port = port;
break; break;
} }
@ -176,11 +200,28 @@ public override void ClientConnect(Uri uri)
client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable); client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable);
client.onError += (Exception e) => // We will not invoke OnClientError if minLogLevel is set to None
// We only send the full exception if minLogLevel is set to Verbose
switch (Log.minLogLevel)
{ {
OnClientError.Invoke(TransportError.Unexpected, e.ToString()); case Log.Levels.Flood:
ClientDisconnect(); case Log.Levels.Verbose:
}; client.onError += (Exception e) =>
{
OnClientError.Invoke(TransportError.Unexpected, e.ToString());
ClientDisconnect();
};
break;
case Log.Levels.Info:
case Log.Levels.Warn:
case Log.Levels.Error:
client.onError += (Exception e) =>
{
OnClientError.Invoke(TransportError.Unexpected, e.Message);
ClientDisconnect();
};
break;
}
client.Connect(uri); client.Connect(uri);
} }
@ -256,7 +297,29 @@ public override void ServerStart()
server.onConnect += OnServerConnected.Invoke; server.onConnect += OnServerConnected.Invoke;
server.onDisconnect += OnServerDisconnected.Invoke; server.onDisconnect += OnServerDisconnected.Invoke;
server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable); server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable);
server.onError += (connId, exception) => OnServerError(connId, TransportError.Unexpected, exception.ToString());
// We will not invoke OnServerError if minLogLevel is set to None
// We only send the full exception if minLogLevel is set to Verbose
switch (Log.minLogLevel)
{
case Log.Levels.Flood:
case Log.Levels.Verbose:
server.onError += (connId, exception) =>
{
OnServerError(connId, TransportError.Unexpected, exception.ToString());
ServerDisconnect(connId);
};
break;
case Log.Levels.Info:
case Log.Levels.Warn:
case Log.Levels.Error:
server.onError += (connId, exception) =>
{
OnServerError(connId, TransportError.Unexpected, exception.Message);
ServerDisconnect(connId);
};
break;
}
SendLoopConfig.batchSend = batchSend || waitBeforeSend; SendLoopConfig.batchSend = batchSend || waitBeforeSend;
SendLoopConfig.sleepBeforeSend = waitBeforeSend; SendLoopConfig.sleepBeforeSend = waitBeforeSend;

View File

@ -250,25 +250,16 @@ public override int GetMaxPacketSize(int channelId)
return serverMaxMessageSize; return serverMaxMessageSize;
} }
public override string ToString() // Keep it short and simple so it looks nice in the HUD.
{ //
if (server != null && server.Active && server.listener != null) // printing server.listener.LocalEndpoint causes an Exception
{ // in UWP + Unity 2019:
// printing server.listener.LocalEndpoint causes an Exception // Exception thrown at 0x00007FF9755DA388 in UWF.exe:
// in UWP + Unity 2019: // Microsoft C++ exception: Il2CppExceptionWrapper at memory
// Exception thrown at 0x00007FF9755DA388 in UWF.exe: // location 0x000000E15A0FCDD0. SocketException: An address
// Microsoft C++ exception: Il2CppExceptionWrapper at memory // incompatible with the requested protocol was used at
// location 0x000000E15A0FCDD0. SocketException: An address // System.Net.Sockets.Socket.get_LocalEndPoint ()
// incompatible with the requested protocol was used at // so just use the regular port instead.
// System.Net.Sockets.Socket.get_LocalEndPoint () public override string ToString() => $"Telepathy [{port}]";
// so let's use the regular port instead.
return $"Telepathy Server port: {port}";
}
else if (client != null && (client.Connecting || client.Connected))
{
return $"Telepathy Client port: {port}";
}
return "Telepathy (inactive/disconnected)";
}
} }
} }

View File

@ -1,15 +1,16 @@
![Mirror Logo](https://user-images.githubusercontent.com/16416509/119120944-6db26780-ba5f-11eb-9cdd-fc8500207f4d.png) ![Mirror Logo](https://user-images.githubusercontent.com/16416509/119120944-6db26780-ba5f-11eb-9cdd-fc8500207f4d.png)
<p align="center">
[![Download](https://img.shields.io/badge/asset_store-brightgreen.svg)](https://assetstore.unity.com/packages/tools/network/mirror-129321) <a href="https://assetstore.unity.com/packages/tools/network/mirror-129321"><img src="https://img.shields.io/badge/download-brightgreen.svg?style=for-the-badge&logo=unity&colorA=363a4f&colorB=f5a97f" alt="Download"></a>
[![Documentation](https://img.shields.io/badge/docs-brightgreen.svg)](https://mirror-networking.gitbook.io/) <a href="https://github.com/MirrorNetworking/Mirror#made-with-mirror"><img src="https://img.shields.io/badge/showcase-brightgreen.svg?style=for-the-badge&logo=github&colorA=363a4f&colorB=f5a97f" alt="Showcase"></a>
[![Showcase](https://img.shields.io/badge/showcase-brightgreen.svg)](https://mirror-networking.com/showcase/) <a href="https://mirror-networking.gitbook.io/"><img src="https://img.shields.io/badge/docs-brightgreen.svg?style=for-the-badge&logo=gitbook&logoColor=white&colorA=363a4f&colorB=f5a97f" alt="Documentation"></a>
[![Video Tutorials](https://img.shields.io/badge/video_tutorial-brightgreen.svg)](https://mirror-networking.gitbook.io/docs/community-guides/video-tutorials) <a href="https://forum.unity.com/threads/mirror-networking-for-unity-aka-hlapi-community-edition.425437/"><img src="https://img.shields.io/badge/forum-brightgreen.svg?style=for-the-badge&logo=unity&colorA=363a4f&colorB=f5a97f" alt="Forum"></a>
[![Forum](https://img.shields.io/badge/forum-brightgreen.svg)](https://forum.unity.com/threads/mirror-networking-for-unity-aka-hlapi-community-edition.425437/) <a href="https://trello.com/b/fgAE7Tud"><img src="https://img.shields.io/badge/roadmap-brightgreen.svg?style=for-the-badge&logo=trello&colorA=363a4f&colorB=f5a97f" alt="Roadmap"></a>
[![Build](https://img.shields.io/appveyor/ci/vis2k73562/hlapi-community-edition/Mirror.svg)](https://ci.appveyor.com/project/vis2k73562/hlapi-community-edition/branch/mirror) <br>
[![Discord](https://img.shields.io/discord/343440455738064897.svg)](https://discordapp.com/invite/xVW4nU4C34) <a href="https://github.com/vis2k/Mirror/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-MIT-brightgreen.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="License: MIT"></a>
[![release](https://img.shields.io/github/release/vis2k/Mirror.svg)](https://github.com/vis2k/Mirror/releases/latest) <a href="https://ci.appveyor.com/project/vis2k73562/hlapi-community-edition/branch/mirror"><img src="https://img.shields.io/appveyor/ci/vis2k73562/hlapi-community-edition/Mirror.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="Build"></a>
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/vis2k/Mirror/blob/master/LICENSE) <a href="https://github.com/vis2k/Mirror/releases/latest"><img src="https://img.shields.io/github/release/vis2k/Mirror.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="release"></a>
[![Roadmap](https://img.shields.io/badge/roadmap-blue.svg)](https://trello.com/b/fgAE7Tud) <a href="https://discordapp.com/invite/xVW4nU4C34"><img src="https://img.shields.io/discord/343440455738064897.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="Discord"></a>
</p>
**It's only the dreamers who ever move mountains.** **It's only the dreamers who ever move mountains.**