aha
This commit is contained in:
8
Assets/Mirror/Transports/Edgegap.meta
Normal file
8
Assets/Mirror/Transports/Edgegap.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d2959d363903444bae4333db12a9ea1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta
Normal file
8
Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 447b4ad1a3db7cf4fa5a0709d297ba9b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Threading;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
namespace Edgegap
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")]
|
||||
public class EdgegapLobbyKcpTransport : EdgegapKcpTransport
|
||||
{
|
||||
[Header("Lobby Settings")]
|
||||
[Tooltip("URL to the Edgegap lobby service, automatically filled in after completing the creation process via button below (or enter manually)")]
|
||||
public string lobbyUrl;
|
||||
[Tooltip("How long to wait for the relay to be assigned after starting a lobby")]
|
||||
public float lobbyWaitTimeout = 60;
|
||||
|
||||
public LobbyApi Api;
|
||||
private LobbyCreateRequest? _request;
|
||||
private string _lobbyId;
|
||||
private string _playerId;
|
||||
private TransportStatus _status = TransportStatus.Offline;
|
||||
public enum TransportStatus
|
||||
{
|
||||
Offline,
|
||||
CreatingLobby,
|
||||
StartingLobby,
|
||||
JoiningLobby,
|
||||
WaitingRelay,
|
||||
Connecting,
|
||||
Connected,
|
||||
Error,
|
||||
}
|
||||
public TransportStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!NetworkClient.active && !NetworkServer.active)
|
||||
{
|
||||
return TransportStatus.Offline;
|
||||
}
|
||||
if (_status == TransportStatus.Connecting)
|
||||
{
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
switch (((EdgegapKcpServer)this.server).state)
|
||||
{
|
||||
case ConnectionState.Valid:
|
||||
return TransportStatus.Connected;
|
||||
case ConnectionState.Invalid:
|
||||
case ConnectionState.SessionTimeout:
|
||||
case ConnectionState.Error:
|
||||
return TransportStatus.Error;
|
||||
}
|
||||
}
|
||||
else if (NetworkClient.active)
|
||||
{
|
||||
switch (((EdgegapKcpClient)this.client).connectionState)
|
||||
{
|
||||
case ConnectionState.Valid:
|
||||
return TransportStatus.Connected;
|
||||
case ConnectionState.Invalid:
|
||||
case ConnectionState.SessionTimeout:
|
||||
case ConnectionState.Error:
|
||||
return TransportStatus.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _status;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
Api = new LobbyApi(lobbyUrl);
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
this.relayGUI = false;
|
||||
}
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (!_request.HasValue)
|
||||
{
|
||||
throw new Exception("No lobby request set. Call SetServerLobbyParams");
|
||||
}
|
||||
_status = TransportStatus.CreatingLobby;
|
||||
Api.CreateLobby(_request.Value, lobby =>
|
||||
{
|
||||
_lobbyId = lobby.lobby_id;
|
||||
_status = TransportStatus.StartingLobby;
|
||||
Api.StartLobby(new LobbyIdRequest(_lobbyId), () =>
|
||||
{
|
||||
StartCoroutine(WaitForLobbyRelay(_lobbyId, true));
|
||||
}, error =>
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
string errorMsg = $"Could not start lobby: {error}";
|
||||
Debug.LogError(errorMsg);
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg);
|
||||
ServerStop();
|
||||
});
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
string errorMsg = $"Couldn't create lobby: {error}";
|
||||
Debug.LogError(errorMsg);
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
public override void ServerStop()
|
||||
{
|
||||
base.ServerStop();
|
||||
|
||||
Api.DeleteLobby(_lobbyId, () =>
|
||||
{
|
||||
// yay
|
||||
}, error =>
|
||||
{
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, $"Failed to delete lobby: {error}");
|
||||
});
|
||||
}
|
||||
|
||||
public override void ClientDisconnect()
|
||||
{
|
||||
base.ClientDisconnect();
|
||||
// this gets called for host mode as well
|
||||
if (!NetworkServer.active)
|
||||
{
|
||||
Api.LeaveLobby(new LobbyJoinOrLeaveRequest
|
||||
{
|
||||
player = new LobbyJoinOrLeaveRequest.Player
|
||||
{
|
||||
id = _playerId
|
||||
},
|
||||
lobby_id = _lobbyId
|
||||
}, () =>
|
||||
{
|
||||
// yay
|
||||
}, error =>
|
||||
{
|
||||
string errorMsg = $"Failed to leave lobby: {error}";
|
||||
OnClientError?.Invoke(TransportError.Unexpected, errorMsg);
|
||||
Debug.LogError(errorMsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
_lobbyId = address;
|
||||
_playerId = RandomPlayerId();
|
||||
_status = TransportStatus.JoiningLobby;
|
||||
Api.JoinLobby(new LobbyJoinOrLeaveRequest
|
||||
{
|
||||
player = new LobbyJoinOrLeaveRequest.Player
|
||||
{
|
||||
id = _playerId,
|
||||
},
|
||||
lobby_id = address
|
||||
}, () =>
|
||||
{
|
||||
StartCoroutine(WaitForLobbyRelay(_lobbyId, false));
|
||||
}, error =>
|
||||
{
|
||||
_status = TransportStatus.Offline;
|
||||
string errorMsg = $"Failed to join lobby: {error}";
|
||||
OnClientError?.Invoke(TransportError.Unexpected, errorMsg);
|
||||
Debug.LogError(errorMsg);
|
||||
OnClientDisconnected?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerator WaitForLobbyRelay(string lobbyId, bool forServer)
|
||||
{
|
||||
_status = TransportStatus.WaitingRelay;
|
||||
double startTime = NetworkTime.localTime;
|
||||
bool running = true;
|
||||
while (running)
|
||||
{
|
||||
if (NetworkTime.localTime - startTime >= lobbyWaitTimeout)
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
string errorMsg = "Timed out waiting for lobby.";
|
||||
Debug.LogError(errorMsg);
|
||||
if (forServer)
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg);
|
||||
ServerStop();
|
||||
}
|
||||
else
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
OnClientError?.Invoke(TransportError.Unexpected, errorMsg);
|
||||
ClientDisconnect();
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
bool waitingForResponse = true;
|
||||
Api.GetLobby(lobbyId, lobby =>
|
||||
{
|
||||
waitingForResponse = false;
|
||||
if (string.IsNullOrEmpty(lobby.assignment.ip))
|
||||
{
|
||||
// no lobby deployed yet, have the outer loop retry
|
||||
return;
|
||||
}
|
||||
relayAddress = lobby.assignment.ip;
|
||||
foreach (Lobby.Port aport in lobby.assignment.ports)
|
||||
{
|
||||
if (aport.protocol == "UDP")
|
||||
{
|
||||
if (aport.name == "server")
|
||||
{
|
||||
relayGameServerPort = (ushort)aport.port;
|
||||
|
||||
}
|
||||
else if (aport.name == "client")
|
||||
{
|
||||
relayGameClientPort = (ushort)aport.port;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool found = false;
|
||||
foreach (Lobby.Player player in lobby.players)
|
||||
{
|
||||
if (player.id == _playerId)
|
||||
{
|
||||
userId = player.authorization_token;
|
||||
sessionId = lobby.assignment.authorization_token;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
running = false;
|
||||
if (!found)
|
||||
{
|
||||
string errorMsg = $"Couldn't find my player ({_playerId})";
|
||||
Debug.LogError(errorMsg);
|
||||
|
||||
if (forServer)
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg);
|
||||
ServerStop();
|
||||
}
|
||||
else
|
||||
{
|
||||
_status = TransportStatus.Error;
|
||||
OnClientError?.Invoke(TransportError.Unexpected, errorMsg);
|
||||
ClientDisconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_status = TransportStatus.Connecting;
|
||||
if (forServer)
|
||||
{
|
||||
base.ServerStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.ClientConnect("");
|
||||
}
|
||||
}, error =>
|
||||
{
|
||||
running = false;
|
||||
waitingForResponse = false;
|
||||
_status = TransportStatus.Error;
|
||||
string errorMsg = $"Failed to get lobby info: {error}";
|
||||
Debug.LogError(errorMsg);
|
||||
if (forServer)
|
||||
{
|
||||
OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg);
|
||||
ServerStop();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClientError?.Invoke(TransportError.Unexpected, errorMsg);
|
||||
ClientDisconnect();
|
||||
}
|
||||
});
|
||||
while (waitingForResponse)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
}
|
||||
}
|
||||
private static string RandomPlayerId()
|
||||
{
|
||||
return $"mirror-player-{Random.Range(1, int.MaxValue)}";
|
||||
}
|
||||
|
||||
public void SetServerLobbyParams(string lobbyName, int capacity)
|
||||
{
|
||||
SetServerLobbyParams(new LobbyCreateRequest
|
||||
{
|
||||
player = new LobbyCreateRequest.Player
|
||||
{
|
||||
id = RandomPlayerId(),
|
||||
},
|
||||
annotations = new LobbyCreateRequest.Annotation[]
|
||||
{
|
||||
},
|
||||
capacity = capacity,
|
||||
is_joinable = true,
|
||||
name = lobbyName,
|
||||
tags = new string[]
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SetServerLobbyParams(LobbyCreateRequest request)
|
||||
{
|
||||
_playerId = request.player.id;
|
||||
_request = request;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// attempt to clean up lobbies, if active
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
ServerStop();
|
||||
// Absolutely make sure there's time for the network request to hit edgegap servers.
|
||||
// sorry. this can go once the lobby service can timeout lobbies itself
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
else if (NetworkClient.active)
|
||||
{
|
||||
ClientDisconnect();
|
||||
// Absolutely make sure there's time for the network request to hit edgegap servers.
|
||||
// sorry. this can go once the lobby service can timeout lobbies itself
|
||||
Thread.Sleep(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa9d4c3f48a245ed89f122f44e1e81ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs
|
||||
uploadId: 736421
|
295
Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs
Normal file
295
Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs
Normal file
@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
// Implements the edgegap lobby api: https://docs.edgegap.com/docs/lobby/functions
|
||||
public class LobbyApi
|
||||
{
|
||||
[Header("Lobby Config")]
|
||||
public string LobbyUrl;
|
||||
public LobbyBrief[] Lobbies;
|
||||
|
||||
public LobbyApi(string url)
|
||||
{
|
||||
LobbyUrl = url;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static UnityWebRequest SendJson<T>(string url, T data, string method = "POST")
|
||||
{
|
||||
string body = JsonUtility.ToJson(data);
|
||||
UnityWebRequest request = new UnityWebRequest(url, method);
|
||||
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
private static bool CheckErrorResponse(UnityWebRequest request, Action<string> onError)
|
||||
{
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
// how I hate http libs that think they need to be smart and handle status code errors.
|
||||
if (request.result != UnityWebRequest.Result.ProtocolError || request.responseCode == 0)
|
||||
{
|
||||
onError?.Invoke(request.error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (request.isNetworkError)
|
||||
{
|
||||
onError?.Invoke(request.error);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (request.responseCode < 200 || request.responseCode >= 300)
|
||||
{
|
||||
onError?.Invoke($"non-200 status code: {request.responseCode}. Body:\n {request.downloadHandler.text}");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RefreshLobbies(Action<LobbyBrief[]> onLoaded, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies");
|
||||
request.SendWebRequest().completed += operation =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
ListLobbiesResponse lobbies = JsonUtility.FromJson<ListLobbiesResponse>(request.downloadHandler.text);
|
||||
Lobbies = lobbies.data;
|
||||
onLoaded?.Invoke(lobbies.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void CreateLobby(LobbyCreateRequest createData, Action<Lobby> onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies", createData);
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
Lobby lobby = JsonUtility.FromJson<Lobby>(request.downloadHandler.text);
|
||||
onResponse?.Invoke(lobby);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateLobby(string lobbyId, LobbyUpdateRequest updateData, Action<LobbyBrief> onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", updateData, "PATCH");
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
LobbyBrief lobby = JsonUtility.FromJson<LobbyBrief>(request.downloadHandler.text);
|
||||
onResponse?.Invoke(lobby);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void GetLobby(string lobbyId, Action<Lobby> onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies/{lobbyId}");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
Lobby lobby = JsonUtility.FromJson<Lobby>(request.downloadHandler.text);
|
||||
onResponse?.Invoke(lobby);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void JoinLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:join", data);
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
onResponse?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void LeaveLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:leave", data);
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
onResponse?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void StartLobby(LobbyIdRequest data, Action onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:start", data);
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
onResponse?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void DeleteLobby(string lobbyId, Action onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", "", "DELETE");
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
onResponse?.Invoke();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct CreateLobbyServiceRequest
|
||||
{
|
||||
public string name;
|
||||
}
|
||||
public struct LobbyServiceResponse
|
||||
{
|
||||
public string name;
|
||||
public string url;
|
||||
public string status;
|
||||
}
|
||||
|
||||
public static void TrimApiKey(ref string apiKey)
|
||||
{
|
||||
if (apiKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (apiKey.StartsWith("token "))
|
||||
{
|
||||
apiKey = apiKey.Substring("token ".Length);
|
||||
}
|
||||
apiKey = apiKey.Trim();
|
||||
}
|
||||
|
||||
public static void CreateAndDeployLobbyService(string apiKey, string name, Action<LobbyServiceResponse> onResponse, Action<string> onError)
|
||||
{
|
||||
TrimApiKey(ref apiKey);
|
||||
|
||||
// try to get the lobby first
|
||||
GetLobbyService(apiKey, name, response =>
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
CreateLobbyService(apiKey, name, onResponse, onError);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(response.Value.url))
|
||||
{
|
||||
onResponse(response.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
DeployLobbyService(apiKey, name, onResponse, onError);
|
||||
}
|
||||
}, onError);
|
||||
}
|
||||
|
||||
private static void CreateLobbyService(string apiKey, string name, Action<LobbyServiceResponse> onResponse, Action<string> onError)
|
||||
{
|
||||
UnityWebRequest request = SendJson("https://api.edgegap.com/v1/lobbies", new CreateLobbyServiceRequest
|
||||
{
|
||||
name = name
|
||||
});
|
||||
request.SetRequestHeader("Authorization", $"token {apiKey}");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
DeployLobbyService(apiKey, name, onResponse, onError);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void GetLobbyService(string apiKey, string name, Action<LobbyServiceResponse?> onResponse, Action<string> onError)
|
||||
{
|
||||
TrimApiKey(ref apiKey);
|
||||
|
||||
var request = UnityWebRequest.Get($"https://api.edgegap.com/v1/lobbies/{name}");
|
||||
request.SetRequestHeader("Authorization", $"token {apiKey}");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (request.responseCode == 404)
|
||||
{
|
||||
onResponse(null);
|
||||
return;
|
||||
}
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
LobbyServiceResponse response = JsonUtility.FromJson<LobbyServiceResponse>(request.downloadHandler.text);
|
||||
onResponse(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void TerminateLobbyService(string apiKey, string name, Action<LobbyServiceResponse> onResponse, Action<string> onError)
|
||||
{
|
||||
TrimApiKey(ref apiKey);
|
||||
|
||||
var request = SendJson("https://api.edgegap.com/v1/lobbies:terminate", new CreateLobbyServiceRequest
|
||||
{
|
||||
name = name
|
||||
});
|
||||
request.SetRequestHeader("Authorization", $"token {apiKey}");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
LobbyServiceResponse response = JsonUtility.FromJson<LobbyServiceResponse>(request.downloadHandler.text);
|
||||
onResponse?.Invoke(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
private static void DeployLobbyService(string apiKey, string name, Action<LobbyServiceResponse> onResponse, Action<string> onError)
|
||||
{
|
||||
var request = SendJson("https://api.edgegap.com/v1/lobbies:deploy", new CreateLobbyServiceRequest
|
||||
{
|
||||
name = name
|
||||
});
|
||||
request.SetRequestHeader("Authorization", $"token {apiKey}");
|
||||
request.SendWebRequest().completed += (op) =>
|
||||
{
|
||||
using (request)
|
||||
{
|
||||
if (CheckErrorResponse(request, onError)) return;
|
||||
LobbyServiceResponse response = JsonUtility.FromJson<LobbyServiceResponse>(request.downloadHandler.text);
|
||||
onResponse?.Invoke(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64510fc75d0d75f4185fec1cf4d12206
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
namespace Edgegap
|
||||
{
|
||||
public class LobbyServiceCreateDialogue : EditorWindow
|
||||
{
|
||||
public Action<string> onLobby;
|
||||
public bool waitingCreate;
|
||||
public bool waitingStatus;
|
||||
private string _name;
|
||||
private string _key;
|
||||
private string _lastStatus;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
minSize = maxSize = new Vector2(450, 300);
|
||||
titleContent = new GUIContent("Edgegap Lobby Service Setup");
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (waitingCreate)
|
||||
{
|
||||
EditorGUILayout.LabelField("Waiting for lobby to create . . . ");
|
||||
return;
|
||||
}
|
||||
if (waitingStatus)
|
||||
{
|
||||
EditorGUILayout.LabelField("Waiting for lobby to deploy . . . ");
|
||||
EditorGUILayout.LabelField($"Latest status: {_lastStatus}");
|
||||
return;
|
||||
}
|
||||
_key = EditorGUILayout.TextField("Edgegap API key", _key);
|
||||
LobbyApi.TrimApiKey(ref _key);
|
||||
EditorGUILayout.HelpBox(new GUIContent("Your API key won't be saved."));
|
||||
if (GUILayout.Button("I have no api key?"))
|
||||
{
|
||||
Application.OpenURL("https://app.edgegap.com/user-settings?tab=tokens");
|
||||
}
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.HelpBox("There's currently a bug where lobby names longer than 5 characters can fail to deploy correctly and will return a \"503 Service Temporarily Unavailable\"\nIt's recommended to limit your lobby names to 4-5 characters for now", UnityEditor.MessageType.Warning);
|
||||
_name = EditorGUILayout.TextField("Lobby Name", _name);
|
||||
EditorGUILayout.HelpBox(new GUIContent("The lobby name is your games identifier for the lobby service"));
|
||||
|
||||
if (GUILayout.Button("Create"))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok");
|
||||
}
|
||||
else
|
||||
{
|
||||
waitingCreate = true;
|
||||
Repaint();
|
||||
|
||||
LobbyApi.CreateAndDeployLobbyService(_key.Trim(), _name.Trim(), res =>
|
||||
{
|
||||
waitingCreate = false;
|
||||
waitingStatus = true;
|
||||
_lastStatus = res.status;
|
||||
RefreshStatus();
|
||||
Repaint();
|
||||
}, error =>
|
||||
{
|
||||
EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) the lobby service:\n\n{error}", "Ok");
|
||||
waitingCreate = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Cancel"))
|
||||
Close();
|
||||
|
||||
EditorGUILayout.HelpBox(new GUIContent("Note: If you forgot your lobby url simply re-create it with the same name!\nIt will re-use the existing lobby service"));
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.Separator();
|
||||
|
||||
|
||||
if (GUILayout.Button("Terminate existing deploy"))
|
||||
{
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok");
|
||||
}
|
||||
else
|
||||
{
|
||||
LobbyApi.TerminateLobbyService(_key.Trim(), _name.Trim(), res =>
|
||||
{
|
||||
EditorUtility.DisplayDialog("Success", $"The lobby service will start terminating (shutting down the deploy) now", "Ok");
|
||||
}, error =>
|
||||
{
|
||||
EditorUtility.DisplayDialog("Failed to terminate lobby", $"The following error happened while trying to terminate the lobby service:\n\n{error}", "Ok");
|
||||
});
|
||||
}
|
||||
}
|
||||
EditorGUILayout.HelpBox(new GUIContent("Done with your lobby?\nEnter the same name as creation to shut it down"));
|
||||
}
|
||||
private void RefreshStatus()
|
||||
{
|
||||
// Stop if window is closed
|
||||
if (!this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LobbyApi.GetLobbyService(_key, _name, res =>
|
||||
{
|
||||
if (!res.HasValue)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Failed to create lobby", $"The lobby seems to have vanished while waiting for it to deploy.", "Ok");
|
||||
waitingStatus = false;
|
||||
Repaint();
|
||||
return;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(res.Value.url))
|
||||
{
|
||||
onLobby(res.Value.url);
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
_lastStatus = res.Value.status;
|
||||
Repaint();
|
||||
Thread.Sleep(100); // :( but this is a lazy editor script, its fiiine
|
||||
RefreshStatus();
|
||||
}, error =>
|
||||
{
|
||||
EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) a lobby:\n\n{error}", "Ok");
|
||||
waitingStatus = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25579cc004424981bf0b05bcec65df0a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using kcp2k;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
namespace Edgegap
|
||||
{
|
||||
[CustomEditor(typeof(EdgegapLobbyKcpTransport))]
|
||||
public class EncryptionTransportInspector : UnityEditor.Editor
|
||||
{
|
||||
SerializedProperty lobbyUrlProperty;
|
||||
SerializedProperty lobbyWaitTimeoutProperty;
|
||||
private List<SerializedProperty> kcpProperties = new List<SerializedProperty>();
|
||||
|
||||
|
||||
// Assuming proper SerializedProperty definitions for properties
|
||||
// Add more SerializedProperty fields related to different modes as needed
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
lobbyUrlProperty = serializedObject.FindProperty("lobbyUrl");
|
||||
lobbyWaitTimeoutProperty = serializedObject.FindProperty("lobbyWaitTimeout");
|
||||
// Get public fields from KcpTransport
|
||||
kcpProperties.Clear();
|
||||
FieldInfo[] fields = typeof(KcpTransport).GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
SerializedProperty prop = serializedObject.FindProperty(field.Name);
|
||||
if (prop == null)
|
||||
{
|
||||
// callbacks have no property
|
||||
continue;
|
||||
}
|
||||
kcpProperties.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
EditorGUILayout.PropertyField(lobbyUrlProperty);
|
||||
if (GUILayout.Button("Create & Deploy Lobby"))
|
||||
{
|
||||
var input = CreateInstance<LobbyServiceCreateDialogue>();
|
||||
input.onLobby = (url) =>
|
||||
{
|
||||
lobbyUrlProperty.stringValue = url;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
};
|
||||
input.ShowUtility();
|
||||
}
|
||||
EditorGUILayout.PropertyField(lobbyWaitTimeoutProperty);
|
||||
EditorGUILayout.Separator();
|
||||
foreach (SerializedProperty prop in kcpProperties)
|
||||
{
|
||||
EditorGUILayout.PropertyField(prop);
|
||||
}
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d7cc53263184754a4682335440df515
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9b459cf5e084bdd8b196df849a2c519
|
||||
timeCreated: 1709953502
|
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions#functions
|
||||
[Serializable]
|
||||
public struct ListLobbiesResponse
|
||||
{
|
||||
public int count;
|
||||
public LobbyBrief[] data;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdb37041d9464f8c90ac86942b940565
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions#getting-a-specific-lobbys-information
|
||||
[Serializable]
|
||||
public struct Lobby
|
||||
{
|
||||
[Serializable]
|
||||
public struct Player
|
||||
{
|
||||
public uint authorization_token;
|
||||
public string id;
|
||||
public bool is_host;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct Port
|
||||
{
|
||||
public string name;
|
||||
public int port;
|
||||
public string protocol;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct Assignment
|
||||
{
|
||||
public uint authorization_token;
|
||||
public string host;
|
||||
public string ip;
|
||||
public Port[] ports;
|
||||
}
|
||||
|
||||
public Assignment assignment;
|
||||
public string name;
|
||||
public string lobby_id;
|
||||
public bool is_joinable;
|
||||
public bool is_started;
|
||||
public int player_count;
|
||||
public int capacity;
|
||||
public int available_slots => capacity - player_count;
|
||||
public string[] tags;
|
||||
public Player[] players;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64db55f096cd4ace83e1aa1c0c0588f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
namespace Edgegap
|
||||
{
|
||||
// Brief lobby data, returned by the list function
|
||||
[Serializable]
|
||||
public struct LobbyBrief
|
||||
{
|
||||
public string lobby_id;
|
||||
public string name;
|
||||
public bool is_joinable;
|
||||
public bool is_started;
|
||||
public int player_count;
|
||||
public int capacity;
|
||||
public int available_slots => capacity - player_count;
|
||||
public string[] tags;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6018ece006144e719c6b3f0d4e256d7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions#creating-a-new-lobby
|
||||
[Serializable]
|
||||
public struct LobbyCreateRequest
|
||||
{
|
||||
[Serializable]
|
||||
public struct Player
|
||||
{
|
||||
public string id;
|
||||
}
|
||||
[Serializable]
|
||||
public struct Annotation
|
||||
{
|
||||
public bool inject;
|
||||
public string key;
|
||||
public string value;
|
||||
}
|
||||
public Annotation[] annotations; // todo
|
||||
public int capacity;
|
||||
public bool is_joinable;
|
||||
public string name;
|
||||
public Player player;
|
||||
public string[] tags;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4040c1adafc3449eaebd3bd22aa3ff26
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions/#starting-a-lobby
|
||||
[Serializable]
|
||||
public struct LobbyIdRequest
|
||||
{
|
||||
public string lobby_id;
|
||||
public LobbyIdRequest(string lobbyId)
|
||||
{
|
||||
lobby_id = lobbyId;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 219c7fba8724473caf170c6254e6dc45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby
|
||||
// https://docs.edgegap.com/docs/lobby/functions#leaving-a-lobby
|
||||
[Serializable]
|
||||
public struct LobbyJoinOrLeaveRequest
|
||||
{
|
||||
[Serializable]
|
||||
public struct Player
|
||||
{
|
||||
public string id;
|
||||
}
|
||||
public string lobby_id;
|
||||
public Player player;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4091d555e62341f0ac30479952d517aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
namespace Edgegap
|
||||
{
|
||||
// https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby
|
||||
[Serializable]
|
||||
public struct LobbyUpdateRequest
|
||||
{
|
||||
public int capacity;
|
||||
public bool is_joinable;
|
||||
public string[] tags;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee158bc379f44cdf9904578f37a5e7a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta
Normal file
8
Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62c28e855fc644011b4079c268b46b71
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,141 @@
|
||||
// overwrite RawSend/Receive
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class EdgegapKcpClient : KcpClient
|
||||
{
|
||||
// need buffer larger than KcpClient.rawReceiveBuffer to add metadata
|
||||
readonly byte[] relayReceiveBuffer;
|
||||
|
||||
// authentication
|
||||
public uint userId;
|
||||
public uint sessionId;
|
||||
public ConnectionState connectionState = ConnectionState.Disconnected;
|
||||
|
||||
// ping
|
||||
double lastPingTime;
|
||||
|
||||
public EdgegapKcpClient(
|
||||
Action OnConnected,
|
||||
Action<ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action OnDisconnected,
|
||||
Action<ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
: base(OnConnected, OnData, OnDisconnected, OnError, config)
|
||||
{
|
||||
relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead];
|
||||
}
|
||||
|
||||
// custom start function with relay parameters; connects udp client.
|
||||
public void Connect(string relayAddress, ushort relayPort, uint userId, uint sessionId)
|
||||
{
|
||||
// reset last state
|
||||
connectionState = ConnectionState.Checking;
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
// reuse base connect
|
||||
base.Connect(relayAddress, relayPort);
|
||||
}
|
||||
|
||||
// parse metadata, then pass to kcp
|
||||
protected override bool RawReceive(out ArraySegment<byte> segment)
|
||||
{
|
||||
segment = default;
|
||||
if (socket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment<byte> content))
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(content))
|
||||
{
|
||||
// parse message type
|
||||
if (reader.Remaining == 0)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapClient: message of {content.Count} is too small to parse.");
|
||||
return false;
|
||||
}
|
||||
byte messageType = reader.ReadByte();
|
||||
|
||||
// handle message type
|
||||
switch (messageType)
|
||||
{
|
||||
case (byte)MessageType.Ping:
|
||||
{
|
||||
// parse state
|
||||
if (reader.Remaining < 1) return false;
|
||||
ConnectionState last = connectionState;
|
||||
connectionState = (ConnectionState)reader.ReadByte();
|
||||
|
||||
// log state changes for debugging.
|
||||
if (connectionState != last) Debug.Log($"EdgegapClient: state updated to: {connectionState}");
|
||||
|
||||
// return true indicates Mirror to keep checking
|
||||
// for further messages.
|
||||
return true;
|
||||
}
|
||||
case (byte)MessageType.Data:
|
||||
{
|
||||
segment = reader.ReadBytesSegment(reader.Remaining);
|
||||
return true;
|
||||
}
|
||||
// wrong message type. return false, don't throw.
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Info($"EdgegapClient: looks like the other end has closed the connection. This is fine: {e}");
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void RawSend(ArraySegment<byte> data)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Data);
|
||||
writer.WriteBytes(data.Array, data.Offset, data.Count);
|
||||
base.RawSend(writer);
|
||||
}
|
||||
}
|
||||
|
||||
void SendPing()
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Ping);
|
||||
base.RawSend(writer);
|
||||
}
|
||||
}
|
||||
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
// ping every interval for keepalive & handshake
|
||||
if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval)
|
||||
{
|
||||
SendPing();
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
base.TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0d6fba7098f4ea3949d0195e8276adc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class EdgegapKcpServer : KcpServer
|
||||
{
|
||||
// need buffer larger than KcpClient.rawReceiveBuffer to add metadata
|
||||
readonly byte[] relayReceiveBuffer;
|
||||
|
||||
// authentication
|
||||
public uint userId;
|
||||
public uint sessionId;
|
||||
public ConnectionState state = ConnectionState.Disconnected;
|
||||
|
||||
// server is an UDP client talking to relay
|
||||
protected Socket relaySocket;
|
||||
public EndPoint remoteEndPoint;
|
||||
|
||||
// ping
|
||||
double lastPingTime;
|
||||
|
||||
// custom 'active'. while connected to relay
|
||||
bool relayActive;
|
||||
|
||||
public EdgegapKcpServer(
|
||||
Action<int, IPEndPoint> OnConnected,
|
||||
Action<int, ArraySegment<byte>, KcpChannel> OnData,
|
||||
Action<int> OnDisconnected,
|
||||
Action<int, ErrorCode, string> OnError,
|
||||
KcpConfig config)
|
||||
// TODO don't call base. don't listen to local UdpServer at all?
|
||||
: base(OnConnected, OnData, OnDisconnected, OnError, config)
|
||||
{
|
||||
relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead];
|
||||
}
|
||||
|
||||
public override bool IsActive() => relayActive;
|
||||
|
||||
// custom start function with relay parameters; connects udp client.
|
||||
public void Start(string relayAddress, ushort relayPort, uint userId, uint sessionId)
|
||||
{
|
||||
// reset last state
|
||||
state = ConnectionState.Checking;
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
|
||||
// try resolve host name
|
||||
if (!Common.ResolveHostname(relayAddress, out IPAddress[] addresses))
|
||||
{
|
||||
OnError(0, ErrorCode.DnsResolve, $"Failed to resolve host: {relayAddress}");
|
||||
return;
|
||||
}
|
||||
|
||||
// create socket
|
||||
remoteEndPoint = new IPEndPoint(addresses[0], relayPort);
|
||||
relaySocket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
|
||||
relaySocket.Blocking = false;
|
||||
|
||||
// configure buffer sizes
|
||||
Common.ConfigureSocketBuffers(relaySocket, config.RecvBufferSize, config.SendBufferSize);
|
||||
|
||||
// bind to endpoint for Send/Receive instead of SendTo/ReceiveFrom
|
||||
relaySocket.Connect(remoteEndPoint);
|
||||
relayActive = true;
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
relayActive = false;
|
||||
}
|
||||
|
||||
protected override bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
|
||||
{
|
||||
segment = default;
|
||||
connectionId = 0;
|
||||
|
||||
if (relaySocket == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
// TODO need separate buffer. don't write into result yet. only payload
|
||||
|
||||
if (relaySocket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment<byte> content))
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(content))
|
||||
{
|
||||
// parse message type
|
||||
if (reader.Remaining == 0)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse header.");
|
||||
return false;
|
||||
}
|
||||
byte messageType = reader.ReadByte();
|
||||
|
||||
// handle message type
|
||||
switch (messageType)
|
||||
{
|
||||
case (byte)MessageType.Ping:
|
||||
{
|
||||
// parse state
|
||||
if (reader.Remaining < 1) return false;
|
||||
ConnectionState last = state;
|
||||
state = (ConnectionState)reader.ReadByte();
|
||||
|
||||
// log state changes for debugging.
|
||||
if (state != last) Debug.Log($"EdgegapServer: state updated to: {state}");
|
||||
|
||||
// return true indicates Mirror to keep checking
|
||||
// for further messages.
|
||||
return true;
|
||||
}
|
||||
case (byte)MessageType.Data:
|
||||
{
|
||||
// parse connectionId and payload
|
||||
if (reader.Remaining <= 4)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse connId.");
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionId = reader.ReadInt();
|
||||
segment = reader.ReadBytesSegment(reader.Remaining);
|
||||
// Debug.Log($"EdgegapServer: receiving from connId={connectionId}: {segment.ToHexString()}");
|
||||
return true;
|
||||
}
|
||||
// wrong message type. return false, don't throw.
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Info($"EdgegapServer: looks like the other end has closed the connection. This is fine: {e}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void RawSend(int connectionId, ArraySegment<byte> data)
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// Debug.Log($"EdgegapServer: sending to connId={connectionId}: {data.ToHexString()}");
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Data);
|
||||
writer.WriteInt(connectionId);
|
||||
writer.WriteBytes(data.Array, data.Offset, data.Count);
|
||||
ArraySegment<byte> message = writer;
|
||||
|
||||
try
|
||||
{
|
||||
relaySocket.SendNonBlocking(message);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Log.Error($"KcpRleayServer: RawSend failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SendPing()
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteUInt(userId);
|
||||
writer.WriteUInt(sessionId);
|
||||
writer.WriteByte((byte)MessageType.Ping);
|
||||
ArraySegment<byte> message = writer;
|
||||
|
||||
try
|
||||
{
|
||||
relaySocket.SendNonBlocking(message);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Debug.LogWarning($"EdgegapServer: failed to ping. perhaps the relay isn't running? {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void TickOutgoing()
|
||||
{
|
||||
if (relayActive)
|
||||
{
|
||||
// ping every interval for keepalive & handshake
|
||||
if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval)
|
||||
{
|
||||
SendPing();
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// base processing
|
||||
base.TickOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd8551078397248b0848950352c208ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,162 @@
|
||||
// edgegap relay transport.
|
||||
// reuses KcpTransport with custom KcpServer/Client.
|
||||
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
using kcp2k;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")]
|
||||
public class EdgegapKcpTransport : KcpTransport
|
||||
{
|
||||
[Header("Relay")]
|
||||
public string relayAddress = "127.0.0.1";
|
||||
public ushort relayGameServerPort = 8888;
|
||||
public ushort relayGameClientPort = 9999;
|
||||
|
||||
// mtu for kcp transport. respects relay overhead.
|
||||
public const int MaxPayload = Kcp.MTU_DEF - Protocol.Overhead;
|
||||
|
||||
[Header("Relay")]
|
||||
public bool relayGUI = true;
|
||||
public uint userId = 11111111;
|
||||
public uint sessionId = 22222222;
|
||||
|
||||
// helper
|
||||
internal static String ReParse(String cmd, String pattern, String defaultValue)
|
||||
{
|
||||
Match match = Regex.Match(cmd, pattern);
|
||||
return match.Success ? match.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
// logging
|
||||
// Log.Info should use Debug.Log if enabled, or nothing otherwise
|
||||
// (don't want to spam the console on headless servers)
|
||||
if (debugLog)
|
||||
Log.Info = Debug.Log;
|
||||
else
|
||||
Log.Info = _ => {};
|
||||
Log.Warning = Debug.LogWarning;
|
||||
Log.Error = Debug.LogError;
|
||||
|
||||
// create config from serialized settings.
|
||||
// with MaxPayload as max size to respect relay overhead.
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MaxPayload, NoDelay, Interval, FastResend, false, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new EdgegapKcpClient(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
|
||||
() => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708
|
||||
(error, reason) => OnClientError.Invoke(ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new EdgegapKcpServer(
|
||||
(connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()),
|
||||
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
(connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason),
|
||||
config);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Debug.Log("EdgegapTransport initialized!");
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MaxPayload, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MaxPayload);
|
||||
}
|
||||
|
||||
// client overwrites to use EdgegapClient instead of KcpClient
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
// connect to relay address:port instead of the expected server address
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
client.userId = userId;
|
||||
client.sessionId = sessionId;
|
||||
client.connectionState = ConnectionState.Checking; // reset from last time
|
||||
client.Connect(relayAddress, relayGameClientPort);
|
||||
}
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
// connect to relay address:port instead of the expected server address
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
client.Connect(relayAddress, relayGameClientPort, userId, sessionId);
|
||||
}
|
||||
|
||||
// server overwrites to use EdgegapServer instead of KcpServer
|
||||
public override void ServerStart()
|
||||
{
|
||||
// start the server
|
||||
EdgegapKcpServer server = (EdgegapKcpServer)this.server;
|
||||
server.Start(relayAddress, relayGameServerPort, userId, sessionId);
|
||||
}
|
||||
|
||||
void OnGUIRelay()
|
||||
{
|
||||
// if (server.IsActive()) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(300, 30, 200, 100));
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("SessionId:");
|
||||
sessionId = Convert.ToUInt32(GUILayout.TextField(sessionId.ToString()));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("UserId:");
|
||||
userId = Convert.ToUInt32(GUILayout.TextField(userId.ToString()));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
EdgegapKcpServer server = (EdgegapKcpServer)this.server;
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("State:");
|
||||
GUILayout.Label(server.state.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
else if (NetworkClient.active)
|
||||
{
|
||||
EdgegapKcpClient client = (EdgegapKcpClient)this.client;
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("State:");
|
||||
GUILayout.Label(client.connectionState.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
// base OnGUI only shows in editor & development builds.
|
||||
// here we always show it because we need the sessionid & userid buttons.
|
||||
#pragma warning disable CS0109
|
||||
new void OnGUI()
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
base.OnGUI();
|
||||
#endif
|
||||
if (relayGUI) OnGUIRelay();
|
||||
}
|
||||
|
||||
public override string ToString() => "Edgegap Kcp Transport";
|
||||
}
|
||||
#pragma warning restore CS0109
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2d1e0e17f753449798fa27474d6b86b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs
|
||||
uploadId: 736421
|
29
Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs
Normal file
29
Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// relay protocol definitions
|
||||
namespace Edgegap
|
||||
{
|
||||
public enum ConnectionState : byte
|
||||
{
|
||||
Disconnected = 0, // until the user calls connect()
|
||||
Checking = 1, // recently connected, validation in progress
|
||||
Valid = 2, // validation succeeded
|
||||
Invalid = 3, // validation rejected by tower
|
||||
SessionTimeout = 4, // session owner timed out
|
||||
Error = 5, // other error
|
||||
}
|
||||
|
||||
public enum MessageType : byte
|
||||
{
|
||||
Ping = 1,
|
||||
Data = 2
|
||||
}
|
||||
|
||||
public static class Protocol
|
||||
{
|
||||
// MTU: relay adds up to 13 bytes of metadata in the worst case.
|
||||
public const int Overhead = 13;
|
||||
|
||||
// ping interval should be between 100 ms and 1 second.
|
||||
// faster ping gives faster authentication, but higher bandwidth.
|
||||
public const float PingInterval = 0.5f;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eac30312ba61470b849e368af3c3b0e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs
|
||||
uploadId: 736421
|
20
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md
Normal file
20
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Edgegap Relay for Mirror
|
||||
Documentation: https://docs.edgegap.com/docs/distributed-relay-manager/
|
||||
|
||||
## Prerequisites
|
||||
- Unity project set up with the Mirror networking library installed
|
||||
- Supported Versions: [Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321) and [Mirror LTS](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631)
|
||||
- EdgegapTransport module downloaded and extracted
|
||||
|
||||
## Steps
|
||||
1. Open your Unity project and navigate to the "Assets" folder.
|
||||
2. Locate the "Mirror" folder within "Assets" and open it.
|
||||
3. Within the "Mirror" folder, open the "Transports" folder.
|
||||
4. Drag and drop the "Unity" folder from the extracted EdgegapTransport files into the "Transports" folder.
|
||||
5. Open your NetworkManager script in the Unity Editor and navigate to the "Inspector" panel.
|
||||
6. In the "Inspector" panel, locate the "Network Manager" component and click the "+" button next to the "Transport" property.
|
||||
7. In the "Add Component" menu that appears, select "Edgegap Transport" to add it to the NetworkManager.
|
||||
8. Drag the newly added "Edgegap Transport" component into the "Transport" property in the "Inspector" panel.
|
||||
|
||||
## Notes
|
||||
- The EdgegapTransport module is only compatible with Mirror and Mirror LTS versions.
|
14
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta
Normal file
14
Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ade7c960d8fe4e94970ddd88ede3bca
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md
|
||||
uploadId: 736421
|
@ -0,0 +1,25 @@
|
||||
// parse session_id and user_id from command line args.
|
||||
// mac: "open mirror.app --args session_id=123 user_id=456"
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Edgegap
|
||||
{
|
||||
public class RelayCredentialsFromArgs : MonoBehaviour
|
||||
{
|
||||
void Awake()
|
||||
{
|
||||
String cmd = Environment.CommandLine;
|
||||
|
||||
// parse session_id via regex
|
||||
String sessionId = EdgegapKcpTransport.ReParse(cmd, "session_id=(\\d+)", "111111");
|
||||
String userID = EdgegapKcpTransport.ReParse(cmd, "user_id=(\\d+)", "222222");
|
||||
Debug.Log($"Parsed sessionId: {sessionId} user_id: {userID}");
|
||||
|
||||
// configure transport
|
||||
EdgegapKcpTransport transport = GetComponent<EdgegapKcpTransport>();
|
||||
transport.sessionId = UInt32.Parse(sessionId);
|
||||
transport.userId = UInt32.Parse(userID);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9ec7091b26c4d3882f4b42f10f9b8c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs
|
||||
uploadId: 736421
|
BIN
Assets/Mirror/Transports/Edgegap/edgegap.png
Normal file
BIN
Assets/Mirror/Transports/Edgegap/edgegap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
130
Assets/Mirror/Transports/Edgegap/edgegap.png.meta
Normal file
130
Assets/Mirror/Transports/Edgegap/edgegap.png.meta
Normal file
@ -0,0 +1,130 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ea6ff15cda674a57b0c7c8b7dc1878c
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 12
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMasterTextureLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 16
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Server
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
spritePackingTag:
|
||||
pSDRemoveMatte: 0
|
||||
pSDShowRemoveMatteOption: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Edgegap/edgegap.png
|
||||
uploadId: 736421
|
3
Assets/Mirror/Transports/Encryption.meta
Normal file
3
Assets/Mirror/Transports/Encryption.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 741b3c7e5d0842049ff50a2f6e27ca12
|
||||
timeCreated: 1708015148
|
3
Assets/Mirror/Transports/Encryption/Editor.meta
Normal file
3
Assets/Mirror/Transports/Encryption/Editor.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d3cd9d7d6e84a578f7e4b384ff813f1
|
||||
timeCreated: 1708793986
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "EncryptionTransportEditor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:627104647b9c04b4ebb8978a92ecac63"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c9c7b0ef83e6e945b276d644816a489
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef
|
||||
uploadId: 736421
|
@ -0,0 +1,90 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
[CustomEditor(typeof(EncryptionTransport), true)]
|
||||
public class EncryptionTransportInspector : UnityEditor.Editor
|
||||
{
|
||||
SerializedProperty innerProperty;
|
||||
SerializedProperty clientValidatesServerPubKeyProperty;
|
||||
SerializedProperty clientTrustedPubKeySignaturesProperty;
|
||||
SerializedProperty serverKeypairPathProperty;
|
||||
SerializedProperty serverLoadKeyPairFromFileProperty;
|
||||
|
||||
// Assuming proper SerializedProperty definitions for properties
|
||||
// Add more SerializedProperty fields related to different modes as needed
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
innerProperty = serializedObject.FindProperty("Inner");
|
||||
clientValidatesServerPubKeyProperty = serializedObject.FindProperty("ClientValidateServerPubKey");
|
||||
clientTrustedPubKeySignaturesProperty = serializedObject.FindProperty("ClientTrustedPubKeySignatures");
|
||||
serverKeypairPathProperty = serializedObject.FindProperty("ServerKeypairPath");
|
||||
serverLoadKeyPairFromFileProperty = serializedObject.FindProperty("ServerLoadKeyPairFromFile");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
// Draw default inspector for the parent class
|
||||
DrawDefaultInspector();
|
||||
EditorGUILayout.LabelField("Encryption Settings", EditorStyles.boldLabel);
|
||||
if (innerProperty != null)
|
||||
{
|
||||
EditorGUILayout.LabelField("Common", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(innerProperty);
|
||||
EditorGUILayout.Separator();
|
||||
}
|
||||
// Client Section
|
||||
EditorGUILayout.LabelField("Client", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox("Validating the servers public key is essential for complete man-in-the-middle (MITM) safety, but might not be feasible for all modes of hosting.", MessageType.Info);
|
||||
EditorGUILayout.PropertyField(clientValidatesServerPubKeyProperty, new GUIContent("Validate Server Public Key"));
|
||||
|
||||
EncryptionTransport.ValidationMode validationMode = (EncryptionTransport.ValidationMode)clientValidatesServerPubKeyProperty.enumValueIndex;
|
||||
|
||||
switch (validationMode)
|
||||
{
|
||||
case EncryptionTransport.ValidationMode.List:
|
||||
EditorGUILayout.PropertyField(clientTrustedPubKeySignaturesProperty);
|
||||
break;
|
||||
case EncryptionTransport.ValidationMode.Callback:
|
||||
EditorGUILayout.HelpBox("Please set the EncryptionTransport.onClientValidateServerPubKey at runtime.", MessageType.Info);
|
||||
break;
|
||||
}
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
// Server Section
|
||||
EditorGUILayout.LabelField("Server", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(serverLoadKeyPairFromFileProperty, new GUIContent("Load Keypair From File"));
|
||||
if (serverLoadKeyPairFromFileProperty.boolValue)
|
||||
{
|
||||
EditorGUILayout.PropertyField(serverKeypairPathProperty, new GUIContent("Keypair File Path"));
|
||||
}
|
||||
if(GUILayout.Button("Generate Key Pair"))
|
||||
{
|
||||
EncryptionCredentials keyPair = EncryptionCredentials.Generate();
|
||||
string path = EditorUtility.SaveFilePanel("Select where to save the keypair", "", "server-keys.json", "json");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
keyPair.SaveToFile(path);
|
||||
EditorUtility.DisplayDialog("KeyPair Saved", $"Successfully saved the keypair.\nThe fingerprint is {keyPair.PublicKeyFingerprint}, you can also retrieve it from the saved json file at any point.", "Ok");
|
||||
if (validationMode == EncryptionTransport.ValidationMode.List)
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Add key to trusted list?", "Do you also want to add the generated key to the trusted list?", "Yes", "No"))
|
||||
{
|
||||
clientTrustedPubKeySignaturesProperty.arraySize++;
|
||||
clientTrustedPubKeySignaturesProperty.GetArrayElementAtIndex(clientTrustedPubKeySignaturesProperty.arraySize - 1).stringValue = keyPair.PublicKeyFingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(ThreadedEncryptionKcpTransport), true)]
|
||||
class EncryptionThreadedTransportInspector : EncryptionTransportInspector {}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871580d2094a46139279d651cec92b5d
|
||||
timeCreated: 1708794004
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs
|
||||
uploadId: 736421
|
554
Assets/Mirror/Transports/Encryption/EncryptedConnection.cs
Normal file
554
Assets/Mirror/Transports/Encryption/EncryptedConnection.cs
Normal file
@ -0,0 +1,554 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Mirror.BouncyCastle.Crypto;
|
||||
using Mirror.BouncyCastle.Crypto.Agreement;
|
||||
using Mirror.BouncyCastle.Crypto.Digests;
|
||||
using Mirror.BouncyCastle.Crypto.Generators;
|
||||
using Mirror.BouncyCastle.Crypto.Modes;
|
||||
using Mirror.BouncyCastle.Crypto.Parameters;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
public class EncryptedConnection
|
||||
{
|
||||
// 256-bit key
|
||||
const int KeyLength = 32;
|
||||
// 512-bit salt for the key derivation function
|
||||
const int HkdfSaltSize = KeyLength * 2;
|
||||
|
||||
// Info tag for the HKDF, this just adds more entropy
|
||||
static readonly byte[] HkdfInfo = Encoding.UTF8.GetBytes("Mirror/EncryptionTransport");
|
||||
|
||||
// fixed size of the unique per-packet nonce. Defaults to 12 bytes/96 bits (not recommended to be changed)
|
||||
const int NonceSize = 12;
|
||||
|
||||
// this is the size of the "checksum" included in each encrypted payload
|
||||
// 16 bytes/128 bytes is the recommended value for best security
|
||||
// can be reduced to 12 bytes for a small space savings, but makes encryption slightly weaker.
|
||||
// Setting it lower than 12 bytes is not recommended
|
||||
const int MacSizeBytes = 16;
|
||||
|
||||
const int MacSizeBits = MacSizeBytes * 8;
|
||||
|
||||
// How much metadata overhead we have for regular packets
|
||||
public const int Overhead = sizeof(OpCodes) + MacSizeBytes + NonceSize;
|
||||
|
||||
// After how many seconds of not receiving a handshake packet we should time out
|
||||
const double DurationTimeout = 2; // 2s
|
||||
|
||||
// After how many seconds to assume the last handshake packet got lost and to resend another one
|
||||
const double DurationResend = 0.05; // 50ms
|
||||
|
||||
|
||||
// Static fields for allocation efficiency, makes this not thread safe
|
||||
// It'd be as easy as using ThreadLocal though to fix that
|
||||
|
||||
// Set up a global cipher instance, it is initialised/reset before use
|
||||
// (AesFastEngine used to exist, but was removed due to side channel issues)
|
||||
// use AesUtilities.CreateEngine here as it'll pick the hardware accelerated one if available (which is will not be unless on .net core)
|
||||
static readonly ThreadLocal<GcmBlockCipher> Cipher = new ThreadLocal<GcmBlockCipher>(() => new GcmBlockCipher(AesUtilities.CreateEngine()));
|
||||
|
||||
// Set up a global HKDF with a SHA-256 digest
|
||||
static readonly ThreadLocal<HkdfBytesGenerator> Hkdf = new ThreadLocal<HkdfBytesGenerator>(() => new HkdfBytesGenerator(new Sha256Digest()));
|
||||
|
||||
// Global byte array to store nonce sent by the remote side, they're used immediately after
|
||||
static readonly ThreadLocal<byte[]> ReceiveNonce = new ThreadLocal<byte[]>(() => new byte[NonceSize]);
|
||||
|
||||
// Buffer for the remote salt, as bouncycastle needs to take a byte[] *rolls eyes*
|
||||
static readonly ThreadLocal<byte[]> TMPRemoteSaltBuffer = new ThreadLocal<byte[]>(() => new byte[HkdfSaltSize]);
|
||||
|
||||
// buffer for encrypt/decrypt operations, resized larger as needed
|
||||
static ThreadLocal<byte[]> TMPCryptBuffer = new ThreadLocal<byte[]>(() => new byte[2048]);
|
||||
|
||||
// packet headers
|
||||
enum OpCodes : byte
|
||||
{
|
||||
// start at 1 to maybe filter out random noise
|
||||
Data = 1,
|
||||
HandshakeStart = 2,
|
||||
HandshakeAck = 3,
|
||||
HandshakeFin = 4
|
||||
}
|
||||
|
||||
enum State
|
||||
{
|
||||
// Waiting for a handshake to arrive
|
||||
// this is for _sendsFirst:
|
||||
// - false: OpCodes.HandshakeStart
|
||||
// - true: Opcodes.HandshakeAck
|
||||
WaitingHandshake,
|
||||
|
||||
// Waiting for a handshake reply/acknowledgement to arrive
|
||||
// this is for _sendsFirst:
|
||||
// - false: OpCodes.HandshakeFine
|
||||
// - true: Opcodes.Data (implicitly)
|
||||
WaitingHandshakeReply,
|
||||
|
||||
// Both sides have confirmed the keys are exchanged and data can be sent freely
|
||||
Ready
|
||||
}
|
||||
|
||||
State state = State.WaitingHandshake;
|
||||
|
||||
// Key exchange confirmed and data can be sent freely
|
||||
public bool IsReady => state == State.Ready;
|
||||
// Callback to send off encrypted data
|
||||
readonly Action<ArraySegment<byte>, int> send;
|
||||
// Callback when received data has been decrypted
|
||||
readonly Action<ArraySegment<byte>, int> receive;
|
||||
// Callback when the connection becomes ready
|
||||
readonly Action ready;
|
||||
// On-error callback, disconnect expected
|
||||
readonly Action<TransportError, string> error;
|
||||
// Optional callback to validate the remotes public key, validation on one side is necessary to ensure MITM resistance
|
||||
// (usually client validates the server key)
|
||||
readonly Func<PubKeyInfo, bool> validateRemoteKey;
|
||||
// Our asymmetric credentials for the initial DH exchange
|
||||
EncryptionCredentials credentials;
|
||||
readonly byte[] hkdfSalt;
|
||||
NetworkReader _tmpReader = new NetworkReader(new ArraySegment<byte>());
|
||||
|
||||
// After no handshake packet in this many seconds, the handshake fails
|
||||
double handshakeTimeout;
|
||||
// When to assume the last handshake packet got lost and to resend another one
|
||||
double nextHandshakeResend;
|
||||
|
||||
|
||||
// we can reuse the _cipherParameters here since the nonce is stored as the byte[] reference we pass in
|
||||
// so we can update it without creating a new AeadParameters instance
|
||||
// this might break in the future! (will cause bad data)
|
||||
byte[] nonce = new byte[NonceSize];
|
||||
AeadParameters cipherParametersEncrypt;
|
||||
AeadParameters cipherParametersDecrypt;
|
||||
|
||||
|
||||
/*
|
||||
* Specifies if we send the first key, then receive ack, then send fin
|
||||
* Or the opposite if set to false
|
||||
*
|
||||
* The client does this, since the fin is not acked explicitly, but by receiving data to decrypt
|
||||
*/
|
||||
readonly bool sendsFirst;
|
||||
|
||||
public EncryptedConnection(EncryptionCredentials credentials,
|
||||
bool isClient,
|
||||
Action<ArraySegment<byte>, int> sendAction,
|
||||
Action<ArraySegment<byte>, int> receiveAction,
|
||||
Action readyAction,
|
||||
Action<TransportError, string> errorAction,
|
||||
Func<PubKeyInfo, bool> validateRemoteKey = null)
|
||||
{
|
||||
this.credentials = credentials;
|
||||
sendsFirst = isClient;
|
||||
if (!sendsFirst)
|
||||
// salt is controlled by the server
|
||||
hkdfSalt = GenerateSecureBytes(HkdfSaltSize);
|
||||
send = sendAction;
|
||||
receive = receiveAction;
|
||||
ready = readyAction;
|
||||
error = errorAction;
|
||||
this.validateRemoteKey = validateRemoteKey;
|
||||
}
|
||||
|
||||
// Generates a random starting nonce
|
||||
static byte[] GenerateSecureBytes(int size)
|
||||
{
|
||||
byte[] bytes = new byte[size];
|
||||
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||
rng.GetBytes(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
public void OnReceiveRaw(ArraySegment<byte> data, int channel)
|
||||
{
|
||||
if (data.Count < 1)
|
||||
{
|
||||
error(TransportError.Unexpected, "Received empty packet");
|
||||
return;
|
||||
}
|
||||
|
||||
_tmpReader.SetBuffer(data);
|
||||
OpCodes opcode = (OpCodes)_tmpReader.ReadByte();
|
||||
switch (opcode)
|
||||
{
|
||||
case OpCodes.Data:
|
||||
// first sender ready is implicit when data is received
|
||||
if (sendsFirst && state == State.WaitingHandshakeReply)
|
||||
SetReady();
|
||||
else if (!IsReady)
|
||||
error(TransportError.Unexpected, "Unexpected data while not ready.");
|
||||
|
||||
if (_tmpReader.Remaining < Overhead)
|
||||
{
|
||||
error(TransportError.Unexpected, "received data packet smaller than metadata size");
|
||||
return;
|
||||
}
|
||||
|
||||
ArraySegment<byte> ciphertext = _tmpReader.ReadBytesSegment(_tmpReader.Remaining - NonceSize);
|
||||
_tmpReader.ReadBytes(ReceiveNonce.Value, NonceSize);
|
||||
|
||||
Profiler.BeginSample("EncryptedConnection.Decrypt");
|
||||
ArraySegment<byte> plaintext = Decrypt(ciphertext);
|
||||
Profiler.EndSample();
|
||||
if (plaintext.Count == 0)
|
||||
// error
|
||||
return;
|
||||
receive(plaintext, channel);
|
||||
break;
|
||||
case OpCodes.HandshakeStart:
|
||||
if (sendsFirst)
|
||||
{
|
||||
error(TransportError.Unexpected, "Received HandshakeStart packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State.WaitingHandshakeReply)
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
|
||||
state = State.WaitingHandshakeReply;
|
||||
ResetTimeouts();
|
||||
CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), hkdfSalt);
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeAck);
|
||||
break;
|
||||
case OpCodes.HandshakeAck:
|
||||
if (!sendsFirst)
|
||||
{
|
||||
error(TransportError.Unexpected, "Received HandshakeAck packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsReady)
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
|
||||
if (state == State.WaitingHandshakeReply)
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
|
||||
|
||||
state = State.WaitingHandshakeReply;
|
||||
ResetTimeouts();
|
||||
_tmpReader.ReadBytes(TMPRemoteSaltBuffer.Value, HkdfSaltSize);
|
||||
CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), TMPRemoteSaltBuffer.Value);
|
||||
SendHandshakeFin();
|
||||
break;
|
||||
case OpCodes.HandshakeFin:
|
||||
if (sendsFirst)
|
||||
{
|
||||
error(TransportError.Unexpected, "Received HandshakeFin packet, we don't expect this.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsReady)
|
||||
// this is fine, packets may arrive out of order
|
||||
return;
|
||||
|
||||
if (state != State.WaitingHandshakeReply)
|
||||
{
|
||||
error(TransportError.Unexpected,
|
||||
"Received HandshakeFin packet, we didn't expect this yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetReady();
|
||||
|
||||
break;
|
||||
default:
|
||||
error(TransportError.InvalidReceive, $"Unhandled opcode {(byte)opcode:x}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SetReady()
|
||||
{
|
||||
// done with credentials, null out the reference
|
||||
credentials = null;
|
||||
|
||||
state = State.Ready;
|
||||
ready();
|
||||
}
|
||||
|
||||
void ResetTimeouts()
|
||||
{
|
||||
handshakeTimeout = 0;
|
||||
nextHandshakeResend = -1;
|
||||
}
|
||||
|
||||
public void Send(ArraySegment<byte> data, int channel)
|
||||
{
|
||||
using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)OpCodes.Data);
|
||||
Profiler.BeginSample("EncryptedConnection.Encrypt");
|
||||
ArraySegment<byte> encrypted = Encrypt(data);
|
||||
Profiler.EndSample();
|
||||
|
||||
if (encrypted.Count == 0)
|
||||
// error
|
||||
return;
|
||||
writer.WriteBytes(encrypted.Array, 0, encrypted.Count);
|
||||
// write nonce after since Encrypt will update it
|
||||
writer.WriteBytes(nonce, 0, NonceSize);
|
||||
send(writer.ToArraySegment(), channel);
|
||||
}
|
||||
}
|
||||
|
||||
ArraySegment<byte> Encrypt(ArraySegment<byte> plaintext)
|
||||
{
|
||||
if (plaintext.Count == 0)
|
||||
// Invalid
|
||||
return new ArraySegment<byte>();
|
||||
// Need to make the nonce unique again before encrypting another message
|
||||
UpdateNonce();
|
||||
// Re-initialize the cipher with our cached parameters
|
||||
Cipher.Value.Init(true, cipherParametersEncrypt);
|
||||
|
||||
// Calculate the expected output size, this should always be input size + mac size
|
||||
int outSize = Cipher.Value.GetOutputSize(plaintext.Count);
|
||||
#if UNITY_EDITOR
|
||||
// expecting the outSize to be input size + MacSize
|
||||
if (outSize != plaintext.Count + MacSizeBytes)
|
||||
throw new Exception($"Encrypt: Unexpected output size (Expected {plaintext.Count + MacSizeBytes}, got {outSize}");
|
||||
#endif
|
||||
// Resize the static buffer to fit
|
||||
byte[] cryptBuffer = TMPCryptBuffer.Value;
|
||||
EnsureSize(ref cryptBuffer, outSize);
|
||||
TMPCryptBuffer.Value = cryptBuffer;
|
||||
|
||||
int resultLen;
|
||||
try
|
||||
{
|
||||
// Run the plain text through the cipher, ProcessBytes will only process full blocks
|
||||
resultLen =
|
||||
Cipher.Value.ProcessBytes(plaintext.Array, plaintext.Offset, plaintext.Count, cryptBuffer, 0);
|
||||
// Then run any potentially remaining partial blocks through with DoFinal (and calculate the mac)
|
||||
resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen);
|
||||
}
|
||||
// catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types
|
||||
//
|
||||
catch (Exception e)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Unexpected exception while encrypting {e.GetType()}: {e.Message}");
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
// expecting the result length to match the previously calculated input size + MacSize
|
||||
if (resultLen != outSize)
|
||||
throw new Exception($"Encrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})");
|
||||
#endif
|
||||
return new ArraySegment<byte>(cryptBuffer, 0, resultLen);
|
||||
}
|
||||
|
||||
ArraySegment<byte> Decrypt(ArraySegment<byte> ciphertext)
|
||||
{
|
||||
if (ciphertext.Count <= MacSizeBytes)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Received too short data packet (min {{MacSizeBytes + 1}}, got {ciphertext.Count})");
|
||||
// Invalid
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
// Re-initialize the cipher with our cached parameters
|
||||
Cipher.Value.Init(false, cipherParametersDecrypt);
|
||||
|
||||
// Calculate the expected output size, this should always be input size - mac size
|
||||
int outSize = Cipher.Value.GetOutputSize(ciphertext.Count);
|
||||
#if UNITY_EDITOR
|
||||
// expecting the outSize to be input size - MacSize
|
||||
if (outSize != ciphertext.Count - MacSizeBytes)
|
||||
throw new Exception($"Decrypt: Unexpected output size (Expected {ciphertext.Count - MacSizeBytes}, got {outSize}");
|
||||
#endif
|
||||
|
||||
byte[] cryptBuffer = TMPCryptBuffer.Value;
|
||||
EnsureSize(ref cryptBuffer, outSize);
|
||||
TMPCryptBuffer.Value = cryptBuffer;
|
||||
|
||||
int resultLen;
|
||||
try
|
||||
{
|
||||
// Run the ciphertext through the cipher, ProcessBytes will only process full blocks
|
||||
resultLen =
|
||||
Cipher.Value.ProcessBytes(ciphertext.Array, ciphertext.Offset, ciphertext.Count, cryptBuffer, 0);
|
||||
// Then run any potentially remaining partial blocks through with DoFinal (and calculate/check the mac)
|
||||
resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen);
|
||||
}
|
||||
// catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types
|
||||
catch (Exception e)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Unexpected exception while decrypting {e.GetType()}: {e.Message}. This usually signifies corrupt data");
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
#if UNITY_EDITOR
|
||||
// expecting the result length to match the previously calculated input size + MacSize
|
||||
if (resultLen != outSize)
|
||||
throw new Exception($"Decrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})");
|
||||
#endif
|
||||
return new ArraySegment<byte>(cryptBuffer, 0, resultLen);
|
||||
}
|
||||
|
||||
void UpdateNonce()
|
||||
{
|
||||
// increment the nonce by one
|
||||
// we need to ensure the nonce is *always* unique and not reused
|
||||
// easiest way to do this is by simply incrementing it
|
||||
for (int i = 0; i < NonceSize; i++)
|
||||
{
|
||||
nonce[i]++;
|
||||
if (nonce[i] != 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void EnsureSize(ref byte[] buffer, int size)
|
||||
{
|
||||
if (buffer.Length < size)
|
||||
// double buffer to avoid constantly resizing by a few bytes
|
||||
Array.Resize(ref buffer, Math.Max(size, buffer.Length * 2));
|
||||
}
|
||||
|
||||
void SendHandshakeAndPubKey(OpCodes opcode)
|
||||
{
|
||||
using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)opcode);
|
||||
if (opcode == OpCodes.HandshakeAck)
|
||||
writer.WriteBytes(hkdfSalt, 0, HkdfSaltSize);
|
||||
writer.WriteBytes(credentials.PublicKeySerialized, 0, credentials.PublicKeySerialized.Length);
|
||||
send(writer.ToArraySegment(), Channels.Unreliable);
|
||||
}
|
||||
}
|
||||
|
||||
void SendHandshakeFin()
|
||||
{
|
||||
using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteByte((byte)OpCodes.HandshakeFin);
|
||||
send(writer.ToArraySegment(), Channels.Unreliable);
|
||||
}
|
||||
}
|
||||
|
||||
void CompleteExchange(ArraySegment<byte> remotePubKeyRaw, byte[] salt)
|
||||
{
|
||||
AsymmetricKeyParameter remotePubKey;
|
||||
try
|
||||
{
|
||||
remotePubKey = EncryptionCredentials.DeserializePublicKey(remotePubKeyRaw);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Failed to deserialize public key of remote. {e.GetType()}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (validateRemoteKey != null)
|
||||
{
|
||||
PubKeyInfo info = new PubKeyInfo
|
||||
{
|
||||
Fingerprint = EncryptionCredentials.PubKeyFingerprint(remotePubKeyRaw),
|
||||
Serialized = remotePubKeyRaw,
|
||||
Key = remotePubKey
|
||||
};
|
||||
if (!validateRemoteKey(info))
|
||||
{
|
||||
error(TransportError.Unexpected, $"Remote public key (fingerprint: {info.Fingerprint}) failed validation. ");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate a common symmetric key from our private key and the remotes public key
|
||||
// This gives us the same key on the other side, with our public key and their remote
|
||||
// It's like magic, but with math!
|
||||
ECDHBasicAgreement ecdh = new ECDHBasicAgreement();
|
||||
ecdh.Init(credentials.PrivateKey);
|
||||
byte[] sharedSecret;
|
||||
try
|
||||
{
|
||||
sharedSecret = ecdh.CalculateAgreement(remotePubKey).ToByteArrayUnsigned();
|
||||
}
|
||||
catch
|
||||
(Exception e)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Failed to calculate the ECDH key exchange. {e.GetType()}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (salt.Length != HkdfSaltSize)
|
||||
{
|
||||
error(TransportError.Unexpected, $"Salt is expected to be {HkdfSaltSize} bytes long, got {salt.Length}.");
|
||||
return;
|
||||
}
|
||||
|
||||
Hkdf.Value.Init(new HkdfParameters(sharedSecret, salt, HkdfInfo));
|
||||
|
||||
// Allocate a buffer for the output key
|
||||
byte[] keyRaw = new byte[KeyLength];
|
||||
|
||||
// Generate the output keying material
|
||||
Hkdf.Value.GenerateBytes(keyRaw, 0, keyRaw.Length);
|
||||
|
||||
KeyParameter key = new KeyParameter(keyRaw);
|
||||
|
||||
// generate a starting nonce
|
||||
nonce = GenerateSecureBytes(NonceSize);
|
||||
|
||||
// we pass in the nonce array once (as it's stored by reference) so we can cache the AeadParameters instance
|
||||
// instead of creating a new one each encrypt/decrypt
|
||||
cipherParametersEncrypt = new AeadParameters(key, MacSizeBits, nonce);
|
||||
cipherParametersDecrypt = new AeadParameters(key, MacSizeBits, ReceiveNonce.Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* non-ready connections need to be ticked for resending key data over unreliable
|
||||
*/
|
||||
public void TickNonReady(double time)
|
||||
{
|
||||
if (IsReady)
|
||||
return;
|
||||
|
||||
// Timeout reset
|
||||
if (handshakeTimeout == 0)
|
||||
handshakeTimeout = time + DurationTimeout;
|
||||
else if (time > handshakeTimeout)
|
||||
{
|
||||
error?.Invoke(TransportError.Timeout, $"Timed out during {state}, this probably just means the other side went away which is fine.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout reset
|
||||
if (nextHandshakeResend < 0)
|
||||
{
|
||||
nextHandshakeResend = time + DurationResend;
|
||||
return;
|
||||
}
|
||||
|
||||
if (time < nextHandshakeResend)
|
||||
// Resend isn't due yet
|
||||
return;
|
||||
|
||||
nextHandshakeResend = time + DurationResend;
|
||||
switch (state)
|
||||
{
|
||||
case State.WaitingHandshake:
|
||||
if (sendsFirst)
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeStart);
|
||||
|
||||
break;
|
||||
case State.WaitingHandshakeReply:
|
||||
if (sendsFirst)
|
||||
SendHandshakeFin();
|
||||
else
|
||||
SendHandshakeAndPubKey(OpCodes.HandshakeAck);
|
||||
|
||||
break;
|
||||
case State.Ready: // IsReady is checked above & early-returned
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28f3ac4ff1d346a895d0b4ff714fb57b
|
||||
timeCreated: 1708111337
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/EncryptedConnection.cs
|
||||
uploadId: 736421
|
119
Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs
Normal file
119
Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Mirror.BouncyCastle.Asn1.Pkcs;
|
||||
using Mirror.BouncyCastle.Asn1.X509;
|
||||
using Mirror.BouncyCastle.Crypto;
|
||||
using Mirror.BouncyCastle.Crypto.Digests;
|
||||
using Mirror.BouncyCastle.Crypto.Generators;
|
||||
using Mirror.BouncyCastle.X509;
|
||||
using Mirror.BouncyCastle.Crypto.Parameters;
|
||||
using Mirror.BouncyCastle.Pkcs;
|
||||
using Mirror.BouncyCastle.Security;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
public class EncryptionCredentials
|
||||
{
|
||||
const int PrivateKeyBits = 256;
|
||||
// don't actually need to store this currently
|
||||
// but we'll need to for loading/saving from file maybe?
|
||||
// public ECPublicKeyParameters PublicKey;
|
||||
|
||||
// The serialized public key, in DER format
|
||||
public byte[] PublicKeySerialized;
|
||||
public ECPrivateKeyParameters PrivateKey;
|
||||
public string PublicKeyFingerprint;
|
||||
|
||||
EncryptionCredentials() {}
|
||||
|
||||
// TODO: load from file
|
||||
public static EncryptionCredentials Generate()
|
||||
{
|
||||
var generator = new ECKeyPairGenerator();
|
||||
generator.Init(new KeyGenerationParameters(new SecureRandom(), PrivateKeyBits));
|
||||
AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair();
|
||||
var serialized = SerializePublicKey((ECPublicKeyParameters)keyPair.Public);
|
||||
return new EncryptionCredentials
|
||||
{
|
||||
// see fields above
|
||||
// PublicKey = (ECPublicKeyParameters)keyPair.Public,
|
||||
PublicKeySerialized = serialized,
|
||||
PublicKeyFingerprint = PubKeyFingerprint(new ArraySegment<byte>(serialized)),
|
||||
PrivateKey = (ECPrivateKeyParameters)keyPair.Private
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] SerializePublicKey(AsymmetricKeyParameter publicKey)
|
||||
{
|
||||
// apparently the best way to transmit this public key over the network is to serialize it as a DER
|
||||
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey);
|
||||
return publicKeyInfo.ToAsn1Object().GetDerEncoded();
|
||||
}
|
||||
|
||||
public static AsymmetricKeyParameter DeserializePublicKey(ArraySegment<byte> pubKey) =>
|
||||
// And then we do this to deserialize from the DER (from above)
|
||||
// the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted
|
||||
// to a byte[] first and then shoved through a MemoryStream
|
||||
PublicKeyFactory.CreateKey(new MemoryStream(pubKey.Array, pubKey.Offset, pubKey.Count, false));
|
||||
|
||||
public static byte[] SerializePrivateKey(AsymmetricKeyParameter privateKey)
|
||||
{
|
||||
// Serialize privateKey as a DER
|
||||
PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey);
|
||||
return privateKeyInfo.ToAsn1Object().GetDerEncoded();
|
||||
}
|
||||
|
||||
public static AsymmetricKeyParameter DeserializePrivateKey(ArraySegment<byte> privateKey) =>
|
||||
// And then we do this to deserialize from the DER (from above)
|
||||
// the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted
|
||||
// to a byte[] first and then shoved through a MemoryStream
|
||||
PrivateKeyFactory.CreateKey(new MemoryStream(privateKey.Array, privateKey.Offset, privateKey.Count, false));
|
||||
|
||||
public static string PubKeyFingerprint(ArraySegment<byte> publicKeyBytes)
|
||||
{
|
||||
Sha256Digest digest = new Sha256Digest();
|
||||
byte[] hash = new byte[digest.GetDigestSize()];
|
||||
digest.BlockUpdate(publicKeyBytes.Array, publicKeyBytes.Offset, publicKeyBytes.Count);
|
||||
digest.DoFinal(hash, 0);
|
||||
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void SaveToFile(string path)
|
||||
{
|
||||
string json = JsonUtility.ToJson(new SerializedPair
|
||||
{
|
||||
PublicKeyFingerprint = PublicKeyFingerprint,
|
||||
PublicKey = Convert.ToBase64String(PublicKeySerialized),
|
||||
PrivateKey= Convert.ToBase64String(SerializePrivateKey(PrivateKey))
|
||||
});
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
public static EncryptionCredentials LoadFromFile(string path)
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
SerializedPair serializedPair = JsonUtility.FromJson<SerializedPair>(json);
|
||||
|
||||
byte[] publicKeyBytes = Convert.FromBase64String(serializedPair.PublicKey);
|
||||
byte[] privateKeyBytes = Convert.FromBase64String(serializedPair.PrivateKey);
|
||||
|
||||
if (serializedPair.PublicKeyFingerprint != PubKeyFingerprint(new ArraySegment<byte>(publicKeyBytes)))
|
||||
throw new Exception("Saved public key fingerprint does not match public key.");
|
||||
return new EncryptionCredentials
|
||||
{
|
||||
PublicKeySerialized = publicKeyBytes,
|
||||
PublicKeyFingerprint = serializedPair.PublicKeyFingerprint,
|
||||
PrivateKey = (ECPrivateKeyParameters) DeserializePrivateKey(new ArraySegment<byte>(privateKeyBytes))
|
||||
};
|
||||
}
|
||||
|
||||
class SerializedPair
|
||||
{
|
||||
public string PublicKeyFingerprint;
|
||||
public string PublicKey;
|
||||
public string PrivateKey;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af6ae5f74f9548588cba5731643fabaf
|
||||
timeCreated: 1708139579
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs
|
||||
uploadId: 736421
|
289
Assets/Mirror/Transports/Encryption/EncryptionTransport.cs
Normal file
289
Assets/Mirror/Transports/Encryption/EncryptionTransport.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Mirror.BouncyCastle.Crypto;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")]
|
||||
public class EncryptionTransport : Transport, PortTransport
|
||||
{
|
||||
public override bool IsEncrypted => true;
|
||||
public override string EncryptionCipher => "AES256-GCM";
|
||||
[FormerlySerializedAs("inner")]
|
||||
[HideInInspector]
|
||||
public Transport Inner;
|
||||
|
||||
public ushort Port
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Inner is PortTransport portTransport)
|
||||
return portTransport.Port;
|
||||
|
||||
Debug.LogError($"EncryptionTransport can't get Port because {Inner} is not a PortTransport");
|
||||
return 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (Inner is PortTransport portTransport)
|
||||
{
|
||||
portTransport.Port = value;
|
||||
return;
|
||||
}
|
||||
Debug.LogError($"EncryptionTransport can't set Port because {Inner} is not a PortTransport");
|
||||
}
|
||||
}
|
||||
|
||||
public enum ValidationMode
|
||||
{
|
||||
Off,
|
||||
List,
|
||||
Callback
|
||||
}
|
||||
|
||||
[FormerlySerializedAs("clientValidateServerPubKey")]
|
||||
[HideInInspector]
|
||||
public ValidationMode ClientValidateServerPubKey;
|
||||
[FormerlySerializedAs("clientTrustedPubKeySignatures")]
|
||||
[HideInInspector]
|
||||
[Tooltip("List of public key fingerprints the client will accept")]
|
||||
public string[] ClientTrustedPubKeySignatures;
|
||||
public Func<PubKeyInfo, bool> OnClientValidateServerPubKey;
|
||||
[FormerlySerializedAs("serverLoadKeyPairFromFile")]
|
||||
[HideInInspector]
|
||||
public bool ServerLoadKeyPairFromFile;
|
||||
[FormerlySerializedAs("serverKeypairPath")]
|
||||
[HideInInspector]
|
||||
public string ServerKeypairPath = "./server-keys.json";
|
||||
|
||||
EncryptedConnection client;
|
||||
|
||||
readonly Dictionary<int, EncryptedConnection> serverConnections = new Dictionary<int, EncryptedConnection>();
|
||||
|
||||
readonly List<EncryptedConnection> serverPendingConnections =
|
||||
new List<EncryptedConnection>();
|
||||
|
||||
EncryptionCredentials credentials;
|
||||
public string EncryptionPublicKeyFingerprint => credentials?.PublicKeyFingerprint;
|
||||
public byte[] EncryptionPublicKey => credentials?.PublicKeySerialized;
|
||||
|
||||
void ServerRemoveFromPending(EncryptedConnection con)
|
||||
{
|
||||
for (int i = 0; i < serverPendingConnections.Count; i++)
|
||||
if (serverPendingConnections[i] == con)
|
||||
{
|
||||
// remove by swapping with last
|
||||
int lastIndex = serverPendingConnections.Count - 1;
|
||||
serverPendingConnections[i] = serverPendingConnections[lastIndex];
|
||||
serverPendingConnections.RemoveAt(lastIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleInnerServerDisconnected(int connId)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connId, out EncryptedConnection con))
|
||||
{
|
||||
ServerRemoveFromPending(con);
|
||||
serverConnections.Remove(connId);
|
||||
}
|
||||
OnServerDisconnected?.Invoke(connId);
|
||||
}
|
||||
|
||||
void HandleInnerServerError(int connId, TransportError type, string msg) => OnServerError?.Invoke(connId, type, $"inner: {msg}");
|
||||
|
||||
void HandleInnerServerDataReceived(int connId, ArraySegment<byte> data, int channel)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connId, out EncryptedConnection c))
|
||||
c.OnReceiveRaw(data, channel);
|
||||
}
|
||||
|
||||
void HandleInnerServerConnected(int connId) => HandleInnerServerConnected(connId, Inner.ServerGetClientAddress(connId));
|
||||
|
||||
void HandleInnerServerConnected(int connId, string clientRemoteAddress)
|
||||
{
|
||||
Debug.Log($"[EncryptionTransport] New connection #{connId} from {clientRemoteAddress}");
|
||||
EncryptedConnection ec = null;
|
||||
ec = new EncryptedConnection(
|
||||
credentials,
|
||||
false,
|
||||
(segment, channel) => Inner.ServerSend(connId, segment, channel),
|
||||
(segment, channel) => OnServerDataReceived?.Invoke(connId, segment, channel),
|
||||
() =>
|
||||
{
|
||||
Debug.Log($"[EncryptionTransport] Connection #{connId} is ready");
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
ServerRemoveFromPending(ec);
|
||||
OnServerConnectedWithAddress?.Invoke(connId, clientRemoteAddress);
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnServerError?.Invoke(connId, type, msg);
|
||||
ServerDisconnect(connId);
|
||||
});
|
||||
serverConnections.Add(connId, ec);
|
||||
serverPendingConnections.Add(ec);
|
||||
}
|
||||
|
||||
void HandleInnerClientDisconnected()
|
||||
{
|
||||
client = null;
|
||||
OnClientDisconnected?.Invoke();
|
||||
}
|
||||
|
||||
void HandleInnerClientError(TransportError arg1, string arg2) => OnClientError?.Invoke(arg1, $"inner: {arg2}");
|
||||
|
||||
void HandleInnerClientDataReceived(ArraySegment<byte> data, int channel) => client?.OnReceiveRaw(data, channel);
|
||||
|
||||
void HandleInnerClientConnected() =>
|
||||
client = new EncryptedConnection(
|
||||
credentials,
|
||||
true,
|
||||
(segment, channel) => Inner.ClientSend(segment, channel),
|
||||
(segment, channel) => OnClientDataReceived?.Invoke(segment, channel),
|
||||
() =>
|
||||
{
|
||||
OnClientConnected?.Invoke();
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnClientError?.Invoke(type, msg);
|
||||
ClientDisconnect();
|
||||
},
|
||||
HandleClientValidateServerPubKey);
|
||||
|
||||
bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo)
|
||||
{
|
||||
switch (ClientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
return true;
|
||||
case ValidationMode.List:
|
||||
return Array.IndexOf(ClientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0;
|
||||
case ValidationMode.Callback:
|
||||
return OnClientValidateServerPubKey(pubKeyInfo);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
void Awake() =>
|
||||
// check if encryption via hardware acceleration is supported.
|
||||
// this can be useful to know for low end devices.
|
||||
//
|
||||
// hardware acceleration requires netcoreapp3.0 or later:
|
||||
// https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/AesUtilities.cs#L18
|
||||
// because AesEngine_x86 requires System.Runtime.Intrinsics.X86:
|
||||
// https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/engines/AesEngine_X86.cs
|
||||
// which Unity does not support yet.
|
||||
Debug.Log($"EncryptionTransport: IsHardwareAccelerated={AesUtilities.IsHardwareAccelerated}");
|
||||
|
||||
public override bool Available() => Inner.Available();
|
||||
|
||||
public override bool ClientConnected() => client != null && client.IsReady;
|
||||
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
switch (ClientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
break;
|
||||
case ValidationMode.List:
|
||||
if (ClientTrustedPubKeySignatures == null || ClientTrustedPubKeySignatures.Length == 0)
|
||||
{
|
||||
OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty.");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case ValidationMode.Callback:
|
||||
if (OnClientValidateServerPubKey == null)
|
||||
{
|
||||
OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
credentials = EncryptionCredentials.Generate();
|
||||
Inner.OnClientConnected = HandleInnerClientConnected;
|
||||
Inner.OnClientDataReceived = HandleInnerClientDataReceived;
|
||||
Inner.OnClientDataSent = (bytes, channel) => OnClientDataSent?.Invoke(bytes, channel);
|
||||
Inner.OnClientError = HandleInnerClientError;
|
||||
Inner.OnClientDisconnected = HandleInnerClientDisconnected;
|
||||
Inner.ClientConnect(address);
|
||||
}
|
||||
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
client?.Send(segment, channelId);
|
||||
|
||||
public override void ClientDisconnect() => Inner.ClientDisconnect();
|
||||
|
||||
public override Uri ServerUri() => Inner.ServerUri();
|
||||
|
||||
public override bool ServerActive() => Inner.ServerActive();
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (ServerLoadKeyPairFromFile)
|
||||
credentials = EncryptionCredentials.LoadFromFile(ServerKeypairPath);
|
||||
else
|
||||
credentials = EncryptionCredentials.Generate();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Inner.OnServerConnected = HandleInnerServerConnected;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Inner.OnServerConnectedWithAddress = HandleInnerServerConnected;
|
||||
Inner.OnServerDataReceived = HandleInnerServerDataReceived;
|
||||
Inner.OnServerDataSent = (connId, bytes, channel) => OnServerDataSent?.Invoke(connId, bytes, channel);
|
||||
Inner.OnServerError = HandleInnerServerError;
|
||||
Inner.OnServerDisconnected = HandleInnerServerDisconnected;
|
||||
Inner.ServerStart();
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady)
|
||||
connection.Send(segment, channelId);
|
||||
}
|
||||
|
||||
public override void ServerDisconnect(int connectionId) =>
|
||||
// cleanup is done via inners disconnect event
|
||||
Inner.ServerDisconnect(connectionId);
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId) => Inner.ServerGetClientAddress(connectionId);
|
||||
|
||||
public override void ServerStop() => Inner.ServerStop();
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable) =>
|
||||
Inner.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead;
|
||||
|
||||
public override int GetBatchThreshold(int channelId = Channels.Reliable) => Inner.GetBatchThreshold(channelId) - EncryptedConnection.Overhead;
|
||||
|
||||
public override void Shutdown() => Inner.Shutdown();
|
||||
|
||||
public override void ClientEarlyUpdate() => Inner.ClientEarlyUpdate();
|
||||
|
||||
public override void ClientLateUpdate()
|
||||
{
|
||||
Inner.ClientLateUpdate();
|
||||
Profiler.BeginSample("EncryptionTransport.ServerLateUpdate");
|
||||
client?.TickNonReady(NetworkTime.localTime);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
public override void ServerEarlyUpdate() => Inner.ServerEarlyUpdate();
|
||||
|
||||
public override void ServerLateUpdate()
|
||||
{
|
||||
Inner.ServerLateUpdate();
|
||||
Profiler.BeginSample("EncryptionTransport.ServerLateUpdate");
|
||||
// Reverse iteration as entries can be removed while updating
|
||||
for (int i = serverPendingConnections.Count - 1; i >= 0; i--)
|
||||
serverPendingConnections[i].TickNonReady(NetworkTime.time);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0aa135acc32a4383ae9a5817f018cb06
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/EncryptionTransport.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/Encryption/Plugins.meta
Normal file
8
Assets/Mirror/Transports/Encryption/Plugins.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4048f5ff245dfa34abec0a401364e7c0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31ff83bf6d2e72542adcbe2c21383f4a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,15 @@
|
||||
Version with renamed namespaces to avoid conflicts lives here: https://github.com/MirrorNetworking/bc-csharp
|
||||
|
||||
Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org).
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sub license, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions: The above copyright notice and this
|
||||
permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
|
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b45a99b5583cda419e1f1ec943fec4b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md
|
||||
uploadId: 736421
|
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03a89f29994a3b44cb3015b3c5ece010
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
Windows Store Apps: WindowsStoreApps
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll
|
||||
uploadId: 736421
|
12
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs
Normal file
12
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using Mirror.BouncyCastle.Crypto;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
public struct PubKeyInfo
|
||||
{
|
||||
public string Fingerprint;
|
||||
public ArraySegment<byte> Serialized;
|
||||
public AsymmetricKeyParameter Key;
|
||||
}
|
||||
}
|
10
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta
Normal file
10
Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta
Normal file
@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1e3744418024c02acf39f44c1d1bd20
|
||||
timeCreated: 1708874062
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/PubKeyInfo.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using kcp2k;
|
||||
using Mirror.BouncyCastle.Crypto;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.Serialization;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace Mirror.Transports.Encryption
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")]
|
||||
public class ThreadedEncryptionKcpTransport : ThreadedKcpTransport
|
||||
{
|
||||
public override bool IsEncrypted => true;
|
||||
public override string EncryptionCipher => "AES256-GCM";
|
||||
public override string ToString() => $"Encrypted {base.ToString()}";
|
||||
|
||||
public enum ValidationMode
|
||||
{
|
||||
Off,
|
||||
List,
|
||||
Callback
|
||||
}
|
||||
|
||||
[HideInInspector]
|
||||
public ValidationMode ClientValidateServerPubKey;
|
||||
|
||||
[Tooltip("List of public key fingerprints the client will accept")]
|
||||
[HideInInspector]
|
||||
public string[] ClientTrustedPubKeySignatures;
|
||||
/// <summary>
|
||||
/// Called when a client connects to a server
|
||||
/// ATTENTION: NOT THREAD SAFE.
|
||||
/// This will be called on the worker thread.
|
||||
/// </summary>
|
||||
public Func<PubKeyInfo, bool> OnClientValidateServerPubKey;
|
||||
[HideInInspector]
|
||||
[FormerlySerializedAs("serverLoadKeyPairFromFile")]
|
||||
public bool ServerLoadKeyPairFromFile;
|
||||
[HideInInspector]
|
||||
[FormerlySerializedAs("serverKeypairPath")]
|
||||
public string ServerKeypairPath = "./server-keys.json";
|
||||
|
||||
EncryptedConnection encryptedClient;
|
||||
|
||||
readonly Dictionary<int, EncryptedConnection> serverConnections = new Dictionary<int, EncryptedConnection>();
|
||||
|
||||
readonly List<EncryptedConnection> serverPendingConnections =
|
||||
new List<EncryptedConnection>();
|
||||
|
||||
EncryptionCredentials credentials;
|
||||
public string EncryptionPublicKeyFingerprint => credentials?.PublicKeyFingerprint;
|
||||
public byte[] EncryptionPublicKey => credentials?.PublicKeySerialized;
|
||||
|
||||
// Used for threaded time keeping as unitys Time.time is not thread safe
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
|
||||
void ServerRemoveFromPending(EncryptedConnection con)
|
||||
{
|
||||
for (int i = 0; i < serverPendingConnections.Count; i++)
|
||||
if (serverPendingConnections[i] == con)
|
||||
{
|
||||
// remove by swapping with last
|
||||
int lastIndex = serverPendingConnections.Count - 1;
|
||||
serverPendingConnections[i] = serverPendingConnections[lastIndex];
|
||||
serverPendingConnections.RemoveAt(lastIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleInnerServerDisconnected(int connId)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connId, out EncryptedConnection con))
|
||||
{
|
||||
ServerRemoveFromPending(con);
|
||||
serverConnections.Remove(connId);
|
||||
}
|
||||
OnThreadedServerDisconnected(connId);
|
||||
}
|
||||
|
||||
void HandleInnerServerDataReceived(int connId, ArraySegment<byte> data, int channel)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connId, out EncryptedConnection c))
|
||||
c.OnReceiveRaw(data, channel);
|
||||
}
|
||||
|
||||
|
||||
void HandleInnerServerConnected(int connId, IPEndPoint clientRemoteAddress)
|
||||
{
|
||||
Debug.Log($"[ThreadedEncryptionKcpTransport] New connection #{connId} from {clientRemoteAddress}");
|
||||
EncryptedConnection ec = null;
|
||||
ec = new EncryptedConnection(
|
||||
credentials,
|
||||
false,
|
||||
(segment, channel) =>
|
||||
{
|
||||
server.Send(connId, segment, KcpTransport.ToKcpChannel(channel));
|
||||
OnThreadedServerSend(connId, segment,channel);
|
||||
},
|
||||
(segment, channel) => OnThreadedServerReceive(connId, segment, channel),
|
||||
() =>
|
||||
{
|
||||
Debug.Log($"[ThreadedEncryptionKcpTransport] Connection #{connId} is ready");
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
ServerRemoveFromPending(ec);
|
||||
OnThreadedServerConnected(connId, clientRemoteAddress);
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnThreadedServerError(connId, type, msg);
|
||||
ServerDisconnect(connId);
|
||||
});
|
||||
serverConnections.Add(connId, ec);
|
||||
serverPendingConnections.Add(ec);
|
||||
}
|
||||
|
||||
void HandleInnerClientDisconnected()
|
||||
{
|
||||
encryptedClient = null;
|
||||
OnThreadedClientDisconnected();
|
||||
}
|
||||
|
||||
void HandleInnerClientDataReceived(ArraySegment<byte> data, int channel) => encryptedClient?.OnReceiveRaw(data, channel);
|
||||
|
||||
void HandleInnerClientConnected() =>
|
||||
encryptedClient = new EncryptedConnection(
|
||||
credentials,
|
||||
true,
|
||||
(segment, channel) =>
|
||||
{
|
||||
client.Send(segment, KcpTransport.ToKcpChannel(channel));
|
||||
OnThreadedClientSend(segment, channel);
|
||||
},
|
||||
(segment, channel) => OnThreadedClientReceive(segment, channel),
|
||||
() =>
|
||||
{
|
||||
OnThreadedClientConnected();
|
||||
},
|
||||
(type, msg) =>
|
||||
{
|
||||
OnThreadedClientError(type, msg);
|
||||
ClientDisconnect();
|
||||
},
|
||||
HandleClientValidateServerPubKey);
|
||||
|
||||
bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo)
|
||||
{
|
||||
switch (ClientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
return true;
|
||||
case ValidationMode.List:
|
||||
return Array.IndexOf(ClientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0;
|
||||
case ValidationMode.Callback:
|
||||
return OnClientValidateServerPubKey(pubKeyInfo);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new KcpClient(
|
||||
HandleInnerClientConnected,
|
||||
(message, channel) => HandleInnerClientDataReceived(message, KcpTransport.FromKcpChannel(channel)),
|
||||
HandleInnerClientDisconnected,
|
||||
(error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new KcpServer(
|
||||
HandleInnerServerConnected,
|
||||
(connectionId, message, channel) => HandleInnerServerDataReceived(connectionId, message, KcpTransport.FromKcpChannel(channel)),
|
||||
HandleInnerServerDisconnected,
|
||||
(connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
// check if encryption via hardware acceleration is supported.
|
||||
// this can be useful to know for low end devices.
|
||||
//
|
||||
// hardware acceleration requires netcoreapp3.0 or later:
|
||||
// https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/AesUtilities.cs#L18
|
||||
// because AesEngine_x86 requires System.Runtime.Intrinsics.X86:
|
||||
// https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/engines/AesEngine_X86.cs
|
||||
// which Unity does not support yet.
|
||||
Debug.Log($"ThreadedEncryptionKcpTransport: IsHardwareAccelerated={AesUtilities.IsHardwareAccelerated}");
|
||||
}
|
||||
|
||||
protected override void ThreadedClientConnect(string address)
|
||||
{
|
||||
if (!SetupEncryptionForClient())
|
||||
return;
|
||||
base.ThreadedClientConnect(address);
|
||||
}
|
||||
|
||||
bool SetupEncryptionForClient()
|
||||
{
|
||||
|
||||
switch (ClientValidateServerPubKey)
|
||||
{
|
||||
case ValidationMode.Off:
|
||||
break;
|
||||
case ValidationMode.List:
|
||||
if (ClientTrustedPubKeySignatures == null || ClientTrustedPubKeySignatures.Length == 0)
|
||||
{
|
||||
OnThreadedClientError(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case ValidationMode.Callback:
|
||||
if (OnClientValidateServerPubKey == null)
|
||||
{
|
||||
OnThreadedClientError(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
credentials = EncryptionCredentials.Generate();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void ThreadedClientConnect(Uri address)
|
||||
{
|
||||
if (!SetupEncryptionForClient())
|
||||
return;
|
||||
base.ThreadedClientConnect(address);
|
||||
}
|
||||
|
||||
protected override void ThreadedClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
encryptedClient?.Send(segment, channelId);
|
||||
}
|
||||
|
||||
protected override void ThreadedServerStart()
|
||||
{
|
||||
if (ServerLoadKeyPairFromFile)
|
||||
credentials = EncryptionCredentials.LoadFromFile(ServerKeypairPath);
|
||||
else
|
||||
credentials = EncryptionCredentials.Generate();
|
||||
base.ThreadedServerStart();
|
||||
}
|
||||
|
||||
protected override void ThreadedServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
if (serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady)
|
||||
connection.Send(segment, channelId);
|
||||
}
|
||||
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable) => base.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead;
|
||||
public override int GetBatchThreshold(int channelId) => base.GetBatchThreshold(channelId) - EncryptedConnection.Overhead;
|
||||
|
||||
protected override void ThreadedClientLateUpdate()
|
||||
{
|
||||
base.ThreadedClientLateUpdate();
|
||||
Profiler.BeginSample("ThreadedEncryptionKcpTransport.ServerLateUpdate");
|
||||
encryptedClient?.TickNonReady(stopwatch.Elapsed.TotalSeconds);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void ThreadedServerLateUpdate()
|
||||
{
|
||||
base.ThreadedServerLateUpdate();
|
||||
Profiler.BeginSample("ThreadedEncryptionKcpTransport.ServerLateUpdate");
|
||||
// Reverse iteration as entries can be removed while updating
|
||||
for (int i = serverPendingConnections.Count - 1; i >= 0; i--)
|
||||
serverPendingConnections[i].TickNonReady(stopwatch.Elapsed.TotalSeconds);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d3e310924fb49c195391b9699f20809
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/FizzySteamworks.meta
Normal file
8
Assets/Mirror/Transports/FizzySteamworks.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e48fe15d95f541f4d855febde911927d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,89 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public class BidirectionalDictionary<T1, T2> : IEnumerable
|
||||
{
|
||||
private Dictionary<T1, T2> t1ToT2Dict = new Dictionary<T1, T2>();
|
||||
private Dictionary<T2, T1> t2ToT1Dict = new Dictionary<T2, T1>();
|
||||
|
||||
public IEnumerable<T1> FirstTypes => t1ToT2Dict.Keys;
|
||||
public IEnumerable<T2> SecondTypes => t2ToT1Dict.Keys;
|
||||
|
||||
public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator();
|
||||
|
||||
public int Count => t1ToT2Dict.Count;
|
||||
|
||||
public void Add(T1 key, T2 value)
|
||||
{
|
||||
if (t1ToT2Dict.ContainsKey(key))
|
||||
{
|
||||
Remove(key);
|
||||
}
|
||||
|
||||
t1ToT2Dict[key] = value;
|
||||
t2ToT1Dict[value] = key;
|
||||
}
|
||||
|
||||
public void Add(T2 key, T1 value)
|
||||
{
|
||||
if (t2ToT1Dict.ContainsKey(key))
|
||||
{
|
||||
Remove(key);
|
||||
}
|
||||
|
||||
t2ToT1Dict[key] = value;
|
||||
t1ToT2Dict[value] = key;
|
||||
}
|
||||
|
||||
public T2 Get(T1 key) => t1ToT2Dict[key];
|
||||
|
||||
public T1 Get(T2 key) => t2ToT1Dict[key];
|
||||
|
||||
public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value);
|
||||
|
||||
public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value);
|
||||
|
||||
public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key);
|
||||
|
||||
public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key);
|
||||
|
||||
public void Remove(T1 key)
|
||||
{
|
||||
if (Contains(key))
|
||||
{
|
||||
T2 val = t1ToT2Dict[key];
|
||||
t1ToT2Dict.Remove(key);
|
||||
t2ToT1Dict.Remove(val);
|
||||
}
|
||||
}
|
||||
public void Remove(T2 key)
|
||||
{
|
||||
if (Contains(key))
|
||||
{
|
||||
T1 val = t2ToT1Dict[key];
|
||||
t1ToT2Dict.Remove(val);
|
||||
t2ToT1Dict.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public T1 this[T2 key]
|
||||
{
|
||||
get => t2ToT1Dict[key];
|
||||
set
|
||||
{
|
||||
Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public T2 this[T1 key]
|
||||
{
|
||||
get => t1ToT2Dict[key];
|
||||
set
|
||||
{
|
||||
Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ffc59ba41dcc8a44b41943b2987dd92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "FizzySteamworks",
|
||||
"references": [
|
||||
"GUID:68bd7fdb68ef2684e982e8a9825b18a5",
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor",
|
||||
"LinuxStandalone64",
|
||||
"macOSStandalone",
|
||||
"WindowsStandalone32",
|
||||
"WindowsStandalone64"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd78bb22ae3a60443a865fb2d7dfdafa
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
309
Assets/Mirror/Transports/FizzySteamworks/FizzySteamworks.cs
Normal file
309
Assets/Mirror/Transports/FizzySteamworks/FizzySteamworks.cs
Normal file
@ -0,0 +1,309 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
[HelpURL("https://github.com/Chykary/FizzySteamworks")]
|
||||
public class FizzySteamworks : Transport
|
||||
{
|
||||
private const string STEAM_SCHEME = "steam";
|
||||
|
||||
private static IClient client;
|
||||
private static IServer server;
|
||||
|
||||
[SerializeField]
|
||||
public EP2PSend[] Channels = new EP2PSend[2] { EP2PSend.k_EP2PSendReliable, EP2PSend.k_EP2PSendUnreliableNoDelay };
|
||||
|
||||
[Tooltip("Timeout for connecting in seconds.")]
|
||||
public int Timeout = 25;
|
||||
[Tooltip("Allow or disallow P2P connections to fall back to being relayed through the Steam servers if a direct connection or NAT-traversal cannot be established.")]
|
||||
public bool AllowSteamRelay = true;
|
||||
|
||||
[Tooltip("Use SteamSockets instead of the (deprecated) SteamNetworking. This will always use Relay.")]
|
||||
public bool UseNextGenSteamNetworking = true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for FizzySteamworks.");
|
||||
Invoke(nameof(InitRelayNetworkAccess), 1f);
|
||||
}
|
||||
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
client?.ReceiveData();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
server?.ReceiveData();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ClientLateUpdate()
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
client?.FlushData();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ServerLateUpdate()
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
server?.FlushData();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ClientConnected() => ClientActive() && client.Connected;
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
|
||||
InitRelayNetworkAccess();
|
||||
|
||||
if (ServerActive())
|
||||
{
|
||||
Debug.LogError("Transport already running as server!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ClientActive() || client.Error)
|
||||
{
|
||||
if (UseNextGenSteamNetworking)
|
||||
{
|
||||
Debug.Log($"Starting client [SteamSockets], target address {address}.");
|
||||
client = NextClient.CreateClient(this, address);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Starting client [DEPRECATED SteamNetworking], target address {address}. Relay enabled: {AllowSteamRelay}");
|
||||
SteamNetworking.AllowP2PPacketRelay(AllowSteamRelay);
|
||||
client = LegacyClient.CreateClient(this, address);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Client already running!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError("Exception: " + ex.Message + ". Client could not be started.");
|
||||
OnClientDisconnected.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != STEAM_SCHEME)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {STEAM_SCHEME}://SteamID instead", nameof(uri));
|
||||
|
||||
ClientConnect(uri.Host);
|
||||
}
|
||||
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
byte[] data = new byte[segment.Count];
|
||||
Array.Copy(segment.Array, segment.Offset, data, 0, segment.Count);
|
||||
client.Send(data, channelId);
|
||||
}
|
||||
|
||||
public override void ClientDisconnect()
|
||||
{
|
||||
if (ClientActive())
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
public bool ClientActive() => client != null;
|
||||
|
||||
|
||||
public override bool ServerActive() => server != null;
|
||||
public override void ServerStart()
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
|
||||
|
||||
InitRelayNetworkAccess();
|
||||
|
||||
if (ClientActive())
|
||||
{
|
||||
Debug.LogError("Transport already running as client!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ServerActive())
|
||||
{
|
||||
if (UseNextGenSteamNetworking)
|
||||
{
|
||||
Debug.Log($"Starting server [SteamSockets].");
|
||||
server = NextServer.CreateServer(this, NetworkManager.singleton.maxConnections);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Starting server [DEPRECATED SteamNetworking]. Relay enabled: {AllowSteamRelay}");
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworking.AllowP2PPacketRelay(AllowSteamRelay);
|
||||
#else
|
||||
|
||||
SteamNetworking.AllowP2PPacketRelay(AllowSteamRelay);
|
||||
#endif
|
||||
server = LegacyServer.CreateServer(this, NetworkManager.singleton.maxConnections);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Server already started!");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
var steamBuilder = new UriBuilder
|
||||
{
|
||||
Scheme = STEAM_SCHEME,
|
||||
#if UNITY_SERVER
|
||||
Host = SteamGameServer.GetSteamID().m_SteamID.ToString()
|
||||
#else
|
||||
Host = SteamUser.GetSteamID().m_SteamID.ToString()
|
||||
#endif
|
||||
};
|
||||
|
||||
return steamBuilder.Uri;
|
||||
}
|
||||
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
byte[] data = new byte[segment.Count];
|
||||
Array.Copy(segment.Array, segment.Offset, data, 0, segment.Count);
|
||||
server.Send(connectionId, data, channelId);
|
||||
}
|
||||
}
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
server.Disconnect(connectionId);
|
||||
}
|
||||
}
|
||||
public override string ServerGetClientAddress(int connectionId) => ServerActive() ? server.ServerGetClientAddress(connectionId) : string.Empty;
|
||||
public override void ServerStop()
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
client.Disconnect();
|
||||
client = null;
|
||||
Debug.Log("Transport shut down - client.");
|
||||
}
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
server.Shutdown();
|
||||
server = null;
|
||||
Debug.Log("Transport shut down - server.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override int GetMaxPacketSize(int channelId)
|
||||
{
|
||||
if (UseNextGenSteamNetworking)
|
||||
{
|
||||
return Constants.k_cbMaxSteamNetworkingSocketsMessageSizeSend;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (channelId >= Channels.Length)
|
||||
{
|
||||
Debug.LogError("Channel Id exceeded configured channels! Please configure more channels.");
|
||||
return 1200;
|
||||
}
|
||||
|
||||
switch (Channels[channelId])
|
||||
{
|
||||
case EP2PSend.k_EP2PSendUnreliable:
|
||||
case EP2PSend.k_EP2PSendUnreliableNoDelay:
|
||||
return 1200;
|
||||
case EP2PSend.k_EP2PSendReliable:
|
||||
case EP2PSend.k_EP2PSendReliableWithBuffering:
|
||||
return 1048576;
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Available()
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitRelayNetworkAccess()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (UseNextGenSteamNetworking)
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to initialize relay network access: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6e55fca1f4e4844593a3dd65a3ecb39
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
14
Assets/Mirror/Transports/FizzySteamworks/IClient.cs
Normal file
14
Assets/Mirror/Transports/FizzySteamworks/IClient.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public interface IClient
|
||||
{
|
||||
bool Connected { get; }
|
||||
bool Error { get; }
|
||||
|
||||
|
||||
void ReceiveData();
|
||||
void Disconnect();
|
||||
void FlushData();
|
||||
void Send(byte[] data, int channelId);
|
||||
}
|
||||
}
|
11
Assets/Mirror/Transports/FizzySteamworks/IClient.cs.meta
Normal file
11
Assets/Mirror/Transports/FizzySteamworks/IClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c65d0a35946fe34aa415488cd674bce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
12
Assets/Mirror/Transports/FizzySteamworks/IServer.cs
Normal file
12
Assets/Mirror/Transports/FizzySteamworks/IServer.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public interface IServer
|
||||
{
|
||||
void ReceiveData();
|
||||
void Send(int connectionId, byte[] data, int channelId);
|
||||
void Disconnect(int connectionId);
|
||||
void FlushData();
|
||||
string ServerGetClientAddress(int connectionId);
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
11
Assets/Mirror/Transports/FizzySteamworks/IServer.cs.meta
Normal file
11
Assets/Mirror/Transports/FizzySteamworks/IServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ed34f1f9e087d74094368efc7a84aad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
37
Assets/Mirror/Transports/FizzySteamworks/LICENSE
Normal file
37
Assets/Mirror/Transports/FizzySteamworks/LICENSE
Normal file
@ -0,0 +1,37 @@
|
||||
MIT License
|
||||
|
||||
Copyright Fizz Cube Ltd (c) 2018
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
===
|
||||
|
||||
Copyright Marco Hoffmann (c) 2020
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
MIT License
|
7
Assets/Mirror/Transports/FizzySteamworks/LICENSE.meta
Normal file
7
Assets/Mirror/Transports/FizzySteamworks/LICENSE.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80411239d210c5c4fb875c2d24c06fe0
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
185
Assets/Mirror/Transports/FizzySteamworks/LegacyClient.cs
Normal file
185
Assets/Mirror/Transports/FizzySteamworks/LegacyClient.cs
Normal file
@ -0,0 +1,185 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public class LegacyClient : LegacyCommon, IClient
|
||||
{
|
||||
public bool Connected { get; private set; }
|
||||
public bool Error { get; private set; }
|
||||
|
||||
private event Action<byte[], int> OnReceivedData;
|
||||
private event Action OnConnected;
|
||||
private event Action OnDisconnected;
|
||||
|
||||
private TimeSpan ConnectionTimeout;
|
||||
|
||||
private CSteamID hostSteamID = CSteamID.Nil;
|
||||
private TaskCompletionSource<Task> connectedComplete;
|
||||
private CancellationTokenSource cancelToken;
|
||||
|
||||
private LegacyClient(FizzySteamworks transport) : base(transport)
|
||||
{
|
||||
ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.Timeout));
|
||||
}
|
||||
|
||||
public static LegacyClient CreateClient(FizzySteamworks transport, string host)
|
||||
{
|
||||
LegacyClient c = new LegacyClient(transport);
|
||||
|
||||
c.OnConnected += () => transport.OnClientConnected.Invoke();
|
||||
c.OnDisconnected += () => transport.OnClientDisconnected.Invoke();
|
||||
c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment<byte>(data), channel);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
InteropHelp.TestIfAvailableGameServer();
|
||||
#else
|
||||
InteropHelp.TestIfAvailableClient();
|
||||
#endif
|
||||
c.Connect(host);
|
||||
}
|
||||
catch(FormatException)
|
||||
{
|
||||
Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?");
|
||||
c.Error = true;
|
||||
c.OnConnectionFailed(CSteamID.Nil);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Debug.LogError($"Unexpected exception: {ex.Message}");
|
||||
c.Error = true;
|
||||
c.OnConnectionFailed(CSteamID.Nil);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
private async void Connect(string host)
|
||||
{
|
||||
cancelToken = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
hostSteamID = new CSteamID(UInt64.Parse(host));
|
||||
connectedComplete = new TaskCompletionSource<Task>();
|
||||
|
||||
OnConnected += SetConnectedComplete;
|
||||
|
||||
SendInternal(hostSteamID, InternalMessages.CONNECT);
|
||||
|
||||
Task connectedCompleteTask = connectedComplete.Task;
|
||||
Task timeOutTask = Task.Delay(ConnectionTimeout, cancelToken.Token);
|
||||
|
||||
if (await Task.WhenAny(connectedCompleteTask, timeOutTask) != connectedCompleteTask)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
Debug.LogError($"The connection attempt was cancelled.");
|
||||
}
|
||||
else if (timeOutTask.IsCompleted)
|
||||
{
|
||||
Debug.LogError($"Connection to {host} timed out.");
|
||||
}
|
||||
OnConnected -= SetConnectedComplete;
|
||||
OnConnectionFailed(hostSteamID);
|
||||
}
|
||||
|
||||
OnConnected -= SetConnectedComplete;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?");
|
||||
Error = true;
|
||||
OnConnectionFailed(hostSteamID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Unexpected exception: {ex.Message}");
|
||||
Error = true;
|
||||
OnConnectionFailed(hostSteamID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Error)
|
||||
{
|
||||
Debug.LogError("Connection failed.");
|
||||
OnConnectionFailed(CSteamID.Nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
Debug.Log("Sending Disconnect message");
|
||||
SendInternal(hostSteamID, InternalMessages.DISCONNECT);
|
||||
Dispose();
|
||||
cancelToken?.Cancel();
|
||||
|
||||
WaitForClose(hostSteamID);
|
||||
}
|
||||
|
||||
private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task);
|
||||
|
||||
protected override void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel)
|
||||
{
|
||||
if (clientSteamID != hostSteamID)
|
||||
{
|
||||
Debug.LogError("Received a message from an unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
OnReceivedData.Invoke(data, channel);
|
||||
}
|
||||
|
||||
protected override void OnNewConnection(P2PSessionRequest_t result)
|
||||
{
|
||||
if (hostSteamID == result.m_steamIDRemote)
|
||||
{
|
||||
SteamNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("P2P Acceptance Request from unknown host ID.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case InternalMessages.ACCEPT_CONNECT:
|
||||
if (!Connected)
|
||||
{
|
||||
Connected = true;
|
||||
OnConnected.Invoke();
|
||||
Debug.Log("Connection established.");
|
||||
}
|
||||
break;
|
||||
case InternalMessages.DISCONNECT:
|
||||
if (Connected)
|
||||
{
|
||||
Connected = false;
|
||||
Debug.Log("Disconnected.");
|
||||
OnDisconnected.Invoke();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.Log("Received unknown message type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] data, int channelId) => Send(hostSteamID, data, channelId);
|
||||
|
||||
protected override void OnConnectionFailed(CSteamID remoteId) => OnDisconnected.Invoke();
|
||||
public void FlushData() { }
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 862fc5e604ebaa0438662870d4a14e43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
204
Assets/Mirror/Transports/FizzySteamworks/LegacyCommon.cs
Normal file
204
Assets/Mirror/Transports/FizzySteamworks/LegacyCommon.cs
Normal file
@ -0,0 +1,204 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public abstract class LegacyCommon
|
||||
{
|
||||
private LegacyClient legacyClient => (LegacyClient)this;
|
||||
|
||||
private EP2PSend[] channels;
|
||||
private int internal_ch => channels.Length;
|
||||
|
||||
protected enum InternalMessages : byte
|
||||
{
|
||||
CONNECT,
|
||||
ACCEPT_CONNECT,
|
||||
DISCONNECT
|
||||
}
|
||||
|
||||
private Callback<P2PSessionRequest_t> callback_OnNewConnection = null;
|
||||
private Callback<P2PSessionConnectFail_t> callback_OnConnectFail = null;
|
||||
|
||||
protected readonly FizzySteamworks transport;
|
||||
|
||||
protected LegacyCommon(FizzySteamworks transport)
|
||||
{
|
||||
channels = transport.Channels;
|
||||
|
||||
callback_OnNewConnection = Callback<P2PSessionRequest_t>.Create(OnNewConnection);
|
||||
callback_OnConnectFail = Callback<P2PSessionConnectFail_t>.Create(OnConnectFail);
|
||||
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
protected void Dispose()
|
||||
{
|
||||
if (callback_OnNewConnection != null)
|
||||
{
|
||||
callback_OnNewConnection.Dispose();
|
||||
callback_OnNewConnection = null;
|
||||
}
|
||||
|
||||
if (callback_OnConnectFail != null)
|
||||
{
|
||||
callback_OnConnectFail.Dispose();
|
||||
callback_OnConnectFail = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void OnNewConnection(P2PSessionRequest_t result);
|
||||
|
||||
private void OnConnectFail(P2PSessionConnectFail_t result)
|
||||
{
|
||||
OnConnectionFailed(result.m_steamIDRemote);
|
||||
CloseP2PSessionWithUser(result.m_steamIDRemote);
|
||||
|
||||
switch (result.m_eP2PSessionError)
|
||||
{
|
||||
case 1:
|
||||
Debug.LogError("Connection failed: The target user is not running the same game.");
|
||||
break;
|
||||
case 2:
|
||||
Debug.LogError("Connection failed: The local user doesn't own the app that is running.");
|
||||
break;
|
||||
case 3:
|
||||
Debug.LogError("Connection failed: Target user isn't connected to Steam.");
|
||||
break;
|
||||
case 4:
|
||||
Debug.LogError("Connection failed: The connection timed out because the target user didn't respond.");
|
||||
break;
|
||||
default:
|
||||
Debug.LogError("Connection failed: Unknown error.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void SendInternal(CSteamID target, InternalMessages type)
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworking.SendP2PPacket(target, new byte[] { (byte)type }, 1, EP2PSend.k_EP2PSendReliable, internal_ch);
|
||||
#else
|
||||
SteamNetworking.SendP2PPacket(target, new byte[] { (byte)type }, 1, EP2PSend.k_EP2PSendReliable, internal_ch);
|
||||
#endif
|
||||
}
|
||||
protected void Send(CSteamID host, byte[] msgBuffer, int channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworking.SendP2PPacket(host, msgBuffer, (uint)msgBuffer.Length, channels[Mathf.Min(channel, channels.Length - 1)], channel);
|
||||
#else
|
||||
SteamNetworking.SendP2PPacket(host, msgBuffer, (uint)msgBuffer.Length, channels[Mathf.Min(channel, channels.Length - 1)], channel);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"SteamNetworking exception during Send: {ex.Message}");
|
||||
legacyClient.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private bool Receive(out CSteamID clientSteamID, out byte[] receiveBuffer, int channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
if (SteamGameServerNetworking.IsP2PPacketAvailable(out uint packetSize, channel))
|
||||
{
|
||||
receiveBuffer = new byte[packetSize];
|
||||
return SteamGameServerNetworking.ReadP2PPacket(receiveBuffer, packetSize, out _, out clientSteamID, channel);
|
||||
}
|
||||
#else
|
||||
if (SteamNetworking.IsP2PPacketAvailable(out uint packetSize, channel))
|
||||
{
|
||||
receiveBuffer = new byte[packetSize];
|
||||
return SteamNetworking.ReadP2PPacket(receiveBuffer, packetSize, out _, out clientSteamID, channel);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"SteamNetworking exception during Recive: {ex.Message}");
|
||||
receiveBuffer = null;
|
||||
clientSteamID = CSteamID.Nil;
|
||||
|
||||
legacyClient.Disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
receiveBuffer = null;
|
||||
clientSteamID = CSteamID.Nil;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void CloseP2PSessionWithUser(CSteamID clientSteamID)
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworking.CloseP2PSessionWithUser(clientSteamID);
|
||||
#else
|
||||
SteamNetworking.CloseP2PSessionWithUser(clientSteamID);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected void WaitForClose(CSteamID cSteamID)
|
||||
{
|
||||
if (transport.enabled)
|
||||
{
|
||||
transport.StartCoroutine(DelayedClose(cSteamID));
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseP2PSessionWithUser(cSteamID);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayedClose(CSteamID cSteamID)
|
||||
{
|
||||
yield return null;
|
||||
CloseP2PSessionWithUser(cSteamID);
|
||||
}
|
||||
|
||||
public void ReceiveData()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (transport.enabled && Receive(out CSteamID clientSteamID, out byte[] internalMessage, internal_ch))
|
||||
{
|
||||
if (internalMessage.Length == 1)
|
||||
{
|
||||
OnReceiveInternalData((InternalMessages)internalMessage[0], clientSteamID);
|
||||
return; // Wait one frame
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Incorrect package length on internal channel.");
|
||||
}
|
||||
}
|
||||
|
||||
for (int chNum = 0; chNum < channels.Length; chNum++)
|
||||
{
|
||||
while (transport.enabled && Receive(out CSteamID clientSteamID, out byte[] receiveBuffer, chNum))
|
||||
{
|
||||
OnReceiveData(receiveBuffer, clientSteamID, chNum);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
legacyClient.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID);
|
||||
protected abstract void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel);
|
||||
protected abstract void OnConnectionFailed(CSteamID remoteId);
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 805bd7b4762d722499a9f229c905a8d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
189
Assets/Mirror/Transports/FizzySteamworks/LegacyServer.cs
Normal file
189
Assets/Mirror/Transports/FizzySteamworks/LegacyServer.cs
Normal file
@ -0,0 +1,189 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public class LegacyServer : LegacyCommon, IServer
|
||||
{
|
||||
private event Action<int,String> OnConnectedWithAddress;
|
||||
private event Action<int, byte[], int> OnReceivedData;
|
||||
private event Action<int> OnDisconnected;
|
||||
private event Action<int, TransportError, string> OnReceivedError;
|
||||
|
||||
private BidirectionalDictionary<CSteamID, int> steamToMirrorIds;
|
||||
private int maxConnections;
|
||||
private int nextConnectionID;
|
||||
|
||||
private static LegacyServer server;
|
||||
|
||||
public static LegacyServer CreateServer(FizzySteamworks transport, int maxConnections)
|
||||
{
|
||||
server = new LegacyServer(transport, maxConnections);
|
||||
|
||||
server.OnConnectedWithAddress += (id,addres) => transport.OnServerConnectedWithAddress.Invoke(id,addres);
|
||||
server.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id);
|
||||
server.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment<byte>(data), channel);
|
||||
server.OnReceivedError += (id, error, reason) => transport.OnServerError.Invoke(id, error, reason);
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
InteropHelp.TestIfAvailableGameServer();
|
||||
#else
|
||||
InteropHelp.TestIfAvailableClient();
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.LogError("SteamWorks not initialized.");
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private LegacyServer(FizzySteamworks transport, int maxConnections) : base(transport)
|
||||
{
|
||||
this.maxConnections = maxConnections;
|
||||
steamToMirrorIds = new BidirectionalDictionary<CSteamID, int>();
|
||||
nextConnectionID = 1;
|
||||
}
|
||||
|
||||
protected override void OnNewConnection(P2PSessionRequest_t result)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote);
|
||||
#else
|
||||
SteamNetworking.AcceptP2PSessionWithUser(result.m_steamIDRemote);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Steam Server error durring new connect, {ex.Message}");
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnReceiveInternalData(InternalMessages type, CSteamID clientSteamID)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case InternalMessages.CONNECT:
|
||||
if (steamToMirrorIds.Count >= maxConnections)
|
||||
{
|
||||
SendInternal(clientSteamID, InternalMessages.DISCONNECT);
|
||||
return;
|
||||
}
|
||||
|
||||
SendInternal(clientSteamID, InternalMessages.ACCEPT_CONNECT);
|
||||
|
||||
int connectionId = nextConnectionID++;
|
||||
steamToMirrorIds.Add(clientSteamID, connectionId);
|
||||
OnConnectedWithAddress.Invoke(connectionId,server.ServerGetClientAddress(connectionId));
|
||||
Debug.Log($"Client with SteamID {clientSteamID} connected. Assigning connection id {connectionId}");
|
||||
break;
|
||||
case InternalMessages.DISCONNECT:
|
||||
if (steamToMirrorIds.TryGetValue(clientSteamID, out int connId))
|
||||
{
|
||||
OnDisconnected.Invoke(connId);
|
||||
CloseP2PSessionWithUser(clientSteamID);
|
||||
steamToMirrorIds.Remove(clientSteamID);
|
||||
Debug.Log($"Client with SteamID {clientSteamID} disconnected.");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Debug.Log("Received unknown message type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnReceiveData(byte[] data, CSteamID clientSteamID, int channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (steamToMirrorIds.TryGetValue(clientSteamID, out int connectionId))
|
||||
{
|
||||
OnReceivedData.Invoke(connectionId, data, channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseP2PSessionWithUser(clientSteamID);
|
||||
Debug.LogError("Data received from steam client thats not known " + clientSteamID);
|
||||
OnReceivedError.Invoke(-1, TransportError.DnsResolve, "ERROR Unknown SteamID");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Error while recive data {ex.Message}");
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect(int connectionId)
|
||||
{
|
||||
if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamID))
|
||||
{
|
||||
SendInternal(steamID, InternalMessages.DISCONNECT);
|
||||
steamToMirrorIds.Remove(connectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
foreach (KeyValuePair<CSteamID, int> client in steamToMirrorIds)
|
||||
{
|
||||
Disconnect(client.Value);
|
||||
WaitForClose(client.Key);
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Send(int connectionId, byte[] data, int channelId)
|
||||
{
|
||||
if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamId))
|
||||
{
|
||||
Send(steamId, data, channelId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Trying to send on unknown connection: " + connectionId);
|
||||
OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection");
|
||||
Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
if (steamToMirrorIds.TryGetValue(connectionId, out CSteamID steamId))
|
||||
{
|
||||
return steamId.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Trying to get info on unknown connection: " + connectionId);
|
||||
OnReceivedError.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionFailed(CSteamID remoteId)
|
||||
{
|
||||
int connectionId = steamToMirrorIds.TryGetValue(remoteId, out int connId) ? connId : nextConnectionID++;
|
||||
OnDisconnected.Invoke(connectionId);
|
||||
|
||||
steamToMirrorIds.Remove(remoteId);
|
||||
}
|
||||
public void FlushData() { }
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6b4fdab451e8c647b7e26d8c4d934f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
246
Assets/Mirror/Transports/FizzySteamworks/NextClient.cs
Normal file
246
Assets/Mirror/Transports/FizzySteamworks/NextClient.cs
Normal file
@ -0,0 +1,246 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public class NextClient : NextCommon, IClient
|
||||
{
|
||||
public bool Connected { get; private set; }
|
||||
public bool Error { get; private set; }
|
||||
|
||||
private TimeSpan ConnectionTimeout;
|
||||
|
||||
private event Action<byte[], int> OnReceivedData;
|
||||
private event Action OnConnected;
|
||||
private event Action OnDisconnected;
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> c_onConnectionChange = null;
|
||||
|
||||
private CancellationTokenSource cancelToken;
|
||||
private TaskCompletionSource<Task> connectedComplete;
|
||||
private CSteamID hostSteamID = CSteamID.Nil;
|
||||
private HSteamNetConnection HostConnection;
|
||||
private List<Action> BufferedData;
|
||||
|
||||
private NextClient(FizzySteamworks transport)
|
||||
{
|
||||
ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.Timeout));
|
||||
BufferedData = new List<Action>();
|
||||
}
|
||||
|
||||
public static NextClient CreateClient(FizzySteamworks transport, string host)
|
||||
{
|
||||
NextClient c = new NextClient(transport);
|
||||
|
||||
c.OnConnected += () => transport.OnClientConnected.Invoke();
|
||||
c.OnDisconnected += () => transport.OnClientDisconnected.Invoke();
|
||||
c.OnReceivedData += (data, ch) => transport.OnClientDataReceived.Invoke(new ArraySegment<byte>(data), ch);
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
c.Connect(host);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?");
|
||||
c.Error = true;
|
||||
c.OnConnectionFailed();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Unexpected exception: {ex.Message}");
|
||||
c.Error = true;
|
||||
c.OnConnectionFailed();
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
private async void Connect(string host)
|
||||
{
|
||||
cancelToken = new CancellationTokenSource();
|
||||
c_onConnectionChange = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
|
||||
try
|
||||
{
|
||||
hostSteamID = new CSteamID(UInt64.Parse(host));
|
||||
connectedComplete = new TaskCompletionSource<Task>();
|
||||
OnConnected += SetConnectedComplete;
|
||||
|
||||
SteamNetworkingIdentity smi = new SteamNetworkingIdentity();
|
||||
smi.SetSteamID(hostSteamID);
|
||||
|
||||
SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { };
|
||||
HostConnection = SteamNetworkingSockets.ConnectP2P(ref smi, 0, options.Length, options);
|
||||
|
||||
Task connectedCompleteTask = connectedComplete.Task;
|
||||
Task timeOutTask = Task.Delay(ConnectionTimeout, cancelToken.Token);
|
||||
|
||||
if (await Task.WhenAny(connectedCompleteTask, timeOutTask) != connectedCompleteTask)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
Debug.LogError($"The connection attempt was cancelled.");
|
||||
}
|
||||
else if (timeOutTask.IsCompleted)
|
||||
{
|
||||
Debug.LogError($"Connection to {host} timed out.");
|
||||
}
|
||||
|
||||
OnConnected -= SetConnectedComplete;
|
||||
OnConnectionFailed();
|
||||
}
|
||||
|
||||
OnConnected -= SetConnectedComplete;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
Debug.LogError($"Connection string was not in the right format. Did you enter a SteamId?");
|
||||
Error = true;
|
||||
OnConnectionFailed();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Unexpected exception: {ex.Message}");
|
||||
Error = true;
|
||||
OnConnectionFailed();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Error)
|
||||
{
|
||||
Debug.LogError("Connection failed.");
|
||||
OnConnectionFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t param)
|
||||
{
|
||||
ulong clientSteamID = param.m_info.m_identityRemote.GetSteamID64();
|
||||
if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected)
|
||||
{
|
||||
Connected = true;
|
||||
OnConnected.Invoke();
|
||||
Debug.Log("Connection established.");
|
||||
|
||||
if (BufferedData.Count > 0)
|
||||
{
|
||||
Debug.Log($"{BufferedData.Count} received before connection was established. Processing now.");
|
||||
{
|
||||
foreach (Action a in BufferedData)
|
||||
{
|
||||
a();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer || param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally)
|
||||
{
|
||||
Debug.Log($"Connection was closed by peer, {param.m_info.m_szEndDebug}");
|
||||
Disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Connection state changed: {param.m_info.m_eState.ToString()} - {param.m_info.m_szEndDebug}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
cancelToken?.Cancel();
|
||||
Dispose();
|
||||
|
||||
if (Connected)
|
||||
{
|
||||
InternalDisconnect();
|
||||
}
|
||||
|
||||
if (HostConnection.m_HSteamNetConnection != 0)
|
||||
{
|
||||
Debug.Log("Sending Disconnect message");
|
||||
SteamNetworkingSockets.CloseConnection(HostConnection, 0, "Graceful disconnect", false);
|
||||
HostConnection.m_HSteamNetConnection = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected void Dispose()
|
||||
{
|
||||
if (c_onConnectionChange != null)
|
||||
{
|
||||
c_onConnectionChange.Dispose();
|
||||
c_onConnectionChange = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InternalDisconnect()
|
||||
{
|
||||
Connected = false;
|
||||
OnDisconnected.Invoke();
|
||||
Debug.Log("Disconnected.");
|
||||
SteamNetworkingSockets.CloseConnection(HostConnection, 0, "Disconnected", false);
|
||||
}
|
||||
|
||||
public void ReceiveData()
|
||||
{
|
||||
IntPtr[] ptrs = new IntPtr[MAX_MESSAGES];
|
||||
int messageCount;
|
||||
|
||||
if ((messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(HostConnection, ptrs, MAX_MESSAGES)) > 0)
|
||||
{
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
(byte[] data, int ch) = ProcessMessage(ptrs[i]);
|
||||
if (Connected)
|
||||
{
|
||||
OnReceivedData(data, ch);
|
||||
}
|
||||
else
|
||||
{
|
||||
BufferedData.Add(() => OnReceivedData(data, ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] data, int channelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
EResult res = SendSocket(HostConnection, data, channelId);
|
||||
|
||||
if (res == EResult.k_EResultNoConnection || res == EResult.k_EResultInvalidParam)
|
||||
{
|
||||
Debug.Log($"Connection to server was lost.");
|
||||
InternalDisconnect();
|
||||
}
|
||||
else if (res != EResult.k_EResultOK)
|
||||
{
|
||||
Debug.LogError($"Could not send: {res.ToString()}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"SteamNetworking exception during Send: {ex.Message}");
|
||||
InternalDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task);
|
||||
private void OnConnectionFailed() => OnDisconnected.Invoke();
|
||||
public void FlushData()
|
||||
{
|
||||
SteamNetworkingSockets.FlushMessagesOnConnection(HostConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
11
Assets/Mirror/Transports/FizzySteamworks/NextClient.cs.meta
Normal file
11
Assets/Mirror/Transports/FizzySteamworks/NextClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a76b4f832739fae40ac8bebdc30ad63d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
48
Assets/Mirror/Transports/FizzySteamworks/NextCommon.cs
Normal file
48
Assets/Mirror/Transports/FizzySteamworks/NextCommon.cs
Normal file
@ -0,0 +1,48 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public abstract class NextCommon
|
||||
{
|
||||
protected const int MAX_MESSAGES = 256;
|
||||
|
||||
protected EResult SendSocket(HSteamNetConnection conn, byte[] data, int channelId)
|
||||
{
|
||||
Array.Resize(ref data, data.Length + 1);
|
||||
data[data.Length - 1] = (byte)channelId;
|
||||
|
||||
GCHandle pinnedArray = GCHandle.Alloc(data, GCHandleType.Pinned);
|
||||
IntPtr pData = pinnedArray.AddrOfPinnedObject();
|
||||
int sendFlag = channelId == Channels.Unreliable ? Constants.k_nSteamNetworkingSend_Unreliable : Constants.k_nSteamNetworkingSend_Reliable;
|
||||
#if UNITY_SERVER
|
||||
EResult res = SteamGameServerNetworkingSockets.SendMessageToConnection(conn, pData, (uint)data.Length, sendFlag, out long _);
|
||||
#else
|
||||
EResult res = SteamNetworkingSockets.SendMessageToConnection(conn, pData, (uint)data.Length, sendFlag, out long _);
|
||||
#endif
|
||||
if (res != EResult.k_EResultOK)
|
||||
{
|
||||
Debug.LogWarning($"Send issue: {res}");
|
||||
}
|
||||
|
||||
pinnedArray.Free();
|
||||
return res;
|
||||
}
|
||||
|
||||
protected (byte[], int) ProcessMessage(IntPtr ptrs)
|
||||
{
|
||||
SteamNetworkingMessage_t data = Marshal.PtrToStructure<SteamNetworkingMessage_t>(ptrs);
|
||||
byte[] managedArray = new byte[data.m_cbSize];
|
||||
Marshal.Copy(data.m_pData, managedArray, 0, data.m_cbSize);
|
||||
SteamNetworkingMessage_t.Release(ptrs);
|
||||
|
||||
int channel = managedArray[managedArray.Length - 1];
|
||||
Array.Resize(ref managedArray, managedArray.Length - 1);
|
||||
return (managedArray, channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
11
Assets/Mirror/Transports/FizzySteamworks/NextCommon.cs.meta
Normal file
11
Assets/Mirror/Transports/FizzySteamworks/NextCommon.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79668d9d397a0fc4982afadecd11c869
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
248
Assets/Mirror/Transports/FizzySteamworks/NextServer.cs
Normal file
248
Assets/Mirror/Transports/FizzySteamworks/NextServer.cs
Normal file
@ -0,0 +1,248 @@
|
||||
#if !DISABLESTEAMWORKS
|
||||
using Steamworks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.FizzySteam
|
||||
{
|
||||
public class NextServer : NextCommon, IServer
|
||||
{
|
||||
private event Action<int,string> OnConnectedWithAddress;
|
||||
private event Action<int, byte[], int> OnReceivedData;
|
||||
private event Action<int> OnDisconnected;
|
||||
private event Action<int, TransportError, string> OnReceivedError;
|
||||
|
||||
private BidirectionalDictionary<HSteamNetConnection, int> connToMirrorID;
|
||||
private BidirectionalDictionary<CSteamID, int> steamIDToMirrorID;
|
||||
private int maxConnections;
|
||||
private int nextConnectionID;
|
||||
|
||||
private HSteamListenSocket listenSocket;
|
||||
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> c_onConnectionChange = null;
|
||||
|
||||
private static NextServer server;
|
||||
private NextServer(int maxConnections)
|
||||
{
|
||||
this.maxConnections = maxConnections;
|
||||
connToMirrorID = new BidirectionalDictionary<HSteamNetConnection, int>();
|
||||
steamIDToMirrorID = new BidirectionalDictionary<CSteamID, int>();
|
||||
nextConnectionID = 1;
|
||||
#if UNITY_SERVER
|
||||
c_onConnectionChange = Callback<SteamNetConnectionStatusChangedCallback_t>.CreateGameServer(OnConnectionStatusChanged);
|
||||
#else
|
||||
c_onConnectionChange = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static NextServer CreateServer(FizzySteamworks transport, int maxConnections)
|
||||
{
|
||||
server = new NextServer(maxConnections);
|
||||
|
||||
server.OnConnectedWithAddress += (id,addres) => transport.OnServerConnectedWithAddress.Invoke(id,addres);
|
||||
server.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id);
|
||||
server.OnReceivedData += (id, data, ch) => transport.OnServerDataReceived.Invoke(id, new ArraySegment<byte>(data), ch);
|
||||
server.OnReceivedError += (id, error, reason) => transport.OnServerError.Invoke(id, error, reason);
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
|
||||
server.Host();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private void Host()
|
||||
{
|
||||
SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { };
|
||||
#if UNITY_SERVER
|
||||
listenSocket = SteamGameServerNetworkingSockets.CreateListenSocketP2P(0, options.Length, options);
|
||||
#else
|
||||
listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(0, options.Length, options);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t param)
|
||||
{
|
||||
ulong clientSteamID = param.m_info.m_identityRemote.GetSteamID64();
|
||||
if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting)
|
||||
{
|
||||
if (connToMirrorID.Count >= maxConnections)
|
||||
{
|
||||
Debug.Log($"Incoming connection {clientSteamID} would exceed max connection count. Rejecting.");
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingSockets.CloseConnection(param.m_hConn, 0, "Max Connection Count", false);
|
||||
#else
|
||||
SteamNetworkingSockets.CloseConnection(param.m_hConn, 0, "Max Connection Count", false);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
EResult res;
|
||||
|
||||
#if UNITY_SERVER
|
||||
if ((res = SteamGameServerNetworkingSockets.AcceptConnection(param.m_hConn)) == EResult.k_EResultOK)
|
||||
#else
|
||||
if ((res = SteamNetworkingSockets.AcceptConnection(param.m_hConn)) == EResult.k_EResultOK)
|
||||
#endif
|
||||
{
|
||||
Debug.Log($"Accepting connection {clientSteamID}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Connection {clientSteamID} could not be accepted: {res}");
|
||||
}
|
||||
}
|
||||
else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected)
|
||||
{
|
||||
int connectionId = nextConnectionID++;
|
||||
connToMirrorID.Add(param.m_hConn, connectionId);
|
||||
steamIDToMirrorID.Add(param.m_info.m_identityRemote.GetSteamID(), connectionId);
|
||||
OnConnectedWithAddress?.Invoke(connectionId,server.ServerGetClientAddress(connectionId));
|
||||
Debug.Log($"Client with SteamID {clientSteamID} connected. Assigning connection id {connectionId}");
|
||||
}
|
||||
else if (param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer || param.m_info.m_eState == ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally)
|
||||
{
|
||||
if (connToMirrorID.TryGetValue(param.m_hConn, out int connId))
|
||||
{
|
||||
InternalDisconnect(connId, param.m_hConn);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Connection {clientSteamID} state changed: {param.m_info.m_eState}");
|
||||
}
|
||||
}
|
||||
|
||||
private void InternalDisconnect(int connId, HSteamNetConnection socket)
|
||||
{
|
||||
OnDisconnected?.Invoke(connId);
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingSockets.CloseConnection(socket, 0, "Graceful disconnect", false);
|
||||
#else
|
||||
SteamNetworkingSockets.CloseConnection(socket, 0, "Graceful disconnect", false);
|
||||
#endif
|
||||
connToMirrorID.Remove(connId);
|
||||
steamIDToMirrorID.Remove(connId);
|
||||
Debug.Log($"Client with ConnectionID {connId} disconnected.");
|
||||
}
|
||||
|
||||
public void Disconnect(int connectionId)
|
||||
{
|
||||
if (connToMirrorID.TryGetValue(connectionId, out HSteamNetConnection conn))
|
||||
{
|
||||
Debug.Log($"Connection id {connectionId} disconnected.");
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingSockets.CloseConnection(conn, 0, "Disconnected by server", false);
|
||||
#else
|
||||
SteamNetworkingSockets.CloseConnection(conn, 0, "Disconnected by server", false);
|
||||
#endif
|
||||
steamIDToMirrorID.Remove(connectionId);
|
||||
connToMirrorID.Remove(connectionId);
|
||||
OnDisconnected?.Invoke(connectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushData()
|
||||
{
|
||||
foreach (HSteamNetConnection conn in connToMirrorID.FirstTypes.ToList())
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingSockets.FlushMessagesOnConnection(conn);
|
||||
#else
|
||||
SteamNetworkingSockets.FlushMessagesOnConnection(conn);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceiveData()
|
||||
{
|
||||
foreach (HSteamNetConnection conn in connToMirrorID.FirstTypes.ToList())
|
||||
{
|
||||
if (connToMirrorID.TryGetValue(conn, out int connId))
|
||||
{
|
||||
IntPtr[] ptrs = new IntPtr[MAX_MESSAGES];
|
||||
int messageCount;
|
||||
|
||||
#if UNITY_SERVER
|
||||
if ((messageCount = SteamGameServerNetworkingSockets.ReceiveMessagesOnConnection(conn, ptrs, MAX_MESSAGES)) > 0)
|
||||
#else
|
||||
if ((messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(conn, ptrs, MAX_MESSAGES)) > 0)
|
||||
#endif
|
||||
{
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
(byte[] data, int ch) = ProcessMessage(ptrs[i]);
|
||||
OnReceivedData?.Invoke(connId, data, ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(int connectionId, byte[] data, int channelId)
|
||||
{
|
||||
if (connToMirrorID.TryGetValue(connectionId, out HSteamNetConnection conn))
|
||||
{
|
||||
EResult res = SendSocket(conn, data, channelId);
|
||||
|
||||
if (res == EResult.k_EResultNoConnection || res == EResult.k_EResultInvalidParam)
|
||||
{
|
||||
Debug.Log($"Connection to {connectionId} was lost.");
|
||||
InternalDisconnect(connectionId, conn);
|
||||
}
|
||||
else if (res != EResult.k_EResultOK)
|
||||
{
|
||||
Debug.LogError($"Could not send: {res}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Trying to send on an unknown connection: " + connectionId);
|
||||
OnReceivedError?.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection");
|
||||
}
|
||||
}
|
||||
|
||||
public string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
if (steamIDToMirrorID.TryGetValue(connectionId, out CSteamID steamId))
|
||||
{
|
||||
return steamId.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Trying to get info on an unknown connection: " + connectionId);
|
||||
OnReceivedError?.Invoke(connectionId, TransportError.Unexpected, "ERROR Unknown Connection");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingSockets.CloseListenSocket(listenSocket);
|
||||
#else
|
||||
SteamNetworkingSockets.CloseListenSocket(listenSocket);
|
||||
#endif
|
||||
|
||||
c_onConnectionChange?.Dispose();
|
||||
c_onConnectionChange = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !DISABLESTEAMWORKS
|
11
Assets/Mirror/Transports/FizzySteamworks/NextServer.cs.meta
Normal file
11
Assets/Mirror/Transports/FizzySteamworks/NextServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84bd1cd26eae15647a6f0a27cc0e2b69
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
55
Assets/Mirror/Transports/FizzySteamworks/README.md
Normal file
55
Assets/Mirror/Transports/FizzySteamworks/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# FizzySteamworks
|
||||
|
||||
This is a community maintained repo forked from **[RayStorm](https://github.com/Raystorms/FizzySteamyMirror)**.
|
||||
|
||||
Mirror **[docs](https://mirror-networking.com/docs/Transports/Fizzy.html)** and the official community **[Discord](https://discord.gg/N9QVxbM)**.
|
||||
|
||||
FizzySteamworks brings together **[Steam](https://store.steampowered.com)** and **[Mirror](https://github.com/vis2k/Mirror)** . It supports both the old SteamNetworking and the new SteamSockets.
|
||||
|
||||
## Dependencies
|
||||
You must have Mirror installed and working before you can use this transport.
|
||||
**[Mirror](https://github.com/vis2k/Mirror)** FizzySteamworks is also obviously dependant on Mirror which is a streamline, bug fixed, maintained version of UNET for Unity.
|
||||
|
||||
You must have Steamworks.NET installed and working before you can use this transport.
|
||||
**[Steamworks.NET](https://github.com/rlabrecque/Steamworks.NET)** FizzySteamworks relies on Steamworks.NET to communicate with the **[Steamworks API](https://partner.steamgames.com/doc/sdk)**. **Requires .Net 4.x**
|
||||
|
||||
## Installation
|
||||
### Unity Package Manager
|
||||
|
||||
Unity Package Manager support is still fairly new but you can use it like so:
|
||||
|
||||
1. Open the Package Manager
|
||||
2. Click the "+" (plus) button located in the upper left of the window
|
||||
3. Select the "Add package from git URL..." option
|
||||
4. Enter the following URL:
|
||||
`https://github.com/Chykary/FizzySteamworks.git?path=/com.mirror.steamworks.net`
|
||||
5. Click the "Add" button and wait several seconds for the system to download and install the Steamworks.NET package from GitHub.
|
||||
|
||||
### Manual
|
||||
|
||||
Fewer steps but more error prone and subject to being out of date with the latest changes:
|
||||
|
||||
1. Download the latest [unitypackage](https://github.com/Chykary/FizzySteamworks/releases) from the release section.
|
||||
2. Import the package into Unity.
|
||||
|
||||
|
||||
## Setting Up
|
||||
|
||||
1. Install Steamworks.NET instructions can be found [here](https://github.com/rlabrecque/Steamworks.NET).
|
||||
2. Install Mirror **(Requires Mirror 35.0+)** from the Unity asset store **[Download Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321)**.
|
||||
3. Install FizzySteamworks from package manager as discribed in the above Install step.
|
||||
3. In your **"NetworkManager"** object replace **"KCP"** with **"FizzySteamworks"**.
|
||||
|
||||
## Host
|
||||
To be able to have your game working you need to make sure you have Steam running in the background and that the Steam API initalized correctly. You can then call StartHost and use Mirror as you normally would.
|
||||
|
||||
## Client
|
||||
To connect a client to a host or server you need the CSteamID of the target you wish to connect to this is used in place of IP/Port. If your creating a Peer to Peer architecture then you would use the CSteamID of the host, this is Steam user ID as a ulong value. If you are creating a Client Server architecture then you will be using the CSteamID issued to the Steam Game Server when it logs the Steam API on. This is an advanced use case supported by Heathen's Steamworks but requires additional custom code if your using Steamworks.NET directly.
|
||||
|
||||
1. Send the game to your buddy.
|
||||
2. Your buddy needs the host or game server **steamID64** to be able to connect.
|
||||
3. Place the **steamID64** into **"localhost"** then click **"Client"**
|
||||
5. Then they will be connected to your server be that your machine as a P2P connection or yoru Steam Game Server as a Client Server connection.
|
||||
|
||||
## Testing your game locally
|
||||
You cant connect to yourself locally while using **FizzySteamworks** since it's using Steams Networking which runs over Steam Client and addresses its connection based on the unique CSteamID of each actor. If you want to test your game locally you'll have to use **"Telepathy Transport"** instead of **"FizzySteamworks"**.
|
7
Assets/Mirror/Transports/FizzySteamworks/README.md.meta
Normal file
7
Assets/Mirror/Transports/FizzySteamworks/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76396690c716d454c93044b15adaaffd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
1
Assets/Mirror/Transports/FizzySteamworks/version.txt
Normal file
1
Assets/Mirror/Transports/FizzySteamworks/version.txt
Normal file
@ -0,0 +1 @@
|
||||
6.0.1
|
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4023ceac5cd8d441ae3fd30409af2e7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
8
Assets/Mirror/Transports/KCP.meta
Normal file
8
Assets/Mirror/Transports/KCP.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea4ea5d03df6a49449fa679ac2390773
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
365
Assets/Mirror/Transports/KCP/KcpTransport.cs
Normal file
365
Assets/Mirror/Transports/KCP/KcpTransport.cs
Normal file
@ -0,0 +1,365 @@
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
public class KcpTransport : Transport, PortTransport
|
||||
{
|
||||
// scheme used by this transport
|
||||
public const string Scheme = "kcp";
|
||||
|
||||
// common
|
||||
[Header("Transport Configuration")]
|
||||
[FormerlySerializedAs("Port")]
|
||||
public ushort port = 7777;
|
||||
public ushort Port { get => port; set => port=value; }
|
||||
[Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")]
|
||||
public bool DualMode = true;
|
||||
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]
|
||||
public bool NoDelay = true;
|
||||
[Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")]
|
||||
public uint Interval = 10;
|
||||
[Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")]
|
||||
public int Timeout = 10000;
|
||||
[Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int RecvBufferSize = 1024 * 1027 * 7;
|
||||
[Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int SendBufferSize = 1024 * 1027 * 7;
|
||||
|
||||
[Header("Advanced")]
|
||||
[Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")]
|
||||
public int FastResend = 2;
|
||||
[Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")]
|
||||
/*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
|
||||
[Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")]
|
||||
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP window size can be modified to support higher loads.")]
|
||||
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")]
|
||||
public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x.
|
||||
[Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")]
|
||||
[FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")]
|
||||
public bool MaximizeSocketBuffers = true;
|
||||
|
||||
[Header("Allowed Max Message Sizes\nBased on Receive Window Size")]
|
||||
[Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")]
|
||||
[ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
[Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")]
|
||||
[ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
|
||||
// config is created from the serialized properties above.
|
||||
// we can expose the config directly in the future.
|
||||
// for now, let's not break people's old settings.
|
||||
protected KcpConfig config;
|
||||
|
||||
// use default MTU for this transport.
|
||||
const int MTU = Kcp.MTU_DEF;
|
||||
|
||||
// server & client
|
||||
protected KcpServer server;
|
||||
protected KcpClient client;
|
||||
|
||||
// debugging
|
||||
[Header("Debug")]
|
||||
public bool debugLog;
|
||||
// show statistics in OnGUI
|
||||
public bool statisticsGUI;
|
||||
// log statistics for headless servers that can't show them in GUI
|
||||
public bool statisticsLog;
|
||||
|
||||
// translate Kcp <-> Mirror channels
|
||||
public static int FromKcpChannel(KcpChannel channel) =>
|
||||
channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable;
|
||||
|
||||
public static KcpChannel ToKcpChannel(int channel) =>
|
||||
channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable;
|
||||
|
||||
public static TransportError ToTransportError(ErrorCode error)
|
||||
{
|
||||
switch(error)
|
||||
{
|
||||
case ErrorCode.DnsResolve: return TransportError.DnsResolve;
|
||||
case ErrorCode.Timeout: return TransportError.Timeout;
|
||||
case ErrorCode.Congestion: return TransportError.Congestion;
|
||||
case ErrorCode.InvalidReceive: return TransportError.InvalidReceive;
|
||||
case ErrorCode.InvalidSend: return TransportError.InvalidSend;
|
||||
case ErrorCode.ConnectionClosed: return TransportError.ConnectionClosed;
|
||||
case ErrorCode.Unexpected: return TransportError.Unexpected;
|
||||
default: throw new InvalidCastException($"KCP: missing error translation for {error}");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// logging
|
||||
// Log.Info should use Debug.Log if enabled, or nothing otherwise
|
||||
// (don't want to spam the console on headless servers)
|
||||
if (debugLog)
|
||||
Log.Info = Debug.Log;
|
||||
else
|
||||
Log.Info = _ => {};
|
||||
Log.Warning = Debug.LogWarning;
|
||||
Log.Error = Debug.LogError;
|
||||
|
||||
// create config from serialized settings
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new KcpClient(
|
||||
() => OnClientConnected.Invoke(),
|
||||
(message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)),
|
||||
() => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708
|
||||
(error, reason) => OnClientError?.Invoke(ToTransportError(error), reason), // may be null during shutdown: https://github.com/MirrorNetworking/Mirror/issues/3876
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new KcpServer(
|
||||
(connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()),
|
||||
(connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)),
|
||||
(connectionId) => OnServerDisconnected.Invoke(connectionId),
|
||||
(connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Log.Info("KcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU);
|
||||
}
|
||||
|
||||
// all except WebGL
|
||||
// Do not change this back to using Application.platform
|
||||
// because that doesn't work in the Editor!
|
||||
public override bool Available() =>
|
||||
#if UNITY_WEBGL
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
// client
|
||||
public override bool ClientConnected() => client.connected;
|
||||
public override void ClientConnect(string address)
|
||||
{
|
||||
client.Connect(address, Port);
|
||||
}
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
int serverPort = uri.IsDefaultPort ? Port : uri.Port;
|
||||
client.Connect(uri.Host, (ushort)serverPort);
|
||||
}
|
||||
public override void ClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
client.Send(segment, ToKcpChannel(channelId));
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnClientDataSent?.Invoke(segment, channelId);
|
||||
}
|
||||
public override void ClientDisconnect() => client.Disconnect();
|
||||
// process incoming in early update
|
||||
public override void ClientEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
if (enabled) client.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
public override void ClientLateUpdate() => client.TickOutgoing();
|
||||
|
||||
// server
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
builder.Port = Port;
|
||||
return builder.Uri;
|
||||
}
|
||||
public override bool ServerActive() => server.IsActive();
|
||||
public override void ServerStart() => server.Start(Port);
|
||||
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
server.Send(connectionId, segment, ToKcpChannel(channelId));
|
||||
|
||||
// call event. might be null if no statistics are listening etc.
|
||||
OnServerDataSent?.Invoke(connectionId, segment, channelId);
|
||||
}
|
||||
public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
|
||||
return endPoint.PrettyAddress();
|
||||
}
|
||||
public override void ServerStop() => server.Stop();
|
||||
public override void ServerEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
if (enabled) server.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
public override void ServerLateUpdate() => server.TickOutgoing();
|
||||
|
||||
// common
|
||||
public override void Shutdown() {}
|
||||
|
||||
// max message size
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable)
|
||||
{
|
||||
// switch to kcp channel.
|
||||
// unreliable or reliable.
|
||||
// default to reliable just to be sure.
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Unreliable:
|
||||
return KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
default:
|
||||
return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize);
|
||||
}
|
||||
}
|
||||
|
||||
// kcp reliable channel max packet size is MTU * WND_RCV
|
||||
// this allows 144kb messages. but due to head of line blocking, all
|
||||
// other messages would have to wait until the maxed size one is
|
||||
// delivered. batching 144kb messages each time would be EXTREMELY slow
|
||||
// and fill the send queue nearly immediately when using it over the
|
||||
// network.
|
||||
// => instead we always use MTU sized batches.
|
||||
// => people can still send maxed size if needed.
|
||||
public override int GetBatchThreshold(int channelId) =>
|
||||
KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
|
||||
// server statistics
|
||||
// LONG to avoid int overflows with connections.Sum.
|
||||
// see also: https://github.com/vis2k/Mirror/pull/2777
|
||||
public long GetAverageMaxSendRate() =>
|
||||
server.connections.Count > 0
|
||||
? server.connections.Values.Sum(conn => conn.MaxSendRate) / server.connections.Count
|
||||
: 0;
|
||||
public long GetAverageMaxReceiveRate() =>
|
||||
server.connections.Count > 0
|
||||
? server.connections.Values.Sum(conn => conn.MaxReceiveRate) / server.connections.Count
|
||||
: 0;
|
||||
long GetTotalSendQueue() =>
|
||||
server.connections.Values.Sum(conn => conn.SendQueueCount);
|
||||
long GetTotalReceiveQueue() =>
|
||||
server.connections.Values.Sum(conn => conn.ReceiveQueueCount);
|
||||
long GetTotalSendBuffer() =>
|
||||
server.connections.Values.Sum(conn => conn.SendBufferCount);
|
||||
long GetTotalReceiveBuffer() =>
|
||||
server.connections.Values.Sum(conn => conn.ReceiveBufferCount);
|
||||
|
||||
// PrettyBytes function from DOTSNET
|
||||
// pretty prints bytes as KB/MB/GB/etc.
|
||||
// long to support > 2GB
|
||||
// divides by floats to return "2.5MB" etc.
|
||||
public static string PrettyBytes(long bytes)
|
||||
{
|
||||
// bytes
|
||||
if (bytes < 1024)
|
||||
return $"{bytes} B";
|
||||
// kilobytes
|
||||
else if (bytes < 1024L * 1024L)
|
||||
return $"{(bytes / 1024f):F2} KB";
|
||||
// megabytes
|
||||
else if (bytes < 1024 * 1024L * 1024L)
|
||||
return $"{(bytes / (1024f * 1024f)):F2} MB";
|
||||
// gigabytes
|
||||
return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB";
|
||||
}
|
||||
|
||||
protected virtual void OnGUIStatistics()
|
||||
{
|
||||
GUILayout.BeginArea(new Rect(5, 110, 300, 300));
|
||||
|
||||
if (ServerActive())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("SERVER");
|
||||
GUILayout.Label($" connections: {server.connections.Count}");
|
||||
GUILayout.Label($" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s");
|
||||
GUILayout.Label($" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s");
|
||||
GUILayout.Label($" SendQueue: {GetTotalSendQueue()}");
|
||||
GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}");
|
||||
GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}");
|
||||
GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("CLIENT");
|
||||
GUILayout.Label($" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s");
|
||||
GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s");
|
||||
GUILayout.Label($" SendQueue: {client.SendQueueCount}");
|
||||
GUILayout.Label($" ReceiveQueue: {client.ReceiveQueueCount}");
|
||||
GUILayout.Label($" SendBuffer: {client.SendBufferCount}");
|
||||
GUILayout.Label($" ReceiveBuffer: {client.ReceiveBufferCount}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (statisticsGUI) OnGUIStatistics();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected virtual void OnLogStatistics()
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" connections: {server.connections.Count}\n";
|
||||
log += $" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n";
|
||||
log += $" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n";
|
||||
log += $" SendQueue: {GetTotalSendQueue()}\n";
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s\n";
|
||||
log += $" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s\n";
|
||||
log += $" SendQueue: {client.SendQueueCount}\n";
|
||||
log += $" ReceiveQueue: {client.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"KCP [{port}]";
|
||||
}
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
18
Assets/Mirror/Transports/KCP/KcpTransport.cs.meta
Normal file
18
Assets/Mirror/Transports/KCP/KcpTransport.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b0fecffa3f624585964b0d0eb21b18e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/KcpTransport.cs
|
||||
uploadId: 736421
|
327
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs
Normal file
327
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs
Normal file
@ -0,0 +1,327 @@
|
||||
// Threaded version of our KCP transport.
|
||||
// Elevates a few milliseconds of transport computations into a worker thread.
|
||||
//
|
||||
//#if MIRROR <- commented out because MIRROR isn't defined on first import yet
|
||||
using System;
|
||||
using System.Net;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace kcp2k
|
||||
{
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
|
||||
[DisallowMultipleComponent]
|
||||
public class ThreadedKcpTransport : ThreadedTransport, PortTransport
|
||||
{
|
||||
// scheme used by this transport
|
||||
public const string Scheme = "kcp";
|
||||
|
||||
// common
|
||||
[Header("Transport Configuration")]
|
||||
[FormerlySerializedAs("Port")]
|
||||
public ushort port = 7777;
|
||||
public ushort Port { get => port; set => port=value; }
|
||||
[Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")]
|
||||
public bool DualMode = true;
|
||||
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]
|
||||
public bool NoDelay = true;
|
||||
[Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")]
|
||||
public uint Interval = 10;
|
||||
[Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")]
|
||||
public int Timeout = 10000;
|
||||
[Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int RecvBufferSize = 1024 * 1027 * 7;
|
||||
[Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")]
|
||||
public int SendBufferSize = 1024 * 1027 * 7;
|
||||
|
||||
[Header("Advanced")]
|
||||
[Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")]
|
||||
public int FastResend = 2;
|
||||
[Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")]
|
||||
/*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use.
|
||||
[Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")]
|
||||
public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP window size can be modified to support higher loads.")]
|
||||
public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more.
|
||||
[Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")]
|
||||
public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x.
|
||||
[Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")]
|
||||
[FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")]
|
||||
public bool MaximizeSocketBuffers = true;
|
||||
|
||||
[Header("Allowed Max Message Sizes\nBased on Receive Window Size")]
|
||||
[Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")]
|
||||
[ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
[Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")]
|
||||
[ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate
|
||||
|
||||
// config is created from the serialized properties above.
|
||||
// we can expose the config directly in the future.
|
||||
// for now, let's not break people's old settings.
|
||||
protected KcpConfig config;
|
||||
|
||||
// use default MTU for this transport.
|
||||
const int MTU = Kcp.MTU_DEF;
|
||||
|
||||
// server & client
|
||||
protected KcpServer server; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD!
|
||||
protected KcpClient client; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD!
|
||||
|
||||
// copy MonoBehaviour.enabled for thread safe access
|
||||
volatile bool enabledCopy = true;
|
||||
|
||||
// debugging
|
||||
[Header("Debug")]
|
||||
public bool debugLog;
|
||||
// show statistics in OnGUI
|
||||
public bool statisticsGUI;
|
||||
// log statistics for headless servers that can't show them in GUI
|
||||
public bool statisticsLog;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
// logging
|
||||
// Log.Info should use Debug.Log if enabled, or nothing otherwise
|
||||
// (don't want to spam the console on headless servers)
|
||||
// THREAD SAFE thanks to ThreadLog.cs
|
||||
if (debugLog)
|
||||
Log.Info = Debug.Log;
|
||||
else
|
||||
Log.Info = _ => {};
|
||||
Log.Warning = Debug.LogWarning;
|
||||
Log.Error = Debug.LogError;
|
||||
|
||||
// create config from serialized settings
|
||||
config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit);
|
||||
|
||||
// client (NonAlloc version is not necessary anymore)
|
||||
client = new KcpClient(
|
||||
OnThreadedClientConnected,
|
||||
(message, channel) => OnThreadedClientReceive(message, KcpTransport.FromKcpChannel(channel)),
|
||||
OnThreadedClientDisconnected,
|
||||
(error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
// server
|
||||
server = new KcpServer(
|
||||
OnThreadedServerConnected,
|
||||
(connectionId, message, channel) => OnThreadedServerReceive(connectionId, message, KcpTransport.FromKcpChannel(channel)),
|
||||
OnThreadedServerDisconnected,
|
||||
(connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason),
|
||||
config
|
||||
);
|
||||
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
// call base after creating kcp.
|
||||
// it'll be used by the created thread immediately.
|
||||
base.Awake();
|
||||
|
||||
Log.Info("ThreadedKcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// show max message sizes in inspector for convenience.
|
||||
// 'config' isn't available in edit mode yet, so use MTU define.
|
||||
ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize);
|
||||
UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU);
|
||||
}
|
||||
|
||||
// copy MonoBehaviour.enabled for thread safe use
|
||||
void OnEnable() => enabledCopy = true;
|
||||
void OnDisable() => enabledCopy = true;
|
||||
|
||||
// all except WebGL
|
||||
// Do not change this back to using Application.platform
|
||||
// because that doesn't work in the Editor!
|
||||
public override bool Available() =>
|
||||
#if UNITY_WEBGL
|
||||
false;
|
||||
#else
|
||||
true;
|
||||
#endif
|
||||
|
||||
protected override void ThreadedClientConnect(string address) => client.Connect(address, Port);
|
||||
protected override void ThreadedClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri));
|
||||
|
||||
int serverPort = uri.IsDefaultPort ? Port : uri.Port;
|
||||
client.Connect(uri.Host, (ushort)serverPort);
|
||||
}
|
||||
protected override void ThreadedClientSend(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
client.Send(segment, KcpTransport.ToKcpChannel(channelId));
|
||||
|
||||
// thread safe version for statistics
|
||||
OnThreadedClientSend(segment, channelId);
|
||||
}
|
||||
protected override void ThreadedClientDisconnect() => client.Disconnect();
|
||||
// process incoming in early update
|
||||
protected override void ThreadedClientEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
// => enabledCopy for thread safe use
|
||||
if (enabledCopy) client.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
protected override void ThreadedClientLateUpdate() => client.TickOutgoing();
|
||||
|
||||
// server thread overrides
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
builder.Port = Port;
|
||||
return builder.Uri;
|
||||
}
|
||||
protected override void ThreadedServerStart() => server.Start(Port);
|
||||
protected override void ThreadedServerSend(int connectionId, ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
server.Send(connectionId, segment, KcpTransport.ToKcpChannel(channelId));
|
||||
|
||||
// thread safe version for statistics
|
||||
OnThreadedServerSend(connectionId, segment, channelId);
|
||||
}
|
||||
protected override void ThreadedServerDisconnect(int connectionId) => server.Disconnect(connectionId);
|
||||
/* NOT THREAD SAFE. ThreadedTransport version throws NotImplementedException for this.
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
IPEndPoint endPoint = server.GetClientEndPoint(connectionId);
|
||||
return endPoint != null
|
||||
// Map to IPv4 if "IsIPv4MappedToIPv6"
|
||||
// "::ffff:127.0.0.1" -> "127.0.0.1"
|
||||
? (endPoint.Address.IsIPv4MappedToIPv6
|
||||
? endPoint.Address.MapToIPv4().ToString()
|
||||
: endPoint.Address.ToString())
|
||||
: "";
|
||||
}
|
||||
*/
|
||||
protected override void ThreadedServerStop() => server.Stop();
|
||||
protected override void ThreadedServerEarlyUpdate()
|
||||
{
|
||||
// only process messages while transport is enabled.
|
||||
// scene change messsages disable it to stop processing.
|
||||
// (see also: https://github.com/vis2k/Mirror/pull/379)
|
||||
// => enabledCopy for thread safe use
|
||||
if (enabledCopy) server.TickIncoming();
|
||||
}
|
||||
// process outgoing in late update
|
||||
protected override void ThreadedServerLateUpdate() => server.TickOutgoing();
|
||||
|
||||
protected override void ThreadedShutdown() {}
|
||||
|
||||
// max message size
|
||||
public override int GetMaxPacketSize(int channelId = Channels.Reliable)
|
||||
{
|
||||
// switch to kcp channel.
|
||||
// unreliable or reliable.
|
||||
// default to reliable just to be sure.
|
||||
switch (channelId)
|
||||
{
|
||||
case Channels.Unreliable:
|
||||
return KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
default:
|
||||
return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize);
|
||||
}
|
||||
}
|
||||
|
||||
// kcp reliable channel max packet size is MTU * WND_RCV
|
||||
// this allows 144kb messages. but due to head of line blocking, all
|
||||
// other messages would have to wait until the maxed size one is
|
||||
// delivered. batching 144kb messages each time would be EXTREMELY slow
|
||||
// and fill the send queue nearly immediately when using it over the
|
||||
// network.
|
||||
// => instead we always use MTU sized batches.
|
||||
// => people can still send maxed size if needed.
|
||||
public override int GetBatchThreshold(int channelId) =>
|
||||
KcpPeer.UnreliableMaxMessageSize(config.Mtu);
|
||||
|
||||
protected virtual void OnGUIStatistics()
|
||||
{
|
||||
// TODO not thread safe
|
||||
/*
|
||||
GUILayout.BeginArea(new Rect(5, 110, 300, 300));
|
||||
|
||||
if (ServerActive())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("SERVER");
|
||||
GUILayout.Label($" connections: {server.connections.Count}");
|
||||
GUILayout.Label($" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s");
|
||||
GUILayout.Label($" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s");
|
||||
GUILayout.Label($" SendQueue: {GetTotalSendQueue()}");
|
||||
GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}");
|
||||
GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}");
|
||||
GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("CLIENT");
|
||||
GUILayout.Label($" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s");
|
||||
GUILayout.Label($" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s");
|
||||
GUILayout.Label($" SendQueue: {client.peer.SendQueueCount}");
|
||||
GUILayout.Label($" ReceiveQueue: {client.peer.ReceiveQueueCount}");
|
||||
GUILayout.Label($" SendBuffer: {client.peer.SendBufferCount}");
|
||||
GUILayout.Label($" ReceiveBuffer: {client.peer.ReceiveBufferCount}");
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
*/
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (statisticsGUI) OnGUIStatistics();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected virtual void OnLogStatistics()
|
||||
{
|
||||
// TODO not thread safe
|
||||
/*
|
||||
if (ServerActive())
|
||||
{
|
||||
string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" connections: {server.connections.Count}\n";
|
||||
log += $" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s\n";
|
||||
log += $" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s\n";
|
||||
log += $" SendQueue: {GetTotalSendQueue()}\n";
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
{
|
||||
string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n";
|
||||
log += $" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s\n";
|
||||
log += $" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s\n";
|
||||
log += $" SendQueue: {client.peer.SendQueueCount}\n";
|
||||
log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.peer.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n";
|
||||
Log.Info(log);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public override string ToString() => $"ThreadedKCP {port}";
|
||||
}
|
||||
}
|
||||
//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet
|
18
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta
Normal file
18
Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7e416e0486524f0d9580be7e13388f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs
|
||||
uploadId: 736421
|
8
Assets/Mirror/Transports/KCP/kcp2k.meta
Normal file
8
Assets/Mirror/Transports/KCP/kcp2k.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a1c8e8c022d4731a481c1808f37e5d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
16
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef
Normal file
16
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "kcp2k",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
14
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta
Normal file
14
Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta
Normal file
@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6806a62c384838046a3c66c44f06d75f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef
|
||||
uploadId: 736421
|
24
Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt
Normal file
24
Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt
Normal file
@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 limpo1989
|
||||
Copyright (c) 2020 Paul Pacheco
|
||||
Copyright (c) 2020 Lymdun
|
||||
Copyright (c) 2020 vis2k
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user