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,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: