Added multiplayer plugin

This commit is contained in:
Anthony Berg
2020-11-30 08:12:07 +00:00
parent 9cb342dd42
commit f64cf54803
450 changed files with 33131 additions and 10 deletions

View File

@@ -0,0 +1,61 @@
using Mirror.Cloud.ListServerService;
using UnityEngine;
namespace Mirror.Cloud
{
/// <summary>
/// Used to requests and responses from the mirror api
/// </summary>
public interface IApiConnector
{
ListServer ListServer { get; }
}
/// <summary>
/// Used to requests and responses from the mirror api
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/CloudServices/ApiConnector")]
[HelpURL("https://mirror-networking.com/docs/CloudServices/ApiConnector.html")]
public class ApiConnector : MonoBehaviour, IApiConnector, ICoroutineRunner
{
#region Inspector
[Header("Settings")]
[Tooltip("Base URL of api, including https")]
[SerializeField] string ApiAddress = "";
[Tooltip("Api key required to access api")]
[SerializeField] string ApiKey = "";
[Header("Events")]
[Tooltip("Triggered when server list updates")]
[SerializeField] ServerListEvent _onServerListUpdated = new ServerListEvent();
#endregion
IRequestCreator requestCreator;
public ListServer ListServer { get; private set; }
void Awake()
{
requestCreator = new RequestCreator(ApiAddress, ApiKey, this);
InitListServer();
}
void InitListServer()
{
IListServerServerApi serverApi = new ListServerServerApi(this, requestCreator);
IListServerClientApi clientApi = new ListServerClientApi(this, requestCreator, _onServerListUpdated);
ListServer = new ListServer(serverApi, clientApi);
}
public void OnDestroy()
{
ListServer?.ServerApi.Shutdown();
ListServer?.ClientApi.Shutdown();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bdb99a29e179d14cb0acc43f175d9ad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f34c32971e65984c93a15376ec11c65
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System;
namespace Mirror.Cloud
{
public interface IBaseApi
{
/// <summary>
/// Cleans up any data created by the instance
/// <para>For Example: removing server from list</para>
/// </summary>
void Shutdown();
}
public abstract class BaseApi
{
protected readonly ICoroutineRunner runner;
protected readonly IRequestCreator requestCreator;
protected BaseApi(ICoroutineRunner runner, IRequestCreator requestCreator)
{
this.runner = runner ?? throw new ArgumentNullException(nameof(runner));
this.requestCreator = requestCreator ?? throw new ArgumentNullException(nameof(requestCreator));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70f563b7a7210ae43bbcde5cb7721a94
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using System;
using Mirror.Cloud.ListServerService;
using UnityEngine.Events;
namespace Mirror.Cloud
{
[Serializable]
public class ServerListEvent : UnityEvent<ServerCollectionJson> { }
[Serializable]
public class MatchFoundEvent : UnityEvent<ServerJson> { }
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7c472a3ea1bc4348bd5a0b05bf7cc3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using UnityEngine.Networking;
namespace Mirror.Cloud
{
public static class Extensions
{
public static bool IsOk(this UnityWebRequest webRequest)
{
return 200 <= webRequest.responseCode && webRequest.responseCode <= 299;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97501e783fc67a4459b15d10e6c63563
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using System.Collections;
using UnityEngine;
namespace Mirror.Cloud
{
public interface ICoroutineRunner : IUnityEqualCheck
{
Coroutine StartCoroutine(IEnumerator routine);
void StopCoroutine(IEnumerator routine);
void StopCoroutine(Coroutine routine);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43472c60a7c72e54eafe559290dd0fc6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System.Collections;
using UnityEngine.Networking;
namespace Mirror.Cloud
{
public delegate void RequestSuccess(string responseBody);
public delegate void RequestFail(string responseBody);
/// <summary>
/// Objects that can be sent to the Api must have this interface
/// </summary>
public interface ICanBeJson { }
/// <summary>
/// Methods to create and send UnityWebRequest
/// </summary>
public interface IRequestCreator
{
UnityWebRequest Delete(string page);
UnityWebRequest Get(string page);
UnityWebRequest Patch<T>(string page, T json) where T : struct, ICanBeJson;
UnityWebRequest Post<T>(string page, T json) where T : struct, ICanBeJson;
/// <summary>
/// Sends Request to api and invokes callback when finished
/// <para>Starts Coroutine of SendRequestEnumerator</para>
/// </summary>
/// <param name="request"></param>
/// <param name="onSuccess"></param>
/// <param name="onFail"></param>
void SendRequest(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null);
/// <summary>
/// Sends Request to api and invokes callback when finished
/// </summary>
/// <param name="request"></param>
/// <param name="onSuccess"></param>
/// <param name="onFail"></param>
/// <returns></returns>
IEnumerator SendRequestEnumerator(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b80b95532a9d6e8418aa676a261e4f69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
using UnityEngine;
namespace Mirror.Cloud
{
/// <summary>
/// Adds Extension to check if unity object is null.
/// <para>Use these methods to stop MissingReferenceException</para>
/// </summary>
public interface IUnityEqualCheck
{
}
public static class UnityEqualCheckExtension
{
public static bool IsNull(this IUnityEqualCheck obj)
{
return (obj as Object) == null;
}
public static bool IsNotNull(this IUnityEqualCheck obj)
{
return (obj as Object) != null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 05185b973ba389a4588fc8a99c75a4f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System;
namespace Mirror.Cloud
{
[Serializable]
public struct CreatedIdJson : ICanBeJson
{
public string id;
}
[Serializable]
public struct ErrorJson : ICanBeJson
{
public string code;
public string message;
public int HtmlCode => int.Parse(code);
}
[Serializable]
public struct EmptyJson : ICanBeJson
{
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0688c0fdae5376e4ea74d5c3904eed17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
using System;
using UnityEngine;
using UnityEngine.Networking;
namespace Mirror.Cloud
{
public static class Logger
{
public static bool VerboseLogging = false;
static readonly ILogger logger = LogFactory.GetLogger("MirrorCloudServices");
public static void LogRequest(string page, string method, bool hasJson, string json)
{
if (hasJson)
{
logger.LogFormat(LogType.Log, "Request: {0} {1} {2}", method, page, json);
}
else
{
logger.LogFormat(LogType.Log, "Request: {0} {1}", method, page);
}
}
public static void LogResponse(UnityWebRequest statusRequest)
{
long code = statusRequest.responseCode;
LogType logType = statusRequest.IsOk()
? LogType.Log
: LogType.Error;
string format = "Response: {0} {1} {2} {3}";
if (logger.IsLogTypeAllowed(logType))
{
// we split path like this to make sure api key doesn't leak
Uri uri = new Uri(statusRequest.url);
string path = string.Join("", uri.Segments);
string msg = string.Format(format, statusRequest.method, code, path, statusRequest.downloadHandler.text);
logger.Log(logType, msg);
}
if (!string.IsNullOrEmpty(statusRequest.error))
{
string msg = string.Format("WEB REQUEST ERROR: {0}", statusRequest.error);
logger.Log(LogType.Error, msg);
}
}
internal static void Log(string msg)
{
if (logger.LogEnabled())
logger.Log(msg);
}
internal static void LogWarning(string msg)
{
if (logger.WarnEnabled())
logger.LogWarning(msg);
}
internal static void LogError(string msg)
{
if (logger.ErrorEnabled())
logger.LogError(msg);
}
internal static void Verbose(string msg)
{
if (VerboseLogging && logger.LogEnabled())
logger.Log(msg);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 457ba2df6cb6e1542996c17c715ee81b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace Mirror.Cloud
{
/// <summary>
/// Methods to create and send UnityWebRequest
/// </summary>
public class RequestCreator : IRequestCreator
{
const string GET = "GET";
const string POST = "POST";
const string PATCH = "PATCH";
const string DELETE = "DELETE";
public readonly string baseAddress;
public readonly string apiKey;
readonly ICoroutineRunner runner;
public RequestCreator(string baseAddress, string apiKey, ICoroutineRunner coroutineRunner)
{
if (string.IsNullOrEmpty(baseAddress))
{
throw new ArgumentNullException(nameof(baseAddress));
}
if (string.IsNullOrEmpty(apiKey))
{
throw new ArgumentNullException(nameof(apiKey));
}
this.baseAddress = baseAddress;
this.apiKey = apiKey;
runner = coroutineRunner ?? throw new ArgumentNullException(nameof(coroutineRunner));
}
Uri CreateUri(string page)
{
return new Uri(string.Format("{0}/{1}?key={2}", baseAddress, page, apiKey));
}
UnityWebRequest CreateWebRequest(string page, string method, string json = null)
{
bool hasJson = !string.IsNullOrEmpty(json);
Logger.LogRequest(page, method, hasJson, json);
UnityWebRequest request = new UnityWebRequest(CreateUri(page));
request.method = method;
if (hasJson)
{
request.SetRequestHeader("Content-Type", "application/json");
}
request.downloadHandler = new DownloadHandlerBuffer();
byte[] bodyRaw = hasJson
? Encoding.UTF8.GetBytes(json)
: null;
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
return request;
}
/// <summary>
/// Create Get Request to page
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
public UnityWebRequest Get(string page)
{
return CreateWebRequest(page, GET);
}
/// <summary>
/// Creates Post Request to page with Json body
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="page"></param>
/// <param name="json"></param>
/// <returns></returns>
public UnityWebRequest Post<T>(string page, T json) where T : struct, ICanBeJson
{
string jsonString = JsonUtility.ToJson(json);
return CreateWebRequest(page, POST, jsonString);
}
/// <summary>
/// Creates Patch Request to page with Json body
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="page"></param>
/// <param name="json"></param>
/// <returns></returns>
public UnityWebRequest Patch<T>(string page, T json) where T : struct, ICanBeJson
{
string jsonString = JsonUtility.ToJson(json);
return CreateWebRequest(page, PATCH, jsonString);
}
/// <summary>
/// Create Delete Request to page
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
public UnityWebRequest Delete(string page)
{
return CreateWebRequest(page, DELETE);
}
public void SendRequest(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null)
{
runner.StartCoroutine(SendRequestEnumerator(request, onSuccess, onFail));
}
public IEnumerator SendRequestEnumerator(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null)
{
using (UnityWebRequest webRequest = request)
{
yield return webRequest.SendWebRequest();
Logger.LogResponse(webRequest);
string text = webRequest.downloadHandler.text;
Logger.Verbose(text);
if (webRequest.IsOk())
{
onSuccess?.Invoke(text);
}
else
{
onFail?.Invoke(text);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfaa626443cc7c94eae138a2e3a04d7c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c4c4be148a492b143a881cd08bf7e320
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
using System;
using UnityEngine.Events;
namespace Mirror.Cloud.ListServerService
{
public sealed class ListServer
{
public readonly IListServerServerApi ServerApi;
public readonly IListServerClientApi ClientApi;
public ListServer(IListServerServerApi serverApi, IListServerClientApi clientApi)
{
ServerApi = serverApi ?? throw new ArgumentNullException(nameof(serverApi));
ClientApi = clientApi ?? throw new ArgumentNullException(nameof(clientApi));
}
}
public interface IListServerServerApi : IBaseApi
{
/// <summary>
/// Has a server been added to the list with this connection
/// </summary>
bool ServerInList { get; }
/// <summary>
/// Add a server to the list
/// </summary>
/// <param name="server"></param>
void AddServer(ServerJson server);
/// <summary>
/// Update the current server
/// </summary>
/// <param name="newPlayerCount"></param>
void UpdateServer(int newPlayerCount);
/// <summary>
/// Update the current server
/// </summary>
/// <param name="server"></param>
void UpdateServer(ServerJson server);
/// <summary>
/// Removes the current server
/// </summary>
void RemoveServer();
}
public interface IListServerClientApi : IBaseApi
{
/// <summary>
/// Called when the server list is updated
/// </summary>
event UnityAction<ServerCollectionJson> onServerListUpdated;
/// <summary>
/// Get the server list once
/// </summary>
void GetServerList();
/// <summary>
/// Start getting the server list every interval
/// </summary>
/// <param name="interval"></param>
void StartGetServerListRepeat(int interval);
/// <summary>
/// Stop getting the server list
/// </summary>
void StopGetServerListRepeat();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f0311899162c5b49a3c11fa9bd9c133
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace Mirror.Cloud.ListServerService
{
public abstract class ListServerBaseApi : BaseApi
{
protected ListServerBaseApi(ICoroutineRunner runner, IRequestCreator requestCreator) : base(runner, requestCreator)
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6838f9df45594d48873518cbb75b329
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
namespace Mirror.Cloud.ListServerService
{
public sealed class ListServerClientApi : ListServerBaseApi, IListServerClientApi
{
readonly ServerListEvent _onServerListUpdated;
Coroutine getServerListRepeatCoroutine;
public event UnityAction<ServerCollectionJson> onServerListUpdated
{
add => _onServerListUpdated.AddListener(value);
remove => _onServerListUpdated.RemoveListener(value);
}
public ListServerClientApi(ICoroutineRunner runner, IRequestCreator requestCreator, ServerListEvent onServerListUpdated) : base(runner, requestCreator)
{
_onServerListUpdated = onServerListUpdated;
}
public void Shutdown()
{
StopGetServerListRepeat();
}
public void GetServerList()
{
runner.StartCoroutine(getServerList());
}
public void StartGetServerListRepeat(int interval)
{
getServerListRepeatCoroutine = runner.StartCoroutine(GetServerListRepeat(interval));
}
public void StopGetServerListRepeat()
{
// if runner is null it has been destroyed and will alraedy be null
if (runner.IsNotNull() && getServerListRepeatCoroutine != null)
{
runner.StopCoroutine(getServerListRepeatCoroutine);
}
}
IEnumerator GetServerListRepeat(int interval)
{
while (true)
{
yield return getServerList();
yield return new WaitForSeconds(interval);
}
}
IEnumerator getServerList()
{
UnityWebRequest request = requestCreator.Get("servers");
yield return requestCreator.SendRequestEnumerator(request, onSuccess);
void onSuccess(string responseBody)
{
ServerCollectionJson serverlist = JsonUtility.FromJson<ServerCollectionJson>(responseBody);
_onServerListUpdated?.Invoke(serverlist);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d49649fb32cb96b46b10f013b38a4b50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mirror.Cloud.ListServerService
{
[Serializable]
public struct ServerCollectionJson : ICanBeJson
{
public ServerJson[] servers;
}
[Serializable]
public struct ServerJson : ICanBeJson
{
public string protocol;
public int port;
public int playerCount;
public int maxPlayerCount;
/// <summary>
/// optional
/// </summary>
public string displayName;
/// <summary>
/// Uri string of the ip and port of the server.
/// <para>The ip is calculated by the request to the API</para>
/// <para>This is returns from the api, any incoming address fields will be ignored</para>
/// </summary>
public string address;
/// <summary>
/// Can be used to set custom uri
/// <para>optional</para>
/// </summary>
public string customAddress;
/// <summary>
/// Array of custom data, use SetCustomData to set values
/// <para>optional</para>
/// </summary>
public KeyValue[] customData;
/// <summary>
/// Uri from address field
/// </summary>
/// <returns></returns>
public Uri GetServerUri() => new Uri(address);
/// <summary>
/// Uri from customAddress field
/// </summary>
/// <returns></returns>
public Uri GetCustomUri() => new Uri(customAddress);
/// <summary>
/// Updates the customData array
/// </summary>
/// <param name="data"></param>
public void SetCustomData(Dictionary<string, string> data)
{
if (data == null)
{
customData = null;
}
else
{
customData = data.ToKeyValueArray();
CustomDataHelper.ValidateCustomData(customData);
}
}
public bool Validate()
{
CustomDataHelper.ValidateCustomData(customData);
if (string.IsNullOrEmpty(protocol))
{
Logger.LogError("ServerJson should not have empty protocol");
return false;
}
if (port == 0)
{
Logger.LogError("ServerJson should not have port equal 0");
return false;
}
if (maxPlayerCount == 0)
{
Logger.LogError("ServerJson should not have maxPlayerCount equal 0");
return false;
}
return true;
}
}
[Serializable]
public struct PartialServerJson : ICanBeJson
{
/// <summary>
/// optional
/// </summary>
public int playerCount;
/// <summary>
/// optional
/// </summary>
public int maxPlayerCount;
/// <summary>
/// optional
/// </summary>
public string displayName;
/// <summary>
/// Array of custom data, use SetCustomData to set values
/// <para>optional</para>
/// </summary>
public KeyValue[] customData;
public void SetCustomData(Dictionary<string, string> data)
{
if (data == null)
{
customData = null;
}
else
{
customData = data.ToKeyValueArray();
CustomDataHelper.ValidateCustomData(customData);
}
}
public void Validate()
{
CustomDataHelper.ValidateCustomData(customData);
}
}
public static class CustomDataHelper
{
const int MaxCustomData = 16;
public static Dictionary<string, string> ToDictionary(this KeyValue[] keyValues)
{
return keyValues.ToDictionary(x => x.key, x => x.value);
}
public static KeyValue[] ToKeyValueArray(this Dictionary<string, string> dictionary)
{
return dictionary.Select(kvp => new KeyValue(kvp.Key, kvp.Value)).ToArray();
}
public static void ValidateCustomData(KeyValue[] customData)
{
if (customData == null)
{
return;
}
if (customData.Length > MaxCustomData)
{
Logger.LogError($"There can only be {MaxCustomData} custom data but there was {customData.Length} values given");
Array.Resize(ref customData, MaxCustomData);
}
foreach (KeyValue item in customData)
{
item.Validate();
}
}
}
[Serializable]
public struct KeyValue
{
const int MaxKeySize = 32;
const int MaxValueSize = 256;
public string key;
public string value;
public KeyValue(string key, string value)
{
this.key = key;
this.value = value;
}
public void Validate()
{
if (key.Length > MaxKeySize)
{
Logger.LogError($"Custom Data must have key with length less than {MaxKeySize}");
key = key.Substring(0, MaxKeySize);
}
if (value.Length > MaxValueSize)
{
Logger.LogError($"Custom Data must have value with length less than {MaxValueSize}");
value = value.Substring(0, MaxValueSize);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a963606335eae0f47abe7ecb5fd028ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,219 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
namespace Mirror.Cloud.ListServerService
{
public sealed class ListServerServerApi : ListServerBaseApi, IListServerServerApi
{
const int PingInterval = 20;
const int MaxPingFails = 15;
ServerJson currentServer;
string serverId;
Coroutine _pingCoroutine;
/// <summary>
/// If the server has already been added
/// </summary>
bool added;
/// <summary>
/// if a request is currently sending
/// </summary>
bool sending;
/// <summary>
/// If an update request was recently sent
/// </summary>
bool skipNextPing;
/// <summary>
/// How many failed pings in a row
/// </summary>
int pingFails = 0;
public bool ServerInList => added;
public ListServerServerApi(ICoroutineRunner runner, IRequestCreator requestCreator) : base(runner, requestCreator)
{
}
public void Shutdown()
{
stopPingCoroutine();
if (added)
{
removeServerWithoutCoroutine();
}
added = false;
}
public void AddServer(ServerJson server)
{
if (added) { Logger.LogWarning("AddServer called when server was already adding or added"); return; }
bool valid = server.Validate();
if (!valid) { return; }
runner.StartCoroutine(addServer(server));
}
public void UpdateServer(int newPlayerCount)
{
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); return; }
currentServer.playerCount = newPlayerCount;
UpdateServer(currentServer);
}
public void UpdateServer(ServerJson server)
{
// TODO, use PartialServerJson as Arg Instead
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); return; }
PartialServerJson partialServer = new PartialServerJson
{
displayName = server.displayName,
playerCount = server.playerCount,
maxPlayerCount = server.maxPlayerCount,
customData = server.customData,
};
partialServer.Validate();
runner.StartCoroutine(updateServer(partialServer));
}
public void RemoveServer()
{
if (!added) { return; }
if (string.IsNullOrEmpty(serverId))
{
Logger.LogWarning("Can not remove server because serverId was empty");
return;
}
stopPingCoroutine();
runner.StartCoroutine(removeServer());
}
void stopPingCoroutine()
{
if (_pingCoroutine != null)
{
runner.StopCoroutine(_pingCoroutine);
_pingCoroutine = null;
}
}
IEnumerator addServer(ServerJson server)
{
added = true;
sending = true;
currentServer = server;
UnityWebRequest request = requestCreator.Post("servers", currentServer);
yield return requestCreator.SendRequestEnumerator(request, onSuccess, onFail);
sending = false;
void onSuccess(string responseBody)
{
CreatedIdJson created = JsonUtility.FromJson<CreatedIdJson>(responseBody);
serverId = created.id;
// Start ping to keep server alive
_pingCoroutine = runner.StartCoroutine(ping());
}
void onFail(string responseBody)
{
added = false;
}
}
IEnumerator updateServer(PartialServerJson server)
{
// wait to not be sending
while (sending)
{
yield return new WaitForSeconds(1);
}
// We need to check added incase Update is called soon after Add, and add failed
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); yield break; }
sending = true;
UnityWebRequest request = requestCreator.Patch("servers/" + serverId, server);
yield return requestCreator.SendRequestEnumerator(request, onSuccess);
sending = false;
void onSuccess(string responseBody)
{
skipNextPing = true;
if (_pingCoroutine == null)
{
_pingCoroutine = runner.StartCoroutine(ping());
}
}
}
/// <summary>
/// Keeps server alive in database
/// </summary>
/// <returns></returns>
IEnumerator ping()
{
while (pingFails <= MaxPingFails)
{
yield return new WaitForSeconds(PingInterval);
if (skipNextPing)
{
skipNextPing = false;
continue;
}
sending = true;
UnityWebRequest request = requestCreator.Patch("servers/" + serverId, new EmptyJson());
yield return requestCreator.SendRequestEnumerator(request, onSuccess, onFail);
sending = false;
}
Logger.LogWarning("Max ping fails reached, stoping to ping server");
_pingCoroutine = null;
void onSuccess(string responseBody)
{
pingFails = 0;
}
void onFail(string responseBody)
{
pingFails++;
}
}
IEnumerator removeServer()
{
sending = true;
UnityWebRequest request = requestCreator.Delete("servers/" + serverId);
yield return requestCreator.SendRequestEnumerator(request);
sending = false;
added = false;
}
void removeServerWithoutCoroutine()
{
if (string.IsNullOrEmpty(serverId))
{
Logger.LogWarning("Can not remove server becuase serverId was empty");
return;
}
UnityWebRequest request = requestCreator.Delete("servers/" + serverId);
UnityWebRequestAsyncOperation operation = request.SendWebRequest();
operation.completed += (op) =>
{
Logger.LogResponse(request);
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 675f0d0fd4e82b04290c4d30c8d78ede
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "Mirror.Cloud",
"references": [
"Mirror"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c21ba7b8c3183cb47b7fe3b3799d49c4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
# Mirror Cloud Services
## Mirror List Server
Example has an API key which can be used as a demo.
To get an API key to use within your game you can subscribe on the [Mirror Networking Website](https://mirror-networking.com/list-server/)
### Key features
- The Cloud Service works via https so it is secure and can be used from any platform.
- It runs on Google Cloud so there is no worry about server downtime.
- It scales really well. Default quota is 1000 API requests per minute. If you have high demands, contact us and we can increase that limit.
## List Server Examples
An example for this can be found in [Mirror/Examples/Cloud/](https://github.com/vis2k/Mirror/tree/master/Assets/Mirror/Examples/Cloud)
*Note: you cannot connect to your own public IP address, you will need at least one other person to test this*
## How to use
Add `ApiConnector` component to an object in your game. It is probably best to put this on the same object as your NetworkManager. Once it has been added set the `ApiAddress` and `ApiKey` fields.
To use `ApiConnector` either directly reference it in an inspector field or get it when your script awakes
```cs
ApiConnector connector;
void Awake()
{
connector = FindObjectOfType<ApiConnector>();
}
```
The Api calls are grouped into objects. `connector.ListServer.ServerApi` has the Server Api calls like `AddServer`. `connector.ListServer.ClientApi` has the Client Api calls like `GetServerList`.
### Server Api Example
Example of how to add server
```cs
void AddServer(int playerCount)
{
Transport transport = Transport.activeTransport;
Uri uri = transport.ServerUri();
int port = uri.Port;
string protocol = uri.Scheme;
connector.ListServer.ServerApi.AddServer(new ServerJson
{
displayName = "Fun game!!!",
protocol = protocol,
port = port,
maxPlayerCount = NetworkManager.singleton.maxConnections,
playerCount = playerCount
});
}
```
### Client Api Example
Example of how to list servers
```cs
ApiConnector connector;
void Awake()
{
connector = FindObjectOfType<ApiConnector>();
// add listener to event that will update UI when Server list is refreshed
connector.ListServer.ClientApi.onServerListUpdated += onServerListUpdated;
// add listen to button so that player can refresh server list
refreshButton.onClick.AddListener(RefreshButtonHandler);
}
public void RefreshButtonHandler()
{
connector.ListServer.ClientApi.GetServerList();
}
void onServerListUpdated()
{
// Update UI here
}
```
## Debug
If something doesn't seem to be working then here are some tips to help solve the problem
### Check logs
Enable `showDebugMessages` on your NetworkManager or use the log level window to enable logging for the cloud scripts
Below are some example logs to look for to check things are working.
#### Add Server
The add request is sent to add a server to the list server
```
Request: POST servers {"protocol":"tcp4","port":7777,"playerCount":0,"maxPlayerCount":4,"displayName":"Tanks Game 521","address":"","customAddress":"","customData":[]}
```
```
Response: POST 200 /servers {"id":"BI6bQQ2TbNiqhdp1D7UB"}
```
#### Update Server
The object sent in update request maybe be empty, this is sent to keep the server record alive so it shows up.
The update request can also be used to change info. For example the player count when someone joins or leaves
```
Request: PATCH servers/BI6bQQ2TbNiqhdp1D7UB {}
```
```
Response: PATCH 204 /servers/BI6bQQ2TbNiqhdp1D7UB
```
#### Remove Server
The remove request is sent to remove a server from the list server. This is automatically called when the ApiConnection is destroyed.
```
Request: DELETE servers/BI6bQQ2TbNiqhdp1D7UB
```
```
Response: DELETE 204 /servers/BI6bQQ2TbNiqhdp1D7UB
```
#### Get Servers
The get request is sent in order to get the list of servers.
The example below shows an array of 2 servers, one with name `Tanks Game 521` and the other with name `Tanks Game 212`
```
Request: GET servers
```
```
Response: GET 200 /servers {"servers":[{"address":"tcp4://xx.xx.xx.xx:7777","displayName":"Tanks Game 521","port":7777,"protocol":"tcp4","playerCount":0,"maxPlayerCount":4,"customAddress":"","customData":[]},{"address":"tcp4://xx.xx.xx.xx:7777","displayName":"Tanks Game 212","port":7777,"protocol":"tcp4","playerCount":0,"maxPlayerCount":4,"customData":[]}]}
```
*xx.xx.xx.xx will be the IP address for the server*
### Use the QuickListServerDebug
The QuickListServerDebug script uses `OnGUI` to show the list of servers. This script can be used to check the server list without using Canvas UI.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 04945d14ccbed964597a1ee00805c059
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
MirrorCloudServices v0.1.0

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bf81e376b88e68e48a47531b8bfeb0f4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: