commit e32164902147dd29984b30bf17d83d88c00a008b Author: James Marcil Date: Mon Oct 17 13:50:41 2022 -0400 Initial commit to the repository. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a6228cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,53 @@ +# 3D models +*.3dm filter=lfs diff=lfs merge=lfs -text +*.3ds filter=lfs diff=lfs merge=lfs -text +*.blend filter=lfs diff=lfs merge=lfs -text +*.c4d filter=lfs diff=lfs merge=lfs -text +*.collada filter=lfs diff=lfs merge=lfs -text +*.dae filter=lfs diff=lfs merge=lfs -text +*.dxf filter=lfs diff=lfs merge=lfs -text +*.fbx filter=lfs diff=lfs merge=lfs -text +*.jas filter=lfs diff=lfs merge=lfs -text +*.lws filter=lfs diff=lfs merge=lfs -text +*.lxo filter=lfs diff=lfs merge=lfs -text +*.ma filter=lfs diff=lfs merge=lfs -text +*.max filter=lfs diff=lfs merge=lfs -text +*.mb filter=lfs diff=lfs merge=lfs -text +*.obj filter=lfs diff=lfs merge=lfs -text +*.ply filter=lfs diff=lfs merge=lfs -text +*.skp filter=lfs diff=lfs merge=lfs -text +*.stl filter=lfs diff=lfs merge=lfs -text +*.ztl filter=lfs diff=lfs merge=lfs -text +# Audio +*.aif filter=lfs diff=lfs merge=lfs -text +*.aiff filter=lfs diff=lfs merge=lfs -text +*.it filter=lfs diff=lfs merge=lfs -text +*.mod filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text +*.s3m filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.xm filter=lfs diff=lfs merge=lfs -text +# Fonts +*.otf filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +# Images +*.bmp filter=lfs diff=lfs merge=lfs -text +*.exr filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text +*.iff filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.pict filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.tga filter=lfs diff=lfs merge=lfs -text +*.tif filter=lfs diff=lfs merge=lfs -text +*.tiff filter=lfs diff=lfs merge=lfs -text +# Collapse Unity-generated files on GitHub +*.asset linguist-generated +*.mat linguist-generated +*.meta linguist-generated +*.prefab linguist-generated +*.unity linguist-generated \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58cbc82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ + +# MemoryCaptures can get excessive in size. +# They also could contain extremely sensitive data +/[Mm]emoryCaptures/ + +# Recordings can get excessive in size +/[Rr]ecordings/ + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.aab +*.unitypackage +*.app + +# Crashlytics generated file +crashlytics-build.properties + +# Packed Addressables +/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* + +# Temporary auto-generated Android Assets +/[Aa]ssets/[Ss]treamingAssets/aa.meta +/[Aa]ssets/[Ss]treamingAssets/aa/* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2f138cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,58 @@ +{ + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.gitignore": true, + "**/.gitmodules": true, + "**/*.booproj": true, + "**/*.pidb": true, + "**/*.suo": true, + "**/*.user": true, + "**/*.userprefs": true, + "**/*.unityproj": true, + "**/*.dll": true, + "**/*.exe": true, + "**/*.pdf": true, + "**/*.mid": true, + "**/*.midi": true, + "**/*.wav": true, + "**/*.gif": true, + "**/*.ico": true, + "**/*.jpg": true, + "**/*.jpeg": true, + "**/*.png": true, + "**/*.psd": true, + "**/*.tga": true, + "**/*.tif": true, + "**/*.tiff": true, + "**/*.3ds": true, + "**/*.3DS": true, + "**/*.fbx": true, + "**/*.FBX": true, + "**/*.lxo": true, + "**/*.LXO": true, + "**/*.ma": true, + "**/*.MA": true, + "**/*.obj": true, + "**/*.OBJ": true, + "**/*.asset": true, + "**/*.cubemap": true, + "**/*.flare": true, + "**/*.mat": true, + "**/*.meta": true, + "**/*.prefab": true, + "**/*.unity": true, + "build/": true, + "Build/": true, + "Library/": true, + "library/": true, + "obj/": true, + "Obj/": true, + "ProjectSettings/": true, + "temp/": true, + "Temp/": true, + "**/*.csproj": true + }, + "git.ignoreLimitWarning": true, + "esbonio.sphinx.confDir": "" +} diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..1f4894b --- /dev/null +++ b/.vsconfig @@ -0,0 +1,6 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Workload.ManagedGame" + ] +} diff --git a/Assets/Materials.meta b/Assets/Materials.meta new file mode 100644 index 0000000..dac6a3c --- /dev/null +++ b/Assets/Materials.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b1424152fd9fc348b8e33878d6e6234 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/Player.mat b/Assets/Materials/Player.mat new file mode 100644 index 0000000..3670205 --- /dev/null +++ b/Assets/Materials/Player.mat @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Player + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.2783019, g: 1, b: 0.3892091, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Materials/Player.mat.meta b/Assets/Materials/Player.mat.meta new file mode 100644 index 0000000..d998067 --- /dev/null +++ b/Assets/Materials/Player.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: be9e482d028e9234da250ae990729af7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror.meta b/Assets/Mirror.meta new file mode 100644 index 0000000..a7a3dd0 --- /dev/null +++ b/Assets/Mirror.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5cf8eb36be0834b3da408c694a41cb88 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators.meta b/Assets/Mirror/Authenticators.meta new file mode 100644 index 0000000..644f4ec --- /dev/null +++ b/Assets/Mirror/Authenticators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b2f9d254154cd942ba40b06b869b8f3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs new file mode 100644 index 0000000..0fdc7e2 --- /dev/null +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror.Authenticators +{ + [AddComponentMenu("Network/ Authenticators/Basic Authenticator")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/basic-authenticator")] + public class BasicAuthenticator : NetworkAuthenticator + { + [Header("Server Credentials")] + public string serverUsername; + public string serverPassword; + + [Header("Client Credentials")] + public string username; + public string password; + + readonly HashSet connectionsPendingDisconnect = new HashSet(); + + #region Messages + + public struct AuthRequestMessage : NetworkMessage + { + // use whatever credentials make sense for your game + // for example, you might want to pass the accessToken if using oauth + public string authUsername; + public string authPassword; + } + + public struct AuthResponseMessage : NetworkMessage + { + public byte code; + public string message; + } + + #endregion + + #region Server + + /// + /// Called on server from StartServer to initialize the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStartServer() + { + // register a handler for the authentication request we expect from client + NetworkServer.RegisterHandler(OnAuthRequestMessage, false); + } + + /// + /// Called on server from StopServer to reset the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStopServer() + { + // unregister the handler for the authentication request + NetworkServer.UnregisterHandler(); + } + + /// + /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// + /// Connection to client. + public override void OnServerAuthenticate(NetworkConnectionToClient conn) + { + // do nothing...wait for AuthRequestMessage from client + } + + /// + /// Called on server when the client's AuthRequestMessage arrives + /// + /// Connection to client. + /// The message payload + public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg) + { + //Debug.Log($"Authentication Request: {msg.authUsername} {msg.authPassword}"); + + if (connectionsPendingDisconnect.Contains(conn)) return; + + // check the credentials by calling your web server, database table, playfab api, or any method appropriate. + if (msg.authUsername == serverUsername && msg.authPassword == serverPassword) + { + // create and send msg to client so it knows to proceed + AuthResponseMessage authResponseMessage = new AuthResponseMessage + { + code = 100, + message = "Success" + }; + + conn.Send(authResponseMessage); + + // Accept the successful authentication + ServerAccept(conn); + } + else + { + connectionsPendingDisconnect.Add(conn); + + // create and send msg to client so it knows to disconnect + AuthResponseMessage authResponseMessage = new AuthResponseMessage + { + code = 200, + message = "Invalid Credentials" + }; + + conn.Send(authResponseMessage); + + // must set NetworkConnection isAuthenticated = false + conn.isAuthenticated = false; + + // disconnect the client after 1 second so that response message gets delivered + StartCoroutine(DelayedDisconnect(conn, 1f)); + } + } + + IEnumerator DelayedDisconnect(NetworkConnectionToClient conn, float waitTime) + { + yield return new WaitForSeconds(waitTime); + + // Reject the unsuccessful authentication + ServerReject(conn); + + yield return null; + + // remove conn from pending connections + connectionsPendingDisconnect.Remove(conn); + } + + #endregion + + #region Client + + /// + /// Called on client from StartClient to initialize the Authenticator + /// Client message handlers should be registered in this method. + /// + public override void OnStartClient() + { + // register a handler for the authentication response we expect from server + NetworkClient.RegisterHandler(OnAuthResponseMessage, false); + } + + /// + /// Called on client from StopClient to reset the Authenticator + /// Client message handlers should be unregistered in this method. + /// + public override void OnStopClient() + { + // unregister the handler for the authentication response + NetworkClient.UnregisterHandler(); + } + + /// + /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// + public override void OnClientAuthenticate() + { + AuthRequestMessage authRequestMessage = new AuthRequestMessage + { + authUsername = username, + authPassword = password + }; + + NetworkClient.connection.Send(authRequestMessage); + } + + /// + /// Called on client when the server's AuthResponseMessage arrives + /// + /// The message payload + public void OnAuthResponseMessage(AuthResponseMessage msg) + { + if (msg.code == 100) + { + //Debug.Log($"Authentication Response: {msg.message}"); + + // Authentication has been accepted + ClientAccept(); + } + else + { + Debug.LogError($"Authentication Response: {msg.message}"); + + // Authentication has been rejected + ClientReject(); + } + } + + #endregion + } +} diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta new file mode 100644 index 0000000..4765013 --- /dev/null +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28496b776660156428f00cf78289c1ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs new file mode 100644 index 0000000..6723cc9 --- /dev/null +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs @@ -0,0 +1,129 @@ +using System; +using UnityEngine; + +namespace Mirror.Authenticators +{ + /// + /// An authenicator that identifies the user by their device. + /// A GUID is used as a fallback when the platform doesn't support SystemInfo.deviceUniqueIdentifier. + /// Note: deviceUniqueIdentifier can be spoofed, so security is not guaranteed. + /// See https://docs.unity3d.com/ScriptReference/SystemInfo-deviceUniqueIdentifier.html for details. + /// + [AddComponentMenu("Network/ Authenticators/Device Authenticator")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/device-authenticator")] + public class DeviceAuthenticator : NetworkAuthenticator + { + #region Messages + + public struct AuthRequestMessage : NetworkMessage + { + public string clientDeviceID; + } + + public struct AuthResponseMessage : NetworkMessage { } + + #endregion + + #region Server + + /// + /// Called on server from StartServer to initialize the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStartServer() + { + // register a handler for the authentication request we expect from client + NetworkServer.RegisterHandler(OnAuthRequestMessage, false); + } + + /// + /// Called on server from StopServer to reset the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStopServer() + { + // unregister the handler for the authentication request + NetworkServer.UnregisterHandler(); + } + + /// + /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// + /// Connection to client. + public override void OnServerAuthenticate(NetworkConnectionToClient conn) + { + // do nothing, wait for client to send his id + } + + void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg) + { + Debug.Log($"connection {conn.connectionId} authenticated with id {msg.clientDeviceID}"); + + // Store the device id for later reference, e.g. when spawning the player + conn.authenticationData = msg.clientDeviceID; + + // Send a response to client telling it to proceed as authenticated + conn.Send(new AuthResponseMessage()); + + // Accept the successful authentication + ServerAccept(conn); + } + + #endregion + + #region Client + + /// + /// Called on client from StartClient to initialize the Authenticator + /// Client message handlers should be registered in this method. + /// + public override void OnStartClient() + { + // register a handler for the authentication response we expect from server + NetworkClient.RegisterHandler(OnAuthResponseMessage, false); + } + + /// + /// Called on client from StopClient to reset the Authenticator + /// Client message handlers should be unregistered in this method. + /// + public override void OnStopClient() + { + // unregister the handler for the authentication response + NetworkClient.UnregisterHandler(); + } + + /// + /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// + public override void OnClientAuthenticate() + { + string deviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier; + + // Not all platforms support this, so we use a GUID instead + if (deviceUniqueIdentifier == SystemInfo.unsupportedIdentifier) + { + // Get the value from PlayerPrefs if it exists, new GUID if it doesn't + deviceUniqueIdentifier = PlayerPrefs.GetString("deviceUniqueIdentifier", Guid.NewGuid().ToString()); + + // Store the deviceUniqueIdentifier to PlayerPrefs (in case we just made a new GUID) + PlayerPrefs.SetString("deviceUniqueIdentifier", deviceUniqueIdentifier); + } + + // send the deviceUniqueIdentifier to the server + NetworkClient.connection.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); + } + + /// + /// Called on client when the server's AuthResponseMessage arrives + /// + /// The message payload + public void OnAuthResponseMessage(AuthResponseMessage msg) + { + Debug.Log("Authentication Success"); + ClientAccept(); + } + + #endregion + } +} diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta new file mode 100644 index 0000000..9ca9f64 --- /dev/null +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60960a6ba81a842deb2fdcdc93788242 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef new file mode 100644 index 0000000..16cdfbc --- /dev/null +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.Authenticators", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta new file mode 100644 index 0000000..2731701 --- /dev/null +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e720aa64e3f58fb4880566a322584340 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs new file mode 100644 index 0000000..711ee6e --- /dev/null +++ b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs @@ -0,0 +1,70 @@ +using System.Collections; +using UnityEngine; + +namespace Mirror.Authenticators +{ + /// + /// An authenticator that disconnects connections if they don't + /// authenticate within a specified time limit. + /// + [AddComponentMenu("Network/ Authenticators/Timeout Authenticator")] + public class TimeoutAuthenticator : NetworkAuthenticator + { + public NetworkAuthenticator authenticator; + + [Range(0, 600), Tooltip("Timeout to auto-disconnect in seconds. Set to 0 for no timeout.")] + public float timeout = 60; + + public void Awake() + { + authenticator.OnServerAuthenticated.AddListener(connection => OnServerAuthenticated.Invoke(connection)); + authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated.Invoke); + } + + public override void OnStartServer() + { + authenticator.OnStartServer(); + } + + public override void OnStopServer() + { + authenticator.OnStopServer(); + } + + public override void OnStartClient() + { + authenticator.OnStartClient(); + } + + public override void OnStopClient() + { + authenticator.OnStopClient(); + } + + public override void OnServerAuthenticate(NetworkConnectionToClient conn) + { + authenticator.OnServerAuthenticate(conn); + if (timeout > 0) + StartCoroutine(BeginAuthentication(conn)); + } + + public override void OnClientAuthenticate() + { + authenticator.OnClientAuthenticate(); + if (timeout > 0) + StartCoroutine(BeginAuthentication(NetworkClient.connection)); + } + + IEnumerator BeginAuthentication(NetworkConnection conn) + { + //Debug.Log($"Authentication countdown started {conn} {timeout}"); + yield return new WaitForSecondsRealtime(timeout); + + if (!conn.isAuthenticated) + { + Debug.LogError($"Authentication Timeout - Disconnecting {conn}"); + conn.Disconnect(); + } + } + } +} diff --git a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta new file mode 100644 index 0000000..b19ddec --- /dev/null +++ b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24d8269a07b8e4edfa374753a91c946e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/CompilerSymbols.meta b/Assets/Mirror/CompilerSymbols.meta new file mode 100644 index 0000000..4652ae1 --- /dev/null +++ b/Assets/Mirror/CompilerSymbols.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f8b918bcd89f5c488b06f5574f34760 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef new file mode 100644 index 0000000..af25622 --- /dev/null +++ b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.CompilerSymbols", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta new file mode 100644 index 0000000..8b23823 --- /dev/null +++ b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 325984b52e4128546bc7558552f8b1d2 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs new file mode 100644 index 0000000..89867c1 --- /dev/null +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using UnityEditor; + +namespace Mirror +{ + static class PreprocessorDefine + { + /// + /// Add define symbols as soon as Unity gets done compiling. + /// + [InitializeOnLoadMethod] + public static void AddDefineSymbols() + { + string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup); + HashSet defines = new HashSet(currentDefines.Split(';')) + { + "MIRROR", + "MIRROR_17_0_OR_NEWER", + "MIRROR_18_0_OR_NEWER", + "MIRROR_24_0_OR_NEWER", + "MIRROR_26_0_OR_NEWER", + "MIRROR_27_0_OR_NEWER", + "MIRROR_28_0_OR_NEWER", + "MIRROR_29_0_OR_NEWER", + "MIRROR_30_0_OR_NEWER", + "MIRROR_30_5_2_OR_NEWER", + "MIRROR_32_1_2_OR_NEWER", + "MIRROR_32_1_4_OR_NEWER", + "MIRROR_35_0_OR_NEWER", + "MIRROR_35_1_OR_NEWER", + "MIRROR_37_0_OR_NEWER", + "MIRROR_38_0_OR_NEWER", + "MIRROR_39_0_OR_NEWER", + "MIRROR_40_0_OR_NEWER", + "MIRROR_41_0_OR_NEWER", + "MIRROR_42_0_OR_NEWER", + "MIRROR_43_0_OR_NEWER", + "MIRROR_44_0_OR_NEWER", + "MIRROR_46_0_OR_NEWER", + "MIRROR_47_0_OR_NEWER", + "MIRROR_53_0_OR_NEWER", + "MIRROR_55_0_OR_NEWER", + "MIRROR_57_0_OR_NEWER", + "MIRROR_58_0_OR_NEWER", + "MIRROR_65_0_OR_NEWER", + "MIRROR_66_0_OR_NEWER" + }; + + // only touch PlayerSettings if we actually modified it. + // otherwise it shows up as changed in git each time. + string newDefines = string.Join(";", defines); + if (newDefines != currentDefines) + { + PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines); + } + } + } +} diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta new file mode 100644 index 0000000..30806d0 --- /dev/null +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1d66fe74ec6f42dd974cba37d25d453 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components.meta b/Assets/Mirror/Components.meta new file mode 100644 index 0000000..c2771d9 --- /dev/null +++ b/Assets/Mirror/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9bee879fbc8ef4b1a9a9f7088bfbf726 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/AssemblyInfo.cs b/Assets/Mirror/Components/AssemblyInfo.cs new file mode 100644 index 0000000..f342716 --- /dev/null +++ b/Assets/Mirror/Components/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mirror.Tests.Common")] +[assembly: InternalsVisibleTo("Mirror.Tests")] +// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline +// for ILPostProcessor tests. +[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")] +[assembly: InternalsVisibleTo("Mirror.Tests.Generated")] +[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")] +[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")] +[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")] +[assembly: InternalsVisibleTo("Mirror.Editor")] diff --git a/Assets/Mirror/Components/AssemblyInfo.cs.meta b/Assets/Mirror/Components/AssemblyInfo.cs.meta new file mode 100644 index 0000000..f9af1fa --- /dev/null +++ b/Assets/Mirror/Components/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a65b9283f7a724e70b8e17cb277f4c1e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery.meta b/Assets/Mirror/Components/Discovery.meta new file mode 100644 index 0000000..d5bb0cb --- /dev/null +++ b/Assets/Mirror/Components/Discovery.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5dcf9618f5e14a4eb60bff5480284a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs new file mode 100644 index 0000000..ddb38ea --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs @@ -0,0 +1,114 @@ +using System; +using System.Net; +using UnityEngine; +using UnityEngine.Events; + +namespace Mirror.Discovery +{ + [Serializable] + public class ServerFoundUnityEvent : UnityEvent {}; + + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Discovery")] + public class NetworkDiscovery : NetworkDiscoveryBase + { + #region Server + + public long ServerId { get; private set; } + + [Tooltip("Transport to be advertised during discovery")] + public Transport transport; + + [Tooltip("Invoked when a server is found")] + public ServerFoundUnityEvent OnServerFound; + + public override void Start() + { + ServerId = RandomLong(); + + // active transport gets initialized in awake + // so make sure we set it here in Start() (after awakes) + // Or just let the user assign it in the inspector + if (transport == null) + transport = Transport.activeTransport; + + base.Start(); + } + + /// + /// Process the request from a client + /// + /// + /// Override if you wish to provide more information to the clients + /// such as the name of the host player + /// + /// Request coming from client + /// Address of the client that sent the request + /// The message to be sent back to the client or null + protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint) + { + // In this case we don't do anything with the request + // but other discovery implementations might want to use the data + // in there, This way the client can ask for + // specific game mode or something + + try + { + // this is an example reply message, return your own + // to include whatever is relevant for your game + return new ServerResponse + { + serverId = ServerId, + uri = transport.ServerUri() + }; + } + catch (NotImplementedException) + { + Debug.LogError($"Transport {transport} does not support network discovery"); + throw; + } + } + + #endregion + + #region Client + + /// + /// Create a message that will be broadcasted on the network to discover servers + /// + /// + /// Override if you wish to include additional data in the discovery message + /// such as desired game mode, language, difficulty, etc... + /// An instance of ServerRequest with data to be broadcasted + protected override ServerRequest GetRequest() => new ServerRequest(); + + /// + /// Process the answer from a server + /// + /// + /// A client receives a reply from a server, this method processes the + /// reply and raises an event + /// + /// Response that came from the server + /// Address of the server that replied + protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint) + { + // we received a message from the remote endpoint + response.EndPoint = endpoint; + + // although we got a supposedly valid url, we may not be able to resolve + // the provided host + // However we know the real ip address of the server because we just + // received a packet from it, so use that as host. + UriBuilder realUri = new UriBuilder(response.uri) + { + Host = response.EndPoint.Address.ToString() + }; + response.uri = realUri.Uri; + + OnServerFound.Invoke(response); + } + + #endregion + } +} diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta new file mode 100644 index 0000000..c691a61 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c761308e733c51245b2e8bb4201f46dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs new file mode 100644 index 0000000..ac57b75 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs @@ -0,0 +1,433 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using UnityEngine; + +// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery +// forked from https://github.com/in0finite/MirrorNetworkDiscovery +// Both are MIT Licensed + +namespace Mirror.Discovery +{ + /// + /// Base implementation for Network Discovery. Extend this component + /// to provide custom discovery with game specific data + /// NetworkDiscovery for a sample implementation + /// + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")] + public abstract class NetworkDiscoveryBase : MonoBehaviour + where Request : NetworkMessage + where Response : NetworkMessage + { + public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } } + + // each game should have a random unique handshake, this way you can tell if this is the same game or not + [HideInInspector] + public long secretHandshake; + + [SerializeField] + [Tooltip("The UDP port the server will listen for multi-cast messages")] + protected int serverBroadcastListenPort = 47777; + + [SerializeField] + [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")] + public bool enableActiveDiscovery = true; + + [SerializeField] + [Tooltip("Time in seconds between multi-cast messages")] + [Range(1, 60)] + float ActiveDiscoveryInterval = 3; + + protected UdpClient serverUdpClient; + protected UdpClient clientUdpClient; + +#if UNITY_EDITOR + void OnValidate() + { + if (secretHandshake == 0) + { + secretHandshake = RandomLong(); + UnityEditor.Undo.RecordObject(this, "Set secret handshake"); + } + } +#endif + + public static long RandomLong() + { + int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); + int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); + return value1 + ((long)value2 << 32); + } + + /// + /// virtual so that inheriting classes' Start() can call base.Start() too + /// + public virtual void Start() + { + // Server mode? then start advertising +#if UNITY_SERVER + AdvertiseServer(); +#endif + } + + // Ensure the ports are cleared no matter when Game/Unity UI exits + void OnApplicationQuit() + { + //Debug.Log("NetworkDiscoveryBase OnApplicationQuit"); + Shutdown(); + } + + void OnDisable() + { + //Debug.Log("NetworkDiscoveryBase OnDisable"); + Shutdown(); + } + + void OnDestroy() + { + //Debug.Log("NetworkDiscoveryBase OnDestroy"); + Shutdown(); + } + + void Shutdown() + { + EndpMulticastLock(); + if (serverUdpClient != null) + { + try + { + serverUdpClient.Close(); + } + catch (Exception) + { + // it is just close, swallow the error + } + + serverUdpClient = null; + } + + if (clientUdpClient != null) + { + try + { + clientUdpClient.Close(); + } + catch (Exception) + { + // it is just close, swallow the error + } + + clientUdpClient = null; + } + + CancelInvoke(); + } + + #region Server + + /// + /// Advertise this server in the local network + /// + public void AdvertiseServer() + { + if (!SupportedOnThisPlatform) + throw new PlatformNotSupportedException("Network discovery not supported in this platform"); + + StopDiscovery(); + + // Setup port -- may throw exception + serverUdpClient = new UdpClient(serverBroadcastListenPort) + { + EnableBroadcast = true, + MulticastLoopback = false + }; + + // listen for client pings + _ = ServerListenAsync(); + } + + public async Task ServerListenAsync() + { + BeginMulticastLock(); + while (true) + { + try + { + await ReceiveRequestAsync(serverUdpClient); + } + catch (ObjectDisposedException) + { + // socket has been closed + break; + } + catch (Exception) + { + } + } + } + + async Task ReceiveRequestAsync(UdpClient udpClient) + { + // only proceed if there is available data in network buffer, or otherwise Receive() will block + // average time for UdpClient.Available : 10 us + + UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); + + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer)) + { + long handshake = networkReader.ReadLong(); + if (handshake != secretHandshake) + { + // message is not for us + throw new ProtocolViolationException("Invalid handshake"); + } + + Request request = networkReader.Read(); + + ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint); + } + } + + /// + /// Reply to the client to inform it of this server + /// + /// + /// Override if you wish to ignore server requests based on + /// custom criteria such as language, full server game mode or difficulty + /// + /// Request coming from client + /// Address of the client that sent the request + protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint) + { + Response info = ProcessRequest(request, endpoint); + + if (info == null) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + try + { + writer.WriteLong(secretHandshake); + + writer.Write(info); + + ArraySegment data = writer.ToArraySegment(); + // signature matches + // send response + serverUdpClient.Send(data.Array, data.Count, endpoint); + } + catch (Exception ex) + { + Debug.LogException(ex, this); + } + } + } + + /// + /// Process the request from a client + /// + /// + /// Override if you wish to provide more information to the clients + /// such as the name of the host player + /// + /// Request coming from client + /// Address of the client that sent the request + /// The message to be sent back to the client or null + protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint); + + // Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887 +#if UNITY_ANDROID + AndroidJavaObject multicastLock; + bool hasMulticastLock; +#endif + void BeginMulticastLock() + { +#if UNITY_ANDROID + if (hasMulticastLock) return; + + if (Application.platform == RuntimePlatform.Android) + { + using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic("currentActivity")) + { + using (var wifiManager = activity.Call("getSystemService", "wifi")) + { + multicastLock = wifiManager.Call("createMulticastLock", "lock"); + multicastLock.Call("acquire"); + hasMulticastLock = true; + } + } + } +#endif + } + + void EndpMulticastLock() + { +#if UNITY_ANDROID + if (!hasMulticastLock) return; + + multicastLock?.Call("release"); + hasMulticastLock = false; +#endif + } + +#endregion + + #region Client + + /// + /// Start Active Discovery + /// + public void StartDiscovery() + { + if (!SupportedOnThisPlatform) + throw new PlatformNotSupportedException("Network discovery not supported in this platform"); + + StopDiscovery(); + + try + { + // Setup port + clientUdpClient = new UdpClient(0) + { + EnableBroadcast = true, + MulticastLoopback = false + }; + } + catch (Exception) + { + // Free the port if we took it + //Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception"); + Shutdown(); + throw; + } + + _ = ClientListenAsync(); + + if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval); + } + + /// + /// Stop Active Discovery + /// + public void StopDiscovery() + { + //Debug.Log("NetworkDiscoveryBase StopDiscovery"); + Shutdown(); + } + + /// + /// Awaits for server response + /// + /// ClientListenAsync Task + public async Task ClientListenAsync() + { + // while clientUpdClient to fix: + // https://github.com/vis2k/Mirror/pull/2908 + // + // If, you cancel discovery the clientUdpClient is set to null. + // However, nothing cancels ClientListenAsync. If we change the if(true) + // to check if the client is null. You can properly cancel the discovery, + // and kill the listen thread. + // + // Prior to this fix, if you cancel the discovery search. It crashes the + // thread, and is super noisy in the output. As well as causes issues on + // the quest. + while (clientUdpClient != null) + { + try + { + await ReceiveGameBroadcastAsync(clientUdpClient); + } + catch (ObjectDisposedException) + { + // socket was closed, no problem + return; + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + } + + /// + /// Sends discovery request from client + /// + public void BroadcastDiscoveryRequest() + { + if (clientUdpClient == null) + return; + + if (NetworkClient.isConnected) + { + StopDiscovery(); + return; + } + + IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort); + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteLong(secretHandshake); + + try + { + Request request = GetRequest(); + + writer.Write(request); + + ArraySegment data = writer.ToArraySegment(); + + clientUdpClient.SendAsync(data.Array, data.Count, endPoint); + } + catch (Exception) + { + // It is ok if we can't broadcast to one of the addresses + } + } + } + + /// + /// Create a message that will be broadcasted on the network to discover servers + /// + /// + /// Override if you wish to include additional data in the discovery message + /// such as desired game mode, language, difficulty, etc... + /// An instance of ServerRequest with data to be broadcasted + protected virtual Request GetRequest() => default; + + async Task ReceiveGameBroadcastAsync(UdpClient udpClient) + { + // only proceed if there is available data in network buffer, or otherwise Receive() will block + // average time for UdpClient.Available : 10 us + + UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); + + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer)) + { + if (networkReader.ReadLong() != secretHandshake) + return; + + Response response = networkReader.Read(); + + ProcessResponse(response, udpReceiveResult.RemoteEndPoint); + } + } + + /// + /// Process the answer from a server + /// + /// + /// A client receives a reply from a server, this method processes the + /// reply and raises an event + /// + /// Response that came from the server + /// Address of the server that replied + protected abstract void ProcessResponse(Response response, IPEndPoint endpoint); + + #endregion + } +} diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta new file mode 100644 index 0000000..7dfbaf6 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b9971d60ce61f4e39b07cd9e7e0c68fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs new file mode 100644 index 0000000..e43c3d7 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror.Discovery +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Discovery HUD")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")] + [RequireComponent(typeof(NetworkDiscovery))] + public class NetworkDiscoveryHUD : MonoBehaviour + { + readonly Dictionary discoveredServers = new Dictionary(); + Vector2 scrollViewPos = Vector2.zero; + + public NetworkDiscovery networkDiscovery; + +#if UNITY_EDITOR + void OnValidate() + { + if (networkDiscovery == null) + { + networkDiscovery = GetComponent(); + UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer); + UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery"); + } + } +#endif + + void OnGUI() + { + if (NetworkManager.singleton == null) + return; + + if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active) + DrawGUI(); + + if (NetworkServer.active || NetworkClient.active) + StopButtons(); + } + + void DrawGUI() + { + GUILayout.BeginArea(new Rect(10, 10, 300, 500)); + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Find Servers")) + { + discoveredServers.Clear(); + networkDiscovery.StartDiscovery(); + } + + // LAN Host + if (GUILayout.Button("Start Host")) + { + discoveredServers.Clear(); + NetworkManager.singleton.StartHost(); + networkDiscovery.AdvertiseServer(); + } + + // Dedicated server + if (GUILayout.Button("Start Server")) + { + discoveredServers.Clear(); + NetworkManager.singleton.StartServer(); + networkDiscovery.AdvertiseServer(); + } + + GUILayout.EndHorizontal(); + + // show list of found server + + GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:"); + + // servers + scrollViewPos = GUILayout.BeginScrollView(scrollViewPos); + + foreach (ServerResponse info in discoveredServers.Values) + if (GUILayout.Button(info.EndPoint.Address.ToString())) + Connect(info); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + void StopButtons() + { + GUILayout.BeginArea(new Rect(10, 40, 100, 25)); + + // stop host if host mode + if (NetworkServer.active && NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Host")) + { + NetworkManager.singleton.StopHost(); + networkDiscovery.StopDiscovery(); + } + } + // stop client if client-only + else if (NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Client")) + { + NetworkManager.singleton.StopClient(); + networkDiscovery.StopDiscovery(); + } + } + // stop server if server-only + else if (NetworkServer.active) + { + if (GUILayout.Button("Stop Server")) + { + NetworkManager.singleton.StopServer(); + networkDiscovery.StopDiscovery(); + } + } + + GUILayout.EndArea(); + } + + void Connect(ServerResponse info) + { + networkDiscovery.StopDiscovery(); + NetworkManager.singleton.StartClient(info.uri); + } + + public void OnDiscoveredServer(ServerResponse info) + { + // Note that you can check the versioning to decide if you can connect to the server or not using this method + discoveredServers[info.serverId] = info; + } + } +} diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta new file mode 100644 index 0000000..f93b275 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88c37d3deca7a834d80cfd8d3cfcc510 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/ServerRequest.cs b/Assets/Mirror/Components/Discovery/ServerRequest.cs new file mode 100644 index 0000000..647b619 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/ServerRequest.cs @@ -0,0 +1,4 @@ +namespace Mirror.Discovery +{ + public struct ServerRequest : NetworkMessage {} +} diff --git a/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta new file mode 100644 index 0000000..84f3232 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea7254bf7b9454da4adad881d94cd141 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Discovery/ServerResponse.cs b/Assets/Mirror/Components/Discovery/ServerResponse.cs new file mode 100644 index 0000000..7465783 --- /dev/null +++ b/Assets/Mirror/Components/Discovery/ServerResponse.cs @@ -0,0 +1,18 @@ +using System; +using System.Net; + +namespace Mirror.Discovery +{ + public struct ServerResponse : NetworkMessage + { + // The server that sent this + // this is a property so that it is not serialized, but the + // client fills this up after we receive it + public IPEndPoint EndPoint { get; set; } + + public Uri uri; + + // Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs + public long serverId; + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta new file mode 100644 index 0000000..44f23ba --- /dev/null +++ b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36f97227fdf2d7a4e902db5bfc43039c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Experimental.meta b/Assets/Mirror/Components/Experimental.meta new file mode 100644 index 0000000..57cce38 --- /dev/null +++ b/Assets/Mirror/Components/Experimental.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bfbf2a1f2b300c5489dcab219ef2846e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs new file mode 100644 index 0000000..06a87a9 --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs @@ -0,0 +1,93 @@ +using UnityEngine; + +namespace Mirror.Experimental +{ + [AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")] + public class NetworkLerpRigidbody : NetworkBehaviour + { + [Header("Settings")] + [SerializeField] internal Rigidbody target = null; + [Tooltip("How quickly current velocity approaches target velocity")] + [SerializeField] float lerpVelocityAmount = 0.5f; + [Tooltip("How quickly current position approaches target position")] + [SerializeField] float lerpPositionAmount = 0.5f; + + [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] + [SerializeField] bool clientAuthority = false; + + float nextSyncTime; + + + [SyncVar()] + Vector3 targetVelocity; + + [SyncVar()] + Vector3 targetPosition; + + /// + /// Ignore value if is host or client with Authority + /// + /// + bool IgnoreSync => isServer || ClientWithAuthority; + + bool ClientWithAuthority => clientAuthority && hasAuthority; + + void OnValidate() + { + if (target == null) + { + target = GetComponent(); + } + } + + void Update() + { + if (isServer) + { + SyncToClients(); + } + else if (ClientWithAuthority) + { + SendToServer(); + } + } + + void SyncToClients() + { + targetVelocity = target.velocity; + targetPosition = target.position; + } + + void SendToServer() + { + float now = Time.time; + if (now > nextSyncTime) + { + nextSyncTime = now + syncInterval; + CmdSendState(target.velocity, target.position); + } + } + + [Command] + void CmdSendState(Vector3 velocity, Vector3 position) + { + target.velocity = velocity; + target.position = position; + targetVelocity = velocity; + targetPosition = position; + } + + void FixedUpdate() + { + if (IgnoreSync) { return; } + + target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount); + target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount); + // add velocity to position as position would have moved on server at that velocity + target.position += target.velocity * Time.fixedDeltaTime; + + // TODO does this also need to sync acceleration so and update velocity? + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta new file mode 100644 index 0000000..35ef1fe --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f032128052c95a46afb0ddd97d994cc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs new file mode 100644 index 0000000..4989d29 --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs @@ -0,0 +1,361 @@ +using UnityEngine; + +namespace Mirror.Experimental +{ + [AddComponentMenu("Network/ Experimental/Network Rigidbody")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")] + public class NetworkRigidbody : NetworkBehaviour + { + [Header("Settings")] + [SerializeField] internal Rigidbody target = null; + + [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] + public bool clientAuthority = false; + + [Header("Velocity")] + + [Tooltip("Syncs Velocity every SyncInterval")] + [SerializeField] bool syncVelocity = true; + + [Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")] + [SerializeField] bool clearVelocity = false; + + [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] + [SerializeField] float velocitySensitivity = 0.1f; + + + [Header("Angular Velocity")] + + [Tooltip("Syncs AngularVelocity every SyncInterval")] + [SerializeField] bool syncAngularVelocity = true; + + [Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")] + [SerializeField] bool clearAngularVelocity = false; + + [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] + [SerializeField] float angularVelocitySensitivity = 0.1f; + + /// + /// Values sent on client with authority after they are sent to the server + /// + readonly ClientSyncState previousValue = new ClientSyncState(); + + void OnValidate() + { + if (target == null) + { + target = GetComponent(); + } + } + + + #region Sync vars + [SyncVar(hook = nameof(OnVelocityChanged))] + Vector3 velocity; + + [SyncVar(hook = nameof(OnAngularVelocityChanged))] + Vector3 angularVelocity; + + [SyncVar(hook = nameof(OnIsKinematicChanged))] + bool isKinematic; + + [SyncVar(hook = nameof(OnUseGravityChanged))] + bool useGravity; + + [SyncVar(hook = nameof(OnuDragChanged))] + float drag; + + [SyncVar(hook = nameof(OnAngularDragChanged))] + float angularDrag; + + /// + /// Ignore value if is host or client with Authority + /// + /// + bool IgnoreSync => isServer || ClientWithAuthority; + + bool ClientWithAuthority => clientAuthority && hasAuthority; + + void OnVelocityChanged(Vector3 _, Vector3 newValue) + { + if (IgnoreSync) + return; + + target.velocity = newValue; + } + + + void OnAngularVelocityChanged(Vector3 _, Vector3 newValue) + { + if (IgnoreSync) + return; + + target.angularVelocity = newValue; + } + + void OnIsKinematicChanged(bool _, bool newValue) + { + if (IgnoreSync) + return; + + target.isKinematic = newValue; + } + + void OnUseGravityChanged(bool _, bool newValue) + { + if (IgnoreSync) + return; + + target.useGravity = newValue; + } + + void OnuDragChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.drag = newValue; + } + + void OnAngularDragChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.angularDrag = newValue; + } + #endregion + + + internal void Update() + { + if (isServer) + { + SyncToClients(); + } + else if (ClientWithAuthority) + { + SendToServer(); + } + } + + internal void FixedUpdate() + { + if (clearAngularVelocity && !syncAngularVelocity) + { + target.angularVelocity = Vector3.zero; + } + + if (clearVelocity && !syncVelocity) + { + target.velocity = Vector3.zero; + } + } + + /// + /// Updates sync var values on server so that they sync to the client + /// + [Server] + void SyncToClients() + { + // only update if they have changed more than Sensitivity + + Vector3 currentVelocity = syncVelocity ? target.velocity : default; + Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; + + bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); + bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); + + if (velocityChanged) + { + velocity = currentVelocity; + previousValue.velocity = currentVelocity; + } + + if (angularVelocityChanged) + { + angularVelocity = currentAngularVelocity; + previousValue.angularVelocity = currentAngularVelocity; + } + + // other rigidbody settings + isKinematic = target.isKinematic; + useGravity = target.useGravity; + drag = target.drag; + angularDrag = target.angularDrag; + } + + /// + /// Uses Command to send values to server + /// + [Client] + void SendToServer() + { + if (!hasAuthority) + { + Debug.LogWarning("SendToServer called without authority"); + return; + } + + SendVelocity(); + SendRigidBodySettings(); + } + + [Client] + void SendVelocity() + { + float now = Time.time; + if (now < previousValue.nextSyncTime) + return; + + Vector3 currentVelocity = syncVelocity ? target.velocity : default; + Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; + + bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); + bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); + + // if angularVelocity has changed it is likely that velocity has also changed so just sync both values + // however if only velocity has changed just send velocity + if (angularVelocityChanged) + { + CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity); + previousValue.velocity = currentVelocity; + previousValue.angularVelocity = currentAngularVelocity; + } + else if (velocityChanged) + { + CmdSendVelocity(currentVelocity); + previousValue.velocity = currentVelocity; + } + + + // only update syncTime if either has changed + if (angularVelocityChanged || velocityChanged) + { + previousValue.nextSyncTime = now + syncInterval; + } + } + + [Client] + void SendRigidBodySettings() + { + // These shouldn't change often so it is ok to send in their own Command + if (previousValue.isKinematic != target.isKinematic) + { + CmdSendIsKinematic(target.isKinematic); + previousValue.isKinematic = target.isKinematic; + } + if (previousValue.useGravity != target.useGravity) + { + CmdSendUseGravity(target.useGravity); + previousValue.useGravity = target.useGravity; + } + if (previousValue.drag != target.drag) + { + CmdSendDrag(target.drag); + previousValue.drag = target.drag; + } + if (previousValue.angularDrag != target.angularDrag) + { + CmdSendAngularDrag(target.angularDrag); + previousValue.angularDrag = target.angularDrag; + } + } + + /// + /// Called when only Velocity has changed on the client + /// + [Command] + void CmdSendVelocity(Vector3 velocity) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.velocity = velocity; + target.velocity = velocity; + } + + /// + /// Called when angularVelocity has changed on the client + /// + [Command] + void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + if (syncVelocity) + { + this.velocity = velocity; + + target.velocity = velocity; + + } + this.angularVelocity = angularVelocity; + target.angularVelocity = angularVelocity; + } + + [Command] + void CmdSendIsKinematic(bool isKinematic) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.isKinematic = isKinematic; + target.isKinematic = isKinematic; + } + + [Command] + void CmdSendUseGravity(bool useGravity) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.useGravity = useGravity; + target.useGravity = useGravity; + } + + [Command] + void CmdSendDrag(float drag) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.drag = drag; + target.drag = drag; + } + + [Command] + void CmdSendAngularDrag(float angularDrag) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.angularDrag = angularDrag; + target.angularDrag = angularDrag; + } + + /// + /// holds previously synced values + /// + public class ClientSyncState + { + /// + /// Next sync time that velocity will be synced, based on syncInterval. + /// + public float nextSyncTime; + public Vector3 velocity; + public Vector3 angularVelocity; + public bool isKinematic; + public bool useGravity; + public float drag; + public float angularDrag; + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta new file mode 100644 index 0000000..1610f0a --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83392ae5c1b731446909f252fd494ae4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs new file mode 100644 index 0000000..c14b260 --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs @@ -0,0 +1,360 @@ +using UnityEngine; + +namespace Mirror.Experimental +{ + [AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")] + public class NetworkRigidbody2D : NetworkBehaviour + { + [Header("Settings")] + [SerializeField] internal Rigidbody2D target = null; + + [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] + public bool clientAuthority = false; + + [Header("Velocity")] + + [Tooltip("Syncs Velocity every SyncInterval")] + [SerializeField] bool syncVelocity = true; + + [Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")] + [SerializeField] bool clearVelocity = false; + + [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] + [SerializeField] float velocitySensitivity = 0.1f; + + + [Header("Angular Velocity")] + + [Tooltip("Syncs AngularVelocity every SyncInterval")] + [SerializeField] bool syncAngularVelocity = true; + + [Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")] + [SerializeField] bool clearAngularVelocity = false; + + [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] + [SerializeField] float angularVelocitySensitivity = 0.1f; + + /// + /// Values sent on client with authority after they are sent to the server + /// + readonly ClientSyncState previousValue = new ClientSyncState(); + + void OnValidate() + { + if (target == null) + { + target = GetComponent(); + } + } + + + #region Sync vars + [SyncVar(hook = nameof(OnVelocityChanged))] + Vector2 velocity; + + [SyncVar(hook = nameof(OnAngularVelocityChanged))] + float angularVelocity; + + [SyncVar(hook = nameof(OnIsKinematicChanged))] + bool isKinematic; + + [SyncVar(hook = nameof(OnGravityScaleChanged))] + float gravityScale; + + [SyncVar(hook = nameof(OnuDragChanged))] + float drag; + + [SyncVar(hook = nameof(OnAngularDragChanged))] + float angularDrag; + + /// + /// Ignore value if is host or client with Authority + /// + /// + bool IgnoreSync => isServer || ClientWithAuthority; + + bool ClientWithAuthority => clientAuthority && hasAuthority; + + void OnVelocityChanged(Vector2 _, Vector2 newValue) + { + if (IgnoreSync) + return; + + target.velocity = newValue; + } + + + void OnAngularVelocityChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.angularVelocity = newValue; + } + + void OnIsKinematicChanged(bool _, bool newValue) + { + if (IgnoreSync) + return; + + target.isKinematic = newValue; + } + + void OnGravityScaleChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.gravityScale = newValue; + } + + void OnuDragChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.drag = newValue; + } + + void OnAngularDragChanged(float _, float newValue) + { + if (IgnoreSync) + return; + + target.angularDrag = newValue; + } + #endregion + + + internal void Update() + { + if (isServer) + { + SyncToClients(); + } + else if (ClientWithAuthority) + { + SendToServer(); + } + } + + internal void FixedUpdate() + { + if (clearAngularVelocity && !syncAngularVelocity) + { + target.angularVelocity = 0f; + } + + if (clearVelocity && !syncVelocity) + { + target.velocity = Vector2.zero; + } + } + + /// + /// Updates sync var values on server so that they sync to the client + /// + [Server] + void SyncToClients() + { + // only update if they have changed more than Sensitivity + + Vector2 currentVelocity = syncVelocity ? target.velocity : default; + float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; + + bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); + bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity); + + if (velocityChanged) + { + velocity = currentVelocity; + previousValue.velocity = currentVelocity; + } + + if (angularVelocityChanged) + { + angularVelocity = currentAngularVelocity; + previousValue.angularVelocity = currentAngularVelocity; + } + + // other rigidbody settings + isKinematic = target.isKinematic; + gravityScale = target.gravityScale; + drag = target.drag; + angularDrag = target.angularDrag; + } + + /// + /// Uses Command to send values to server + /// + [Client] + void SendToServer() + { + if (!hasAuthority) + { + Debug.LogWarning("SendToServer called without authority"); + return; + } + + SendVelocity(); + SendRigidBodySettings(); + } + + [Client] + void SendVelocity() + { + float now = Time.time; + if (now < previousValue.nextSyncTime) + return; + + Vector2 currentVelocity = syncVelocity ? target.velocity : default; + float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; + + bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); + bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); + + // if angularVelocity has changed it is likely that velocity has also changed so just sync both values + // however if only velocity has changed just send velocity + if (angularVelocityChanged) + { + CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity); + previousValue.velocity = currentVelocity; + previousValue.angularVelocity = currentAngularVelocity; + } + else if (velocityChanged) + { + CmdSendVelocity(currentVelocity); + previousValue.velocity = currentVelocity; + } + + + // only update syncTime if either has changed + if (angularVelocityChanged || velocityChanged) + { + previousValue.nextSyncTime = now + syncInterval; + } + } + + [Client] + void SendRigidBodySettings() + { + // These shouldn't change often so it is ok to send in their own Command + if (previousValue.isKinematic != target.isKinematic) + { + CmdSendIsKinematic(target.isKinematic); + previousValue.isKinematic = target.isKinematic; + } + if (previousValue.gravityScale != target.gravityScale) + { + CmdChangeGravityScale(target.gravityScale); + previousValue.gravityScale = target.gravityScale; + } + if (previousValue.drag != target.drag) + { + CmdSendDrag(target.drag); + previousValue.drag = target.drag; + } + if (previousValue.angularDrag != target.angularDrag) + { + CmdSendAngularDrag(target.angularDrag); + previousValue.angularDrag = target.angularDrag; + } + } + + /// + /// Called when only Velocity has changed on the client + /// + [Command] + void CmdSendVelocity(Vector2 velocity) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.velocity = velocity; + target.velocity = velocity; + } + + /// + /// Called when angularVelocity has changed on the client + /// + [Command] + void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + if (syncVelocity) + { + this.velocity = velocity; + + target.velocity = velocity; + + } + this.angularVelocity = angularVelocity; + target.angularVelocity = angularVelocity; + } + + [Command] + void CmdSendIsKinematic(bool isKinematic) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.isKinematic = isKinematic; + target.isKinematic = isKinematic; + } + + [Command] + void CmdChangeGravityScale(float gravityScale) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.gravityScale = gravityScale; + target.gravityScale = gravityScale; + } + + [Command] + void CmdSendDrag(float drag) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.drag = drag; + target.drag = drag; + } + + [Command] + void CmdSendAngularDrag(float angularDrag) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + this.angularDrag = angularDrag; + target.angularDrag = angularDrag; + } + + /// + /// holds previously synced values + /// + public class ClientSyncState + { + /// + /// Next sync time that velocity will be synced, based on syncInterval. + /// + public float nextSyncTime; + public Vector2 velocity; + public float angularVelocity; + public bool isKinematic; + public float gravityScale; + public float drag; + public float angularDrag; + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta new file mode 100644 index 0000000..df466bd --- /dev/null +++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab2cbc52526ea384ba280d13cd1a57b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/GUIConsole.cs b/Assets/Mirror/Components/GUIConsole.cs new file mode 100644 index 0000000..c6acbb8 --- /dev/null +++ b/Assets/Mirror/Components/GUIConsole.cs @@ -0,0 +1,115 @@ +// People should be able to see and report errors to the developer very easily. +// +// Unity's Developer Console only works in development builds and it only shows +// errors. This class provides a console that works in all builds and also shows +// log and warnings in development builds. +// +// Note: we don't include the stack trace, because that can also be grabbed from +// the log files if needed. +// +// Note: there is no 'hide' button because we DO want people to see those errors +// and report them back to us. +// +// Note: normal Debug.Log messages can be shown by building in Debug/Development +// mode. +using UnityEngine; +using System.Collections.Generic; + +namespace Mirror +{ + struct LogEntry + { + public string message; + public LogType type; + + public LogEntry(string message, LogType type) + { + this.message = message; + this.type = type; + } + } + + public class GUIConsole : MonoBehaviour + { + public int height = 150; + + // only keep the recent 'n' entries. otherwise memory would grow forever + // and drawing would get slower and slower. + public int maxLogCount = 50; + + // log as queue so we can remove the first entry easily + Queue log = new Queue(); + + // hotkey to show/hide at runtime for easier debugging + // (sometimes we need to temporarily hide/show it) + // => F12 makes sense. nobody can find ^ in other games. + public KeyCode hotKey = KeyCode.F12; + + // GUI + bool visible; + Vector2 scroll = Vector2.zero; + + void Awake() + { + Application.logMessageReceived += OnLog; + } + + // OnLog logs everything, even Debug.Log messages in release builds + // => this makes a lot of things easier. e.g. addon initialization logs. + // => it's really better to have than not to have those + void OnLog(string message, string stackTrace, LogType type) + { + // is this important? + // => always show exceptions & errors + // => usually a good idea to show warnings too, otherwise it's too + // easy to miss OnDeserialize warnings etc. in builds + bool isImportant = type == LogType.Error || type == LogType.Exception || type == LogType.Warning; + + // use stack trace only if important + // (otherwise users would have to find and search the log file. + // seeing it in the console directly is way easier to deal with.) + // => only add \n if stack trace is available (only in debug builds) + if (isImportant && !string.IsNullOrWhiteSpace(stackTrace)) + message += $"\n{stackTrace}"; + + // add to queue + log.Enqueue(new LogEntry(message, type)); + + // respect max entries + if (log.Count > maxLogCount) + log.Dequeue(); + + // become visible if it was important + // (no need to become visible for regular log. let the user decide.) + if (isImportant) + visible = true; + + // auto scroll + scroll.y = float.MaxValue; + } + + void Update() + { + if (Input.GetKeyDown(hotKey)) + visible = !visible; + } + + void OnGUI() + { + if (!visible) return; + + scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height)); + foreach (LogEntry entry in log) + { + if (entry.type == LogType.Error || entry.type == LogType.Exception) + GUI.color = Color.red; + else if (entry.type == LogType.Warning) + GUI.color = Color.yellow; + + GUILayout.Label(entry.message); + GUI.color = Color.white; + } + GUILayout.EndScrollView(); + } + } +} diff --git a/Assets/Mirror/Components/GUIConsole.cs.meta b/Assets/Mirror/Components/GUIConsole.cs.meta new file mode 100644 index 0000000..5664216 --- /dev/null +++ b/Assets/Mirror/Components/GUIConsole.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9021b6cc314944290986ab6feb48db79 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement.meta b/Assets/Mirror/Components/InterestManagement.meta new file mode 100644 index 0000000..9b1f746 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c66f27e006ab94253b39a55a3b213651 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Distance.meta b/Assets/Mirror/Components/InterestManagement/Distance.meta new file mode 100644 index 0000000..9847902 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Distance.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa4cbc6b9c584db4971985cb9f369077 +timeCreated: 1613110605 \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs new file mode 100644 index 0000000..116051b --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -0,0 +1,74 @@ +// straight forward Vector3.Distance based interest management. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Distance/Distance Interest Management")] + public class DistanceInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")] + public int visRange = 10; + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + // helper function to get vis range for a given object, or default. + int GetVisRange(NetworkIdentity identity) + { + return identity.TryGetComponent(out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + } + + [ServerCallback] + public override void Reset() + { + lastRebuildTime = 0D; + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + int range = GetVisRange(identity); + return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // cache range and .transform because both call GetComponent. + int range = GetVisRange(identity); + Vector3 position = identity.transform.position; + + // brute force distance check + // -> only player connections can be observers, so it's enough if we + // go through all connections instead of all spawned identities. + // -> compared to UNET's sphere cast checking, this one is orders of + // magnitude faster. if we have 10k monsters and run a sphere + // cast 10k times, we will see a noticeable lag even with physics + // layers. but checking to every connection is fast. + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // authenticated and joined world with a player? + if (conn != null && conn.isAuthenticated && conn.identity != null) + { + // check distance + if (Vector3.Distance(conn.identity.transform.position, position) < range) + { + newObservers.Add(conn); + } + } + } + } + + // internal so we can update from tests + [ServerCallback] + internal void Update() + { + // rebuild all spawned NetworkIdentity's observers every interval + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta new file mode 100644 index 0000000..1a575af --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f60becab051427fbdd3c8ac9ab4712b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs new file mode 100644 index 0000000..25f5347 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs @@ -0,0 +1,15 @@ +// add this to NetworkIdentities for custom range if needed. +// only works with DistanceInterestManagement. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Interest Management/ Distance/Distance Custom Range")] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public class DistanceInterestManagementCustomRange : NetworkBehaviour + { + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 20; + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta new file mode 100644 index 0000000..406ca78 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2e242ee38a14076a39934172a19079b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Match.meta b/Assets/Mirror/Components/InterestManagement/Match.meta new file mode 100644 index 0000000..e429883 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Match.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5eca5245ae6bb460e9a92f7e14d5493a +timeCreated: 1622649517 \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs new file mode 100644 index 0000000..ff2eadc --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")] + public class MatchInterestManagement : InterestManagement + { + readonly Dictionary> matchObjects = + new Dictionary>(); + + readonly Dictionary lastObjectMatch = + new Dictionary(); + + readonly HashSet dirtyMatches = new HashSet(); + + public override void OnSpawned(NetworkIdentity identity) + { + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + return; + + Guid currentMatch = networkMatch.matchId; + lastObjectMatch[identity] = currentMatch; + + // Guid.Empty is never a valid matchId...do not add to matchObjects collection + if (currentMatch == Guid.Empty) + return; + + // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}"); + if (!matchObjects.TryGetValue(currentMatch, out HashSet objects)) + { + objects = new HashSet(); + matchObjects.Add(currentMatch, objects); + } + + objects.Add(identity); + } + + public override void OnDestroyed(NetworkIdentity identity) + { + lastObjectMatch.TryGetValue(identity, out Guid currentMatch); + lastObjectMatch.Remove(identity); + if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity)) + RebuildMatchObservers(currentMatch); + } + + // internal so we can update from tests + [ServerCallback] + internal void Update() + { + // for each spawned: + // if match changed: + // add previous to dirty + // add new to dirty + foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) + { + // Ignore objects that don't have a NetworkMatch component + if (!netIdentity.TryGetComponent(out NetworkMatch networkMatch)) + continue; + + Guid newMatch = networkMatch.matchId; + lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch); + + // Guid.Empty is never a valid matchId + // Nothing to do if matchId hasn't changed + if (newMatch == Guid.Empty || newMatch == currentMatch) + continue; + + // Mark new/old matches as dirty so they get rebuilt + UpdateDirtyMatches(newMatch, currentMatch); + + // This object is in a new match so observers in the prior match + // and the new match need to rebuild their respective observers lists. + UpdateMatchObjects(netIdentity, newMatch, currentMatch); + } + + // rebuild all dirty matchs + foreach (Guid dirtyMatch in dirtyMatches) + RebuildMatchObservers(dirtyMatch); + + dirtyMatches.Clear(); + } + + void UpdateDirtyMatches(Guid newMatch, Guid currentMatch) + { + // Guid.Empty is never a valid matchId + if (currentMatch != Guid.Empty) + dirtyMatches.Add(currentMatch); + + dirtyMatches.Add(newMatch); + } + + void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch) + { + // Remove this object from the hashset of the match it just left + // Guid.Empty is never a valid matchId + if (currentMatch != Guid.Empty) + matchObjects[currentMatch].Remove(netIdentity); + + // Set this to the new match this object just entered + lastObjectMatch[netIdentity] = newMatch; + + // Make sure this new match is in the dictionary + if (!matchObjects.ContainsKey(newMatch)) + matchObjects.Add(newMatch, new HashSet()); + + // Add this object to the hashset of the new match + matchObjects[newMatch].Add(netIdentity); + } + + void RebuildMatchObservers(Guid matchId) + { + foreach (NetworkIdentity netIdentity in matchObjects[matchId]) + if (netIdentity != null) + NetworkServer.RebuildObservers(netIdentity, false); + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Never observed if no NetworkMatch component + if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) + return false; + + // Guid.Empty is never a valid matchId + if (identityNetworkMatch.matchId == Guid.Empty) + return false; + + // Never observed if no NetworkMatch component + if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) + return false; + + // Guid.Empty is never a valid matchId + if (newObserverNetworkMatch.matchId == Guid.Empty) + return false; + + return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + return; + + Guid matchId = networkMatch.matchId; + + // Guid.Empty is never a valid matchId + if (matchId == Guid.Empty) + return; + + if (!matchObjects.TryGetValue(matchId, out HashSet objects)) + return; + + // Add everything in the hashset for this object's current match + foreach (NetworkIdentity networkIdentity in objects) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + newObservers.Add(networkIdentity.connectionToClient); + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta new file mode 100644 index 0000000..605d215 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs new file mode 100644 index 0000000..f6689f2 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs @@ -0,0 +1,15 @@ +// simple component that holds match information +using System; +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Interest Management/ Match/Network Match")] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public class NetworkMatch : NetworkBehaviour + { + ///Set this to the same value on all networked objects that belong to a given match + public Guid matchId; + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta new file mode 100644 index 0000000..47fc70a --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d17e718851449a6879986e45c458fb7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Scene.meta b/Assets/Mirror/Components/InterestManagement/Scene.meta new file mode 100644 index 0000000..28b469f --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Scene.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7655d309a46a4bd4860edf964228b3f6 +timeCreated: 1622649517 \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs new file mode 100644 index 0000000..8cbfa3b --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Scene/Scene Interest Management")] + public class SceneInterestManagement : InterestManagement + { + // Use Scene instead of string scene.name because when additively + // loading multiples of a subscene the name won't be unique + readonly Dictionary> sceneObjects = + new Dictionary>(); + + readonly Dictionary lastObjectScene = + new Dictionary(); + + HashSet dirtyScenes = new HashSet(); + + public override void OnSpawned(NetworkIdentity identity) + { + Scene currentScene = identity.gameObject.scene; + lastObjectScene[identity] = currentScene; + // Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}"); + if (!sceneObjects.TryGetValue(currentScene, out HashSet objects)) + { + objects = new HashSet(); + sceneObjects.Add(currentScene, objects); + } + + objects.Add(identity); + } + + public override void OnDestroyed(NetworkIdentity identity) + { + Scene currentScene = lastObjectScene[identity]; + lastObjectScene.Remove(identity); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + RebuildSceneObservers(currentScene); + } + + // internal so we can update from tests + [ServerCallback] + internal void Update() + { + // for each spawned: + // if scene changed: + // add previous to dirty + // add new to dirty + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) + { + Scene currentScene = lastObjectScene[identity]; + Scene newScene = identity.gameObject.scene; + if (newScene == currentScene) + continue; + + // Mark new/old scenes as dirty so they get rebuilt + dirtyScenes.Add(currentScene); + dirtyScenes.Add(newScene); + + // This object is in a new scene so observers in the prior scene + // and the new scene need to rebuild their respective observers lists. + + // Remove this object from the hashset of the scene it just left + sceneObjects[currentScene].Remove(identity); + + // Set this to the new scene this object just entered + lastObjectScene[identity] = newScene; + + // Make sure this new scene is in the dictionary + if (!sceneObjects.ContainsKey(newScene)) + sceneObjects.Add(newScene, new HashSet()); + + // Add this object to the hashset of the new scene + sceneObjects[newScene].Add(identity); + } + + // rebuild all dirty scenes + foreach (Scene dirtyScene in dirtyScenes) + RebuildSceneObservers(dirtyScene); + + dirtyScenes.Clear(); + } + + void RebuildSceneObservers(Scene scene) + { + foreach (NetworkIdentity netIdentity in sceneObjects[scene]) + if (netIdentity != null) + NetworkServer.RebuildObservers(netIdentity, false); + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + return identity.gameObject.scene == newObserver.identity.gameObject.scene; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet objects)) + return; + + // Add everything in the hashset for this object's current scene + foreach (NetworkIdentity networkIdentity in objects) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + newObservers.Add(networkIdentity.connectionToClient); + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta new file mode 100644 index 0000000..9cc1ff5 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b979f26c95d34324ba005bfacfa9c4fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta new file mode 100644 index 0000000..00f5cd6 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfa12b73503344d49b398b01bcb07967 +timeCreated: 1613110634 \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs new file mode 100644 index 0000000..88f7197 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs @@ -0,0 +1,88 @@ +// Grid2D from uMMORPG: get/set values of type T at any point +// -> not named 'Grid' because Unity already has a Grid type. causes warnings. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public class Grid2D + { + // the grid + // note that we never remove old keys. + // => over time, HashSets will be allocated for every possible + // grid position in the world + // => Clear() doesn't clear them so we don't constantly reallocate the + // entries when populating the grid in every Update() call + // => makes the code a lot easier too + // => this is FINE because in the worst case, every grid position in the + // game world is filled with a player anyway! + Dictionary> grid = new Dictionary>(); + + // cache a 9 neighbor grid of vector2 offsets so we can use them more easily + Vector2Int[] neighbourOffsets = + { + Vector2Int.up, + Vector2Int.up + Vector2Int.left, + Vector2Int.up + Vector2Int.right, + Vector2Int.left, + Vector2Int.zero, + Vector2Int.right, + Vector2Int.down, + Vector2Int.down + Vector2Int.left, + Vector2Int.down + Vector2Int.right + }; + + // helper function so we can add an entry without worrying + public void Add(Vector2Int position, T value) + { + // initialize set in grid if it's not in there yet + if (!grid.TryGetValue(position, out HashSet hashSet)) + { + hashSet = new HashSet(); + grid[position] = hashSet; + } + + // add to it + hashSet.Add(value); + } + + // helper function to get set at position without worrying + // -> result is passed as parameter to avoid allocations + // -> result is not cleared before. this allows us to pass the HashSet from + // GetWithNeighbours and avoid .UnionWith which is very expensive. + void GetAt(Vector2Int position, HashSet result) + { + // return the set at position + if (grid.TryGetValue(position, out HashSet hashSet)) + { + foreach (T entry in hashSet) + result.Add(entry); + } + } + + // helper function to get at position and it's 8 neighbors without worrying + // -> result is passed as parameter to avoid allocations + public void GetWithNeighbours(Vector2Int position, HashSet result) + { + // clear result first + result.Clear(); + + // add neighbours + foreach (Vector2Int offset in neighbourOffsets) + GetAt(position + offset, result); + } + + // clear: clears the whole grid + // IMPORTANT: we already allocated HashSets and don't want to do + // reallocate every single update when we rebuild the grid. + // => so simply remove each position's entries, but keep + // every position in there + // => see 'grid' comments above! + // => named ClearNonAlloc to make it more obvious! + public void ClearNonAlloc() + { + foreach (HashSet hashSet in grid.Values) + hashSet.Clear(); + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta new file mode 100644 index 0000000..f1d3cf0 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c5232a4d2854116a35d52b80ec07752 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs new file mode 100644 index 0000000..eb4c2c5 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -0,0 +1,144 @@ +// extremely fast spatial hashing interest management based on uMMORPG GridChecker. +// => 30x faster in initial tests +// => scales way higher +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] + public class SpatialHashingInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 30; + + // if we see 8 neighbors then 1 entry is visRange/3 + public int resolution => visRange / 3; + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + public enum CheckMethod + { + XZ_FOR_3D, + XY_FOR_2D + } + [Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")] + public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D; + + // debugging + public bool showSlider; + + // the grid + Grid2D grid = new Grid2D(); + + // project 3d world position to grid position + Vector2Int ProjectToGrid(Vector3 position) => + checkMethod == CheckMethod.XZ_FOR_3D + ? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution) + : Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution); + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // calculate projected positions + Vector2Int projected = ProjectToGrid(identity.transform.position); + Vector2Int observerProjected = ProjectToGrid(newObserver.identity.transform.position); + + // distance needs to be at max one of the 8 neighbors, which is + // 1 for the direct neighbors + // 1.41 for the diagonal neighbors (= sqrt(2)) + // => use sqrMagnitude and '2' to avoid computations. same result. + return (projected - observerProjected).sqrMagnitude <= 2; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // add everyone in 9 neighbour grid + // -> pass observers to GetWithNeighbours directly to avoid allocations + // and expensive .UnionWith computations. + Vector2Int current = ProjectToGrid(identity.transform.position); + grid.GetWithNeighbours(current, newObservers); + } + + [ServerCallback] + public override void Reset() + { + lastRebuildTime = 0D; + } + + // update everyone's position in the grid + // (internal so we can update from tests) + [ServerCallback] + internal void Update() + { + // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL + // entities every INTERVAL. consider the other approach later. + + // IMPORTANT: refresh grid every update! + // => newly spawned entities get observers assigned via + // OnCheckObservers. this can happen any time and we don't want + // them broadcast to old (moved or destroyed) connections. + // => players do move all the time. we want them to always be in the + // correct grid position. + // => note that the actual 'rebuildall' doesn't need to happen all + // the time. + // NOTE: consider refreshing grid only every 'interval' too. but not + // for now. stability & correctness matter. + + // clear old grid results before we update everyone's position. + // (this way we get rid of destroyed connections automatically) + // + // NOTE: keeps allocated HashSets internally. + // clearing & populating every frame works without allocations + grid.ClearNonAlloc(); + + // put every connection into the grid at it's main player's position + // NOTE: player sees in a radius around him. NOT around his pet too. + foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values) + { + // authenticated and joined world with a player? + if (connection.isAuthenticated && connection.identity != null) + { + // calculate current grid position + Vector2Int position = ProjectToGrid(connection.identity.transform.position); + + // put into grid + grid.Add(position, connection); + } + } + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // slider from dotsnet. it's nice to play around with in the benchmark + // demo. + void OnGUI() + { + if (!showSlider) return; + + // only show while server is running. not on client, etc. + if (!NetworkServer.active) return; + + int height = 30; + int width = 250; + GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height)); + GUILayout.BeginHorizontal("Box"); + GUILayout.Label("Radius:"); + visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150))); + GUILayout.Label(visRange.ToString()); + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + } +#endif + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs.meta new file mode 100644 index 0000000..271e433 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39adc6e09d5544ed955a50ce8600355a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Team.meta b/Assets/Mirror/Components/InterestManagement/Team.meta new file mode 100644 index 0000000..fe40aa4 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Team.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d418e60072433b4bbebbf5f3a7de1bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs new file mode 100644 index 0000000..e6033ad --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs @@ -0,0 +1,17 @@ +// simple component that holds team information +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Interest Management/ Team/Network Team")] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public class NetworkTeam : NetworkBehaviour + { + [Tooltip("Set this to the same value on all networked objects that belong to a given team")] + public string teamId = string.Empty; + + [Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")] + public bool forceShown; + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta new file mode 100644 index 0000000..ca75a7a --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2576730625b1632468cbcbfe5e721f88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs new file mode 100644 index 0000000..22a8eb0 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")] + public class TeamInterestManagement : InterestManagement + { + readonly Dictionary> teamObjects = + new Dictionary>(); + + readonly Dictionary lastObjectTeam = + new Dictionary(); + + readonly HashSet dirtyTeams = new HashSet(); + + public override void OnSpawned(NetworkIdentity identity) + { + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + return; + + string currentTeam = networkTeam.teamId; + lastObjectTeam[identity] = currentTeam; + + // string.Empty is never a valid teamId...do not add to teamObjects collection + if (currentTeam == string.Empty) + return; + + // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentTeam}"); + if (!teamObjects.TryGetValue(currentTeam, out HashSet objects)) + { + objects = new HashSet(); + teamObjects.Add(currentTeam, objects); + } + + objects.Add(identity); + } + + public override void OnDestroyed(NetworkIdentity identity) + { + lastObjectTeam.TryGetValue(identity, out string currentTeam); + lastObjectTeam.Remove(identity); + if (currentTeam != string.Empty && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) + RebuildTeamObservers(currentTeam); + } + + // internal so we can update from tests + [ServerCallback] + internal void Update() + { + // for each spawned: + // if team changed: + // add previous to dirty + // add new to dirty + foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) + { + // Ignore objects that don't have a NetworkTeam component + if (!netIdentity.TryGetComponent(out NetworkTeam networkTeam)) + continue; + + string newTeam = networkTeam.teamId; + if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam)) + continue; + + // string.Empty is never a valid teamId + // Nothing to do if teamId hasn't changed + if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam) + continue; + + // Mark new/old Teams as dirty so they get rebuilt + UpdateDirtyTeams(newTeam, currentTeam); + + // This object is in a new team so observers in the prior team + // and the new team need to rebuild their respective observers lists. + UpdateTeamObjects(netIdentity, newTeam, currentTeam); + } + + // rebuild all dirty teams + foreach (string dirtyTeam in dirtyTeams) + RebuildTeamObservers(dirtyTeam); + + dirtyTeams.Clear(); + } + + void UpdateDirtyTeams(string newTeam, string currentTeam) + { + // string.Empty is never a valid teamId + if (currentTeam != string.Empty) + dirtyTeams.Add(currentTeam); + + dirtyTeams.Add(newTeam); + } + + void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam) + { + // Remove this object from the hashset of the team it just left + // string.Empty is never a valid teamId + if (!string.IsNullOrWhiteSpace(currentTeam)) + teamObjects[currentTeam].Remove(netIdentity); + + // Set this to the new team this object just entered + lastObjectTeam[netIdentity] = newTeam; + + // Make sure this new team is in the dictionary + if (!teamObjects.ContainsKey(newTeam)) + teamObjects.Add(newTeam, new HashSet()); + + // Add this object to the hashset of the new team + teamObjects[newTeam].Add(netIdentity); + } + + void RebuildTeamObservers(string teamId) + { + foreach (NetworkIdentity netIdentity in teamObjects[teamId]) + if (netIdentity != null) + NetworkServer.RebuildObservers(netIdentity, false); + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Always observed if no NetworkTeam component + if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) + return true; + + if (identityNetworkTeam.forceShown) + return true; + + // string.Empty is never a valid teamId + if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId)) + return false; + + // Always observed if no NetworkTeam component + if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) + return true; + + if (newObserverNetworkTeam.forceShown) + return true; + + // string.Empty is never a valid teamId + if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId)) + return false; + + // Observed only if teamId's match + return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // If this object doesn't have a NetworkTeam then it's visible to all clients + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + { + AddAllConnections(newObservers); + return; + } + + // If this object has NetworkTeam and forceShown == true then it's visible to all clients + if (networkTeam.forceShown) + { + AddAllConnections(newObservers); + return; + } + + // string.Empty is never a valid teamId + if (networkTeam.teamId == string.Empty) + return; + + // Abort if this team hasn't been created yet by OnSpawned or UpdateTeamObjects + if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet objects)) + return; + + // Add everything in the hashset for this object's current team + foreach (NetworkIdentity networkIdentity in objects) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + newObservers.Add(networkIdentity.connectionToClient); + } + + void AddAllConnections(HashSet newObservers) + { + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // authenticated and joined world with a player? + if (conn != null && conn.isAuthenticated && conn.identity != null) + newObservers.Add(conn); + } + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta new file mode 100644 index 0000000..6e8748a --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dceb9a7085758fd4590419ff5b14b636 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef b/Assets/Mirror/Components/Mirror.Components.asmdef new file mode 100644 index 0000000..a61c7db --- /dev/null +++ b/Assets/Mirror/Components/Mirror.Components.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.Components", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef.meta b/Assets/Mirror/Components/Mirror.Components.asmdef.meta new file mode 100644 index 0000000..263b6f0 --- /dev/null +++ b/Assets/Mirror/Components/Mirror.Components.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 72872094b21c16e48b631b2224833d49 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs new file mode 100644 index 0000000..2360fe3 --- /dev/null +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -0,0 +1,634 @@ +using System.Linq; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mirror +{ + /// + /// A component to synchronize Mecanim animation states for networked objects. + /// + /// + /// The animation of game objects can be networked by this component. There are two models of authority for networked movement: + /// If the object has authority on the client, then it should be animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects. + /// If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit. + /// The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients. + /// + [AddComponentMenu("Network/Network Animator")] + [RequireComponent(typeof(NetworkIdentity))] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")] + public class NetworkAnimator : NetworkBehaviour + { + [Header("Authority")] + [Tooltip("Set to true if animations come from owner client, set to false if animations always come from server")] + public bool clientAuthority; + + /// + /// The animator component to synchronize. + /// + [FormerlySerializedAs("m_Animator")] + [Header("Animator")] + [Tooltip("Animator that will have parameters synchronized")] + public Animator animator; + + /// + /// Syncs animator.speed + /// + [SyncVar(hook = nameof(OnAnimatorSpeedChanged))] + float animatorSpeed; + float previousSpeed; + + // Note: not an object[] array because otherwise initialization is real annoying + int[] lastIntParameters; + float[] lastFloatParameters; + bool[] lastBoolParameters; + AnimatorControllerParameter[] parameters; + + // multiple layers + int[] animationHash; + int[] transitionHash; + float[] layerWeight; + double nextSendTime; + + bool SendMessagesAllowed + { + get + { + if (isServer) + { + if (!clientAuthority) + return true; + + // This is a special case where we have client authority but we have not assigned the client who has + // authority over it, no animator data will be sent over the network by the server. + // + // So we check here for a connectionToClient and if it is null we will + // let the server send animation data until we receive an owner. + if (netIdentity != null && netIdentity.connectionToClient == null) + return true; + } + + return (hasAuthority && clientAuthority); + } + } + + void Awake() + { + // store the animator parameters in a variable - the "Animator.parameters" getter allocates + // a new parameter array every time it is accessed so we should avoid doing it in a loop + parameters = animator.parameters + .Where(par => !animator.IsParameterControlledByCurve(par.nameHash)) + .ToArray(); + lastIntParameters = new int[parameters.Length]; + lastFloatParameters = new float[parameters.Length]; + lastBoolParameters = new bool[parameters.Length]; + + animationHash = new int[animator.layerCount]; + transitionHash = new int[animator.layerCount]; + layerWeight = new float[animator.layerCount]; + } + + void FixedUpdate() + { + if (!SendMessagesAllowed) + return; + + if (!animator.enabled) + return; + + CheckSendRate(); + + for (int i = 0; i < animator.layerCount; i++) + { + int stateHash; + float normalizedTime; + if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i)) + { + continue; + } + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + WriteParameters(writer); + SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray()); + } + } + + CheckSpeed(); + } + + void CheckSpeed() + { + float newSpeed = animator.speed; + if (Mathf.Abs(previousSpeed - newSpeed) > 0.001f) + { + previousSpeed = newSpeed; + if (isServer) + { + animatorSpeed = newSpeed; + } + else if (isClient) + { + CmdSetAnimatorSpeed(newSpeed); + } + } + } + + void OnAnimatorSpeedChanged(float _, float value) + { + // skip if host or client with authority + // they will have already set the speed so don't set again + if (isServer || (hasAuthority && clientAuthority)) + return; + + animator.speed = value; + } + + bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId) + { + bool change = false; + stateHash = 0; + normalizedTime = 0; + + float lw = animator.GetLayerWeight(layerId); + if (Mathf.Abs(lw - layerWeight[layerId]) > 0.001f) + { + layerWeight[layerId] = lw; + change = true; + } + + if (animator.IsInTransition(layerId)) + { + AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId); + if (tt.fullPathHash != transitionHash[layerId]) + { + // first time in this transition + transitionHash[layerId] = tt.fullPathHash; + animationHash[layerId] = 0; + return true; + } + return change; + } + + AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId); + if (st.fullPathHash != animationHash[layerId]) + { + // first time in this animation state + if (animationHash[layerId] != 0) + { + // came from another animation directly - from Play() + stateHash = st.fullPathHash; + normalizedTime = st.normalizedTime; + } + transitionHash[layerId] = 0; + animationHash[layerId] = st.fullPathHash; + return true; + } + return change; + } + + void CheckSendRate() + { + double now = NetworkTime.localTime; + if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime) + { + nextSendTime = now + syncInterval; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + if (WriteParameters(writer)) + SendAnimationParametersMessage(writer.ToArray()); + } + } + } + + void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters) + { + if (isServer) + { + RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters); + } + else if (isClient) + { + CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, weight, parameters); + } + } + + void SendAnimationParametersMessage(byte[] parameters) + { + if (isServer) + { + RpcOnAnimationParametersClientMessage(parameters); + } + else if (isClient) + { + CmdOnAnimationParametersServerMessage(parameters); + } + } + + void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader) + { + if (hasAuthority && clientAuthority) + return; + + // usually transitions will be triggered by parameters, if not, play anims directly. + // NOTE: this plays "animations", not transitions, so any transitions will be skipped. + // NOTE: there is no API to play a transition(?) + if (stateHash != 0 && animator.enabled) + { + animator.Play(stateHash, layerId, normalizedTime); + } + + animator.SetLayerWeight(layerId, weight); + + ReadParameters(reader); + } + + void HandleAnimParamsMsg(NetworkReader reader) + { + if (hasAuthority && clientAuthority) + return; + + ReadParameters(reader); + } + + void HandleAnimTriggerMsg(int hash) + { + if (animator.enabled) + animator.SetTrigger(hash); + } + + void HandleAnimResetTriggerMsg(int hash) + { + if (animator.enabled) + animator.ResetTrigger(hash); + } + + ulong NextDirtyBits() + { + ulong dirtyBits = 0; + for (int i = 0; i < parameters.Length; i++) + { + AnimatorControllerParameter par = parameters[i]; + bool changed = false; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = animator.GetInteger(par.nameHash); + changed = newIntValue != lastIntParameters[i]; + if (changed) + lastIntParameters[i] = newIntValue; + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = animator.GetFloat(par.nameHash); + changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f; + // only set lastValue if it was changed, otherwise value could slowly drift within the 0.001f limit each frame + if (changed) + lastFloatParameters[i] = newFloatValue; + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = animator.GetBool(par.nameHash); + changed = newBoolValue != lastBoolParameters[i]; + if (changed) + lastBoolParameters[i] = newBoolValue; + } + if (changed) + { + dirtyBits |= 1ul << i; + } + } + return dirtyBits; + } + + bool WriteParameters(NetworkWriter writer, bool forceAll = false) + { + ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits(); + writer.WriteULong(dirtyBits); + for (int i = 0; i < parameters.Length; i++) + { + if ((dirtyBits & (1ul << i)) == 0) + continue; + + AnimatorControllerParameter par = parameters[i]; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = animator.GetInteger(par.nameHash); + writer.WriteInt(newIntValue); + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = animator.GetFloat(par.nameHash); + writer.WriteFloat(newFloatValue); + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = animator.GetBool(par.nameHash); + writer.WriteBool(newBoolValue); + } + } + return dirtyBits != 0; + } + + void ReadParameters(NetworkReader reader) + { + bool animatorEnabled = animator.enabled; + // need to read values from NetworkReader even if animator is disabled + + ulong dirtyBits = reader.ReadULong(); + for (int i = 0; i < parameters.Length; i++) + { + if ((dirtyBits & (1ul << i)) == 0) + continue; + + AnimatorControllerParameter par = parameters[i]; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = reader.ReadInt(); + if (animatorEnabled) + animator.SetInteger(par.nameHash, newIntValue); + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = reader.ReadFloat(); + if (animatorEnabled) + animator.SetFloat(par.nameHash, newFloatValue); + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = reader.ReadBool(); + if (animatorEnabled) + animator.SetBool(par.nameHash, newBoolValue); + } + } + } + + /// + /// Custom Serialization + /// + /// + /// + /// + public override bool OnSerialize(NetworkWriter writer, bool initialState) + { + bool changed = base.OnSerialize(writer, initialState); + if (initialState) + { + for (int i = 0; i < animator.layerCount; i++) + { + if (animator.IsInTransition(i)) + { + AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i); + writer.WriteInt(st.fullPathHash); + writer.WriteFloat(st.normalizedTime); + } + else + { + AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i); + writer.WriteInt(st.fullPathHash); + writer.WriteFloat(st.normalizedTime); + } + writer.WriteFloat(animator.GetLayerWeight(i)); + } + WriteParameters(writer, initialState); + return true; + } + return changed; + } + + /// + /// Custom Deserialization + /// + /// + /// + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + base.OnDeserialize(reader, initialState); + if (initialState) + { + for (int i = 0; i < animator.layerCount; i++) + { + int stateHash = reader.ReadInt(); + float normalizedTime = reader.ReadFloat(); + animator.SetLayerWeight(i, reader.ReadFloat()); + animator.Play(stateHash, i, normalizedTime); + } + + ReadParameters(reader); + } + } + + /// + /// Causes an animation trigger to be invoked for a networked object. + /// If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients. + /// + /// Name of trigger. + public void SetTrigger(string triggerName) + { + SetTrigger(Animator.StringToHash(triggerName)); + } + + /// + /// Causes an animation trigger to be invoked for a networked object. + /// + /// Hash id of trigger (from the Animator). + public void SetTrigger(int hash) + { + if (clientAuthority) + { + if (!isClient) + { + Debug.LogWarning("Tried to set animation in the server for a client-controlled animator"); + return; + } + + if (!hasAuthority) + { + Debug.LogWarning("Only the client with authority can set animations"); + return; + } + + if (isClient) + CmdOnAnimationTriggerServerMessage(hash); + + // call on client right away + HandleAnimTriggerMsg(hash); + } + else + { + if (!isServer) + { + Debug.LogWarning("Tried to set animation in the client for a server-controlled animator"); + return; + } + + HandleAnimTriggerMsg(hash); + RpcOnAnimationTriggerClientMessage(hash); + } + } + + /// + /// Causes an animation trigger to be reset for a networked object. + /// If local authority is set, and this is called from the client, then the trigger will be reset on the server and all clients. If not, then this is called on the server, and the trigger will be reset on all clients. + /// + /// Name of trigger. + public void ResetTrigger(string triggerName) + { + ResetTrigger(Animator.StringToHash(triggerName)); + } + + /// + /// Causes an animation trigger to be reset for a networked object. + /// + /// Hash id of trigger (from the Animator). + public void ResetTrigger(int hash) + { + if (clientAuthority) + { + if (!isClient) + { + Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator"); + return; + } + + if (!hasAuthority) + { + Debug.LogWarning("Only the client with authority can reset animations"); + return; + } + + if (isClient) + CmdOnAnimationResetTriggerServerMessage(hash); + + // call on client right away + HandleAnimResetTriggerMsg(hash); + } + else + { + if (!isServer) + { + Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator"); + return; + } + + HandleAnimResetTriggerMsg(hash); + RpcOnAnimationResetTriggerClientMessage(hash); + } + } + + #region server message handlers + + [Command] + void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + //Debug.Log($"OnAnimationMessage for netId {netId}"); + + // handle and broadcast + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters)) + { + HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader); + RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters); + } + } + + [Command] + void CmdOnAnimationParametersServerMessage(byte[] parameters) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + // handle and broadcast + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters)) + { + HandleAnimParamsMsg(networkReader); + RpcOnAnimationParametersClientMessage(parameters); + } + } + + [Command] + void CmdOnAnimationTriggerServerMessage(int hash) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + // handle and broadcast + // host should have already the trigger + bool isHostOwner = isClient && hasAuthority; + if (!isHostOwner) + { + HandleAnimTriggerMsg(hash); + } + + RpcOnAnimationTriggerClientMessage(hash); + } + + [Command] + void CmdOnAnimationResetTriggerServerMessage(int hash) + { + // Ignore messages from client if not in client authority mode + if (!clientAuthority) + return; + + // handle and broadcast + // host should have already the trigger + bool isHostOwner = isClient && hasAuthority; + if (!isHostOwner) + { + HandleAnimResetTriggerMsg(hash); + } + + RpcOnAnimationResetTriggerClientMessage(hash); + } + + [Command] + void CmdSetAnimatorSpeed(float newSpeed) + { + // set animator + animator.speed = newSpeed; + animatorSpeed = newSpeed; + } + + #endregion + + #region client message handlers + + [ClientRpc] + void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters) + { + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters)) + HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader); + } + + [ClientRpc] + void RpcOnAnimationParametersClientMessage(byte[] parameters) + { + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters)) + HandleAnimParamsMsg(networkReader); + } + + [ClientRpc] + void RpcOnAnimationTriggerClientMessage(int hash) + { + // host/owner handles this before it is sent + if (isServer || (clientAuthority && hasAuthority)) return; + + HandleAnimTriggerMsg(hash); + } + + [ClientRpc] + void RpcOnAnimationResetTriggerClientMessage(int hash) + { + // host/owner handles this before it is sent + if (isServer || (clientAuthority && hasAuthority)) return; + + HandleAnimResetTriggerMsg(hash); + } + + #endregion + } +} diff --git a/Assets/Mirror/Components/NetworkAnimator.cs.meta b/Assets/Mirror/Components/NetworkAnimator.cs.meta new file mode 100644 index 0000000..211ce78 --- /dev/null +++ b/Assets/Mirror/Components/NetworkAnimator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f6f3bf89aa97405989c802ba270f815 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkLobbyManager.cs b/Assets/Mirror/Components/NetworkLobbyManager.cs new file mode 100644 index 0000000..3dcd4ea --- /dev/null +++ b/Assets/Mirror/Components/NetworkLobbyManager.cs @@ -0,0 +1,18 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// This is a specialized NetworkManager that includes a networked lobby. + /// + /// + /// The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects. + /// NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager. + /// The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called. + /// + [AddComponentMenu("Network/Network Lobby Manager")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")] + [Obsolete("Use / inherit from NetworkRoomManager instead")] + public class NetworkLobbyManager : NetworkRoomManager {} +} diff --git a/Assets/Mirror/Components/NetworkLobbyManager.cs.meta b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta new file mode 100644 index 0000000..a32c8c7 --- /dev/null +++ b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4c96e6dd99826849ab1431f94547141 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkLobbyPlayer.cs b/Assets/Mirror/Components/NetworkLobbyPlayer.cs new file mode 100644 index 0000000..95ffbbc --- /dev/null +++ b/Assets/Mirror/Components/NetworkLobbyPlayer.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system. + /// The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Lobby Player")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")] + [Obsolete("Use / inherit from NetworkRoomPlayer instead")] + public class NetworkLobbyPlayer : NetworkRoomPlayer {} +} diff --git a/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta new file mode 100644 index 0000000..7a21eec --- /dev/null +++ b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 777a368af85f2e84da7ea5666581921b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs new file mode 100644 index 0000000..61e9241 --- /dev/null +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs @@ -0,0 +1,33 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// Component that will display the clients ping in milliseconds + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Ping Display")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-ping-display")] + public class NetworkPingDisplay : MonoBehaviour + { + public Color color = Color.white; + public int padding = 2; + int width = 150; + int height = 25; + + void OnGUI() + { + // only while client is active + if (!NetworkClient.active) return; + + // show rtt in bottom right corner, right aligned + GUI.color = color; + Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height); + GUIStyle style = GUI.skin.GetStyle("Label"); + style.alignment = TextAnchor.MiddleRight; + GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style); + GUI.color = Color.white; + } + } +} diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs.meta b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta new file mode 100644 index 0000000..221a745 --- /dev/null +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc654f29862fc2643b948f772ebb9e68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs b/Assets/Mirror/Components/NetworkRoomManager.cs new file mode 100644 index 0000000..d432fbb --- /dev/null +++ b/Assets/Mirror/Components/NetworkRoomManager.cs @@ -0,0 +1,714 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Serialization; + +namespace Mirror +{ + /// + /// This is a specialized NetworkManager that includes a networked room. + /// + /// + /// The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects. + /// NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager. + /// The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called. + /// + [AddComponentMenu("Network/Network Room Manager")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")] + public class NetworkRoomManager : NetworkManager + { + public struct PendingPlayer + { + public NetworkConnectionToClient conn; + public GameObject roomPlayer; + } + + [Header("Room Settings")] + + [FormerlySerializedAs("m_ShowRoomGUI")] + [SerializeField] + [Tooltip("This flag controls whether the default UI is shown for the room")] + public bool showRoomGUI = true; + + [FormerlySerializedAs("m_MinPlayers")] + [SerializeField] + [Tooltip("Minimum number of players to auto-start the game")] + public int minPlayers = 1; + + [FormerlySerializedAs("m_RoomPlayerPrefab")] + [SerializeField] + [Tooltip("Prefab to use for the Room Player")] + public NetworkRoomPlayer roomPlayerPrefab; + + /// + /// The scene to use for the room. This is similar to the offlineScene of the NetworkManager. + /// + [Scene] + public string RoomScene; + + /// + /// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager. + /// + [Scene] + public string GameplayScene; + + /// + /// List of players that are in the Room + /// + [FormerlySerializedAs("m_PendingPlayers")] + public List pendingPlayers = new List(); + + [Header("Diagnostics")] + + /// + /// True when all players have submitted a Ready message + /// + [Tooltip("Diagnostic flag indicating all players are ready to play")] + [FormerlySerializedAs("allPlayersReady")] + [SerializeField] bool _allPlayersReady; + + /// + /// These slots track players that enter the room. + /// The slotId on players is global to the game - across all players. + /// + [Tooltip("List of Room Player objects")] + public List roomSlots = new List(); + + public bool allPlayersReady + { + get => _allPlayersReady; + set + { + bool wasReady = _allPlayersReady; + bool nowReady = value; + + if (wasReady != nowReady) + { + _allPlayersReady = value; + + if (nowReady) + { + OnRoomServerPlayersReady(); + } + else + { + OnRoomServerPlayersNotReady(); + } + } + } + } + + public override void OnValidate() + { + // always >= 0 + maxConnections = Mathf.Max(maxConnections, 0); + + // always <= maxConnections + minPlayers = Mathf.Min(minPlayers, maxConnections); + + // always >= 0 + minPlayers = Mathf.Max(minPlayers, 0); + + if (roomPlayerPrefab != null) + { + NetworkIdentity identity = roomPlayerPrefab.GetComponent(); + if (identity == null) + { + roomPlayerPrefab = null; + Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component."); + } + } + + base.OnValidate(); + } + + public void ReadyStatusChanged() + { + int CurrentPlayers = 0; + int ReadyPlayers = 0; + + foreach (NetworkRoomPlayer item in roomSlots) + { + if (item != null) + { + CurrentPlayers++; + if (item.readyToBegin) + ReadyPlayers++; + } + } + + if (CurrentPlayers == ReadyPlayers) + CheckReadyToBegin(); + else + allPlayersReady = false; + } + + /// + /// Called on the server when a client is ready. + /// The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process. + /// + /// Connection from client. + public override void OnServerReady(NetworkConnectionToClient conn) + { + Debug.Log($"NetworkRoomManager OnServerReady {conn}"); + base.OnServerReady(conn); + + if (conn != null && conn.identity != null) + { + GameObject roomPlayer = conn.identity.gameObject; + + // if null or not a room player, don't replace it + if (roomPlayer != null && roomPlayer.GetComponent() != null) + SceneLoadedForPlayer(conn, roomPlayer); + } + } + + void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) + { + Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}"); + + if (IsSceneActive(RoomScene)) + { + // cant be ready in room, add to ready list + PendingPlayer pending; + pending.conn = conn; + pending.roomPlayer = roomPlayer; + pendingPlayers.Add(pending); + return; + } + + GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer); + if (gamePlayer == null) + { + // get start position from base class + Transform startPos = GetStartPosition(); + gamePlayer = startPos != null + ? Instantiate(playerPrefab, startPos.position, startPos.rotation) + : Instantiate(playerPrefab, Vector3.zero, Quaternion.identity); + } + + if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer)) + return; + + // replace room player with game player + NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true); + } + + /// + /// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set. + /// If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState. + /// + public void CheckReadyToBegin() + { + if (!IsSceneActive(RoomScene)) + return; + + int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent().readyToBegin); + bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers; + if (enoughReadyPlayers) + { + pendingPlayers.Clear(); + allPlayersReady = true; + } + else + { + allPlayersReady = false; + } + } + + internal void CallOnClientEnterRoom() + { + OnRoomClientEnter(); + foreach (NetworkRoomPlayer player in roomSlots) + if (player != null) + { + player.OnClientEnterRoom(); + } + } + + internal void CallOnClientExitRoom() + { + OnRoomClientExit(); + foreach (NetworkRoomPlayer player in roomSlots) + if (player != null) + { + player.OnClientExitRoom(); + } + } + + #region server handlers + + /// + /// Called on the server when a new client connects. + /// Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server. + /// + /// Connection from client. + public override void OnServerConnect(NetworkConnectionToClient conn) + { + if (numPlayers >= maxConnections) + { + conn.Disconnect(); + return; + } + + // cannot join game in progress + if (!IsSceneActive(RoomScene)) + { + conn.Disconnect(); + return; + } + + base.OnServerConnect(conn); + OnRoomServerConnect(conn); + } + + /// + /// Called on the server when a client disconnects. + /// This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected. + /// + /// Connection from client. + public override void OnServerDisconnect(NetworkConnectionToClient conn) + { + if (conn.identity != null) + { + NetworkRoomPlayer roomPlayer = conn.identity.GetComponent(); + + if (roomPlayer != null) + roomSlots.Remove(roomPlayer); + + foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects) + { + roomPlayer = clientOwnedObject.GetComponent(); + if (roomPlayer != null) + roomSlots.Remove(roomPlayer); + } + } + + allPlayersReady = false; + + foreach (NetworkRoomPlayer player in roomSlots) + { + if (player != null) + player.GetComponent().readyToBegin = false; + } + + if (IsSceneActive(RoomScene)) + RecalculateRoomPlayerIndices(); + + OnRoomServerDisconnect(conn); + base.OnServerDisconnect(conn); + +#if UNITY_SERVER + if (numPlayers < 1) + StopServer(); +#endif + } + + // Sequential index used in round-robin deployment of players into instances and score positioning + public int clientIndex; + + /// + /// Called on the server when a client adds a new player with NetworkClient.AddPlayer. + /// The default implementation for this function creates a new player object from the playerPrefab. + /// + /// Connection from client. + public override void OnServerAddPlayer(NetworkConnectionToClient conn) + { + // increment the index before adding the player, so first player starts at 1 + clientIndex++; + + if (IsSceneActive(RoomScene)) + { + if (roomSlots.Count == maxConnections) + return; + + allPlayersReady = false; + + //Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}"); + + GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn); + if (newRoomGameObject == null) + newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity); + + NetworkServer.AddPlayerForConnection(conn, newRoomGameObject); + } + else + OnRoomServerAddPlayer(conn); + } + + [Server] + public void RecalculateRoomPlayerIndices() + { + if (roomSlots.Count > 0) + { + for (int i = 0; i < roomSlots.Count; i++) + { + roomSlots[i].index = i; + } + } + } + + /// + /// This causes the server to switch scenes and sets the networkSceneName. + /// Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene. + /// + /// + public override void ServerChangeScene(string newSceneName) + { + if (newSceneName == RoomScene) + { + foreach (NetworkRoomPlayer roomPlayer in roomSlots) + { + if (roomPlayer == null) + continue; + + // find the game-player object for this connection, and destroy it + NetworkIdentity identity = roomPlayer.GetComponent(); + + if (NetworkServer.active) + { + // re-add the room object + roomPlayer.GetComponent().readyToBegin = false; + NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject); + } + } + + allPlayersReady = false; + } + + base.ServerChangeScene(newSceneName); + } + + /// + /// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene(). + /// + /// The name of the new scene. + public override void OnServerSceneChanged(string sceneName) + { + if (sceneName != RoomScene) + { + // call SceneLoadedForPlayer on any players that become ready while we were loading the scene. + foreach (PendingPlayer pending in pendingPlayers) + SceneLoadedForPlayer(pending.conn, pending.roomPlayer); + + pendingPlayers.Clear(); + } + + OnRoomServerSceneChanged(sceneName); + } + + /// + /// This is invoked when a server is started - including when a host is started. + /// StartServer has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartServer() + { + if (string.IsNullOrWhiteSpace(RoomScene)) + { + Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager"); + return; + } + + if (string.IsNullOrWhiteSpace(GameplayScene)) + { + Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager"); + return; + } + + OnRoomStartServer(); + } + + /// + /// This is invoked when a host is started. + /// StartHost has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartHost() + { + OnRoomStartHost(); + } + + /// + /// This is called when a server is stopped - including when a host is stopped. + /// + public override void OnStopServer() + { + roomSlots.Clear(); + OnRoomStopServer(); + } + + /// + /// This is called when a host is stopped. + /// + public override void OnStopHost() + { + OnRoomStopHost(); + } + + #endregion + + #region client handlers + + /// + /// This is invoked when the client is started. + /// + public override void OnStartClient() + { + if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null) + Debug.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab."); + else + NetworkClient.RegisterPrefab(roomPlayerPrefab.gameObject); + + if (playerPrefab == null) + Debug.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab."); + + OnRoomStartClient(); + } + + /// + /// Called on the client when connected to a server. + /// The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects. + /// + public override void OnClientConnect() + { +#pragma warning disable 618 + // obsolete method calls new method + OnRoomClientConnect(NetworkClient.connection); +#pragma warning restore 618 + base.OnClientConnect(); + } + + /// + /// Called on clients when disconnected from a server. + /// This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects. + /// + public override void OnClientDisconnect() + { +#pragma warning disable 618 + OnRoomClientDisconnect(NetworkClient.connection); +#pragma warning restore 618 + base.OnClientDisconnect(); + } + + /// + /// This is called when a client is stopped. + /// + public override void OnStopClient() + { + OnRoomStopClient(); + CallOnClientExitRoom(); + roomSlots.Clear(); + } + + /// + /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. + /// Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists. + /// + public override void OnClientSceneChanged() + { + if (IsSceneActive(RoomScene)) + { + if (NetworkClient.isConnected) + CallOnClientEnterRoom(); + } + else + CallOnClientExitRoom(); + + base.OnClientSceneChanged(); +#pragma warning disable 618 + // obsolete method calls new method + OnRoomClientSceneChanged(NetworkClient.connection); +#pragma warning restore 618 + } + + #endregion + + #region room server virtuals + + /// + /// This is called on the host when a host is started. + /// + public virtual void OnRoomStartHost() {} + + /// + /// This is called on the host when the host is stopped. + /// + public virtual void OnRoomStopHost() {} + + /// + /// This is called on the server when the server is started - including when a host is started. + /// + public virtual void OnRoomStartServer() {} + + /// + /// This is called on the server when the server is started - including when a host is stopped. + /// + public virtual void OnRoomStopServer() {} + + /// + /// This is called on the server when a new client connects to the server. + /// + /// The new connection. + public virtual void OnRoomServerConnect(NetworkConnectionToClient conn) {} + + /// + /// This is called on the server when a client disconnects. + /// + /// The connection that disconnected. + public virtual void OnRoomServerDisconnect(NetworkConnectionToClient conn) {} + + /// + /// This is called on the server when a networked scene finishes loading. + /// + /// Name of the new scene. + public virtual void OnRoomServerSceneChanged(string sceneName) {} + + /// + /// This allows customization of the creation of the room-player object on the server. + /// By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized. + /// + /// The connection the player object is for. + /// The new room-player object. + public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn) + { + return null; + } + + /// + /// This allows customization of the creation of the GamePlayer object on the server. + /// By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection. + /// + /// The connection the player object is for. + /// The room player object for this connection. + /// A new GamePlayer object. + public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer) + { + return null; + } + + /// + /// This allows customization of the creation of the GamePlayer object on the server. + /// This is only called for subsequent GamePlay scenes after the first one. + /// See OnRoomServerCreateGamePlayer(NetworkConnection, GameObject) to customize the player object for the initial GamePlay scene. + /// + /// The connection the player object is for. + public virtual void OnRoomServerAddPlayer(NetworkConnectionToClient conn) + { + base.OnServerAddPlayer(conn); + } + + // for users to apply settings from their room player object to their in-game player object + /// + /// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene. + /// When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object. + /// + /// The connection of the player + /// The room player object. + /// The game player object. + /// False to not allow this player to replace the room player. + public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer) + { + return true; + } + + /// + /// This is called on the server when all the players in the room are ready. + /// The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader. + /// + public virtual void OnRoomServerPlayersReady() + { + // all players are readyToBegin, start the game + ServerChangeScene(GameplayScene); + } + + /// + /// This is called on the server when CheckReadyToBegin finds that players are not ready + /// May be called multiple times while not ready players are joining + /// + public virtual void OnRoomServerPlayersNotReady() {} + + #endregion + + #region room client virtuals + + /// + /// This is a hook to allow custom behaviour when the game client enters the room. + /// + public virtual void OnRoomClientEnter() {} + + /// + /// This is a hook to allow custom behaviour when the game client exits the room. + /// + public virtual void OnRoomClientExit() {} + + /// + /// This is called on the client when it connects to server. + /// + public virtual void OnRoomClientConnect() {} + + // Deprecated 2021-10-30 + [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] + public virtual void OnRoomClientConnect(NetworkConnection conn) => OnRoomClientConnect(); + + /// + /// This is called on the client when disconnected from a server. + /// + public virtual void OnRoomClientDisconnect() {} + + // Deprecated 2021-10-30 + [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] + public virtual void OnRoomClientDisconnect(NetworkConnection conn) => OnRoomClientDisconnect(); + + /// + /// This is called on the client when a client is started. + /// + public virtual void OnRoomStartClient() {} + + /// + /// This is called on the client when the client stops. + /// + public virtual void OnRoomStopClient() {} + + /// + /// This is called on the client when the client is finished loading a new networked scene. + /// + public virtual void OnRoomClientSceneChanged() {} + + // Deprecated 2021-10-30 + [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] + public virtual void OnRoomClientSceneChanged(NetworkConnection conn) => OnRoomClientSceneChanged(); + + /// + /// Called on the client when adding a player to the room fails. + /// This could be because the room is full, or the connection is not allowed to have more players. + /// + public virtual void OnRoomClientAddPlayerFailed() {} + + #endregion + + #region optional UI + + /// + /// virtual so inheriting classes can roll their own + /// + public virtual void OnGUI() + { + if (!showRoomGUI) + return; + + if (NetworkServer.active && IsSceneActive(GameplayScene)) + { + GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f)); + if (GUILayout.Button("Return to Room")) + ServerChangeScene(RoomScene); + GUILayout.EndArea(); + } + + if (IsSceneActive(RoomScene)) + GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS"); + } + + #endregion + } +} diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs.meta b/Assets/Mirror/Components/NetworkRoomManager.cs.meta new file mode 100644 index 0000000..76e7d42 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRoomManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 615e6c6589cf9e54cad646b5a11e0529 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs new file mode 100644 index 0000000..d2763d5 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs @@ -0,0 +1,195 @@ +using UnityEngine; + +namespace Mirror +{ + /// + /// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system. + /// The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Room Player")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")] + public class NetworkRoomPlayer : NetworkBehaviour + { + /// + /// This flag controls whether the default UI is shown for the room player. + /// As this UI is rendered using the old GUI system, it is only recommended for testing purposes. + /// + [Tooltip("This flag controls whether the default UI is shown for the room player")] + public bool showRoomGUI = true; + + [Header("Diagnostics")] + + /// + /// Diagnostic flag indicating whether this player is ready for the game to begin. + /// Invoke CmdChangeReadyState method on the client to set this flag. + /// When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server. + /// + [Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")] + [SyncVar(hook = nameof(ReadyStateChanged))] + public bool readyToBegin; + + /// + /// Diagnostic index of the player, e.g. Player1, Player2, etc. + /// + [Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")] + [SyncVar(hook = nameof(IndexChanged))] + public int index; + + #region Unity Callbacks + + /// + /// Do not use Start - Override OnStartHost / OnStartClient instead! + /// + public void Start() + { + if (NetworkManager.singleton is NetworkRoomManager room) + { + // NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager + // in server and all clients, otherwise it will be respawned in the game scene which would + // have undesirable effects. + if (room.dontDestroyOnLoad) + DontDestroyOnLoad(gameObject); + + room.roomSlots.Add(this); + + if (NetworkServer.active) + room.RecalculateRoomPlayerIndices(); + + if (NetworkClient.active) + room.CallOnClientEnterRoom(); + } + else Debug.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene."); + } + + public virtual void OnDisable() + { + if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room) + { + // only need to call this on client as server removes it before object is destroyed + room.roomSlots.Remove(this); + + room.CallOnClientExitRoom(); + } + } + + #endregion + + #region Commands + + [Command] + public void CmdChangeReadyState(bool readyState) + { + readyToBegin = readyState; + NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager; + if (room != null) + { + room.ReadyStatusChanged(); + } + } + + #endregion + + #region SyncVar Hooks + + /// + /// This is a hook that is invoked on clients when the index changes. + /// + /// The old index value + /// The new index value + public virtual void IndexChanged(int oldIndex, int newIndex) {} + + /// + /// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready. + /// This function is called when the a client player calls CmdChangeReadyState. + /// + /// New Ready State + public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) {} + + #endregion + + #region Room Client Virtuals + + /// + /// This is a hook that is invoked on clients for all room player objects when entering the room. + /// Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called. + /// + public virtual void OnClientEnterRoom() {} + + /// + /// This is a hook that is invoked on clients for all room player objects when exiting the room. + /// + public virtual void OnClientExitRoom() {} + + #endregion + + #region Optional UI + + /// + /// Render a UI for the room. Override to provide your own UI + /// + public virtual void OnGUI() + { + if (!showRoomGUI) + return; + + NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager; + if (room) + { + if (!room.showRoomGUI) + return; + + if (!NetworkManager.IsSceneActive(room.RoomScene)) + return; + + DrawPlayerReadyState(); + DrawPlayerReadyButton(); + } + } + + void DrawPlayerReadyState() + { + GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f)); + + GUILayout.Label($"Player [{index + 1}]"); + + if (readyToBegin) + GUILayout.Label("Ready"); + else + GUILayout.Label("Not Ready"); + + if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE")) + { + // This button only shows on the Host for all players other than the Host + // Host and Players can't remove themselves (stop the client instead) + // Host can kick a Player this way. + GetComponent().connectionToClient.Disconnect(); + } + + GUILayout.EndArea(); + } + + void DrawPlayerReadyButton() + { + if (NetworkClient.active && isLocalPlayer) + { + GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f)); + + if (readyToBegin) + { + if (GUILayout.Button("Cancel")) + CmdChangeReadyState(false); + } + else + { + if (GUILayout.Button("Ready")) + CmdChangeReadyState(true); + } + + GUILayout.EndArea(); + } + } + + #endregion + } +} diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta new file mode 100644 index 0000000..0299bea --- /dev/null +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 79874ac94d5b1314788ecf0e86bd23fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs new file mode 100644 index 0000000..a95d4a9 --- /dev/null +++ b/Assets/Mirror/Components/NetworkStatistics.cs @@ -0,0 +1,192 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// Shows Network messages and bytes sent & received per second. + /// Add this component to the same object as Network Manager. + /// + [AddComponentMenu("Network/Network Statistics")] + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")] + public class NetworkStatistics : MonoBehaviour + { + // update interval + double intervalStartTime; + + // --------------------------------------------------------------------- + + // CLIENT + // long bytes to support >2GB + int clientIntervalReceivedPackets; + long clientIntervalReceivedBytes; + int clientIntervalSentPackets; + long clientIntervalSentBytes; + + // results from last interval + // long bytes to support >2GB + int clientReceivedPacketsPerSecond; + long clientReceivedBytesPerSecond; + int clientSentPacketsPerSecond; + long clientSentBytesPerSecond; + + // --------------------------------------------------------------------- + + // SERVER + // capture interval + // long bytes to support >2GB + int serverIntervalReceivedPackets; + long serverIntervalReceivedBytes; + int serverIntervalSentPackets; + long serverIntervalSentBytes; + + // results from last interval + // long bytes to support >2GB + int serverReceivedPacketsPerSecond; + long serverReceivedBytesPerSecond; + int serverSentPacketsPerSecond; + long serverSentBytesPerSecond; + + // NetworkManager sets Transport.activeTransport in Awake(). + // so let's hook into it in Start(). + void Start() + { + // find available transport + Transport transport = Transport.activeTransport; + if (transport != null) + { + transport.OnClientDataReceived += OnClientReceive; + transport.OnClientDataSent += OnClientSend; + transport.OnServerDataReceived += OnServerReceive; + transport.OnServerDataSent += OnServerSend; + } + else Debug.LogError($"NetworkStatistics: no available or active Transport found on this platform: {Application.platform}"); + } + + void OnDestroy() + { + // remove transport hooks + Transport transport = Transport.activeTransport; + if (transport != null) + { + transport.OnClientDataReceived -= OnClientReceive; + transport.OnClientDataSent -= OnClientSend; + transport.OnServerDataReceived -= OnServerReceive; + transport.OnServerDataSent -= OnServerSend; + } + } + + void OnClientReceive(ArraySegment data, int channelId) + { + ++clientIntervalReceivedPackets; + clientIntervalReceivedBytes += data.Count; + } + + void OnClientSend(ArraySegment data, int channelId) + { + ++clientIntervalSentPackets; + clientIntervalSentBytes += data.Count; + } + + void OnServerReceive(int connectionId, ArraySegment data, int channelId) + { + ++serverIntervalReceivedPackets; + serverIntervalReceivedBytes += data.Count; + } + + void OnServerSend(int connectionId, ArraySegment data, int channelId) + { + ++serverIntervalSentPackets; + serverIntervalSentBytes += data.Count; + } + + void Update() + { + // calculate results every second + if (NetworkTime.localTime >= intervalStartTime + 1) + { + if (NetworkClient.active) UpdateClient(); + if (NetworkServer.active) UpdateServer(); + + intervalStartTime = NetworkTime.localTime; + } + } + + void UpdateClient() + { + clientReceivedPacketsPerSecond = clientIntervalReceivedPackets; + clientReceivedBytesPerSecond = clientIntervalReceivedBytes; + clientSentPacketsPerSecond = clientIntervalSentPackets; + clientSentBytesPerSecond = clientIntervalSentBytes; + + clientIntervalReceivedPackets = 0; + clientIntervalReceivedBytes = 0; + clientIntervalSentPackets = 0; + clientIntervalSentBytes = 0; + } + + void UpdateServer() + { + serverReceivedPacketsPerSecond = serverIntervalReceivedPackets; + serverReceivedBytesPerSecond = serverIntervalReceivedBytes; + serverSentPacketsPerSecond = serverIntervalSentPackets; + serverSentBytesPerSecond = serverIntervalSentBytes; + + serverIntervalReceivedPackets = 0; + serverIntervalReceivedBytes = 0; + serverIntervalSentPackets = 0; + serverIntervalSentBytes = 0; + } + + void OnGUI() + { + // only show if either server or client active + if (NetworkClient.active || NetworkServer.active) + { + // create main GUI area + // 105 is below NetworkManager HUD in all cases. + GUILayout.BeginArea(new Rect(10, 105, 215, 300)); + + // show client / server stats if active + if (NetworkClient.active) OnClientGUI(); + if (NetworkServer.active) OnServerGUI(); + + // end of GUI area + GUILayout.EndArea(); + } + } + + void OnClientGUI() + { + // background + GUILayout.BeginVertical("Box"); + GUILayout.Label("Client Statistics"); + + // sending ("msgs" instead of "packets" to fit larger numbers) + GUILayout.Label($"Send: {clientSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientSentBytesPerSecond)}/s"); + + // receiving ("msgs" instead of "packets" to fit larger numbers) + GUILayout.Label($"Recv: {clientReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientReceivedBytesPerSecond)}/s"); + + // end background + GUILayout.EndVertical(); + } + + void OnServerGUI() + { + // background + GUILayout.BeginVertical("Box"); + GUILayout.Label("Server Statistics"); + + // sending ("msgs" instead of "packets" to fit larger numbers) + GUILayout.Label($"Send: {serverSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverSentBytesPerSecond)}/s"); + + // receiving ("msgs" instead of "packets" to fit larger numbers) + GUILayout.Label($"Recv: {serverReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s"); + + // end background + GUILayout.EndVertical(); + } + } +} diff --git a/Assets/Mirror/Components/NetworkStatistics.cs.meta b/Assets/Mirror/Components/NetworkStatistics.cs.meta new file mode 100644 index 0000000..0bf4251 --- /dev/null +++ b/Assets/Mirror/Components/NetworkStatistics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d7da4e566d24ea7b0e12178d934b648 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform2k.meta b/Assets/Mirror/Components/NetworkTransform2k.meta new file mode 100644 index 0000000..fe99bf0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 44e823b93c7d2477c8796766dc364c59 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs new file mode 100644 index 0000000..b7b8e81 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs @@ -0,0 +1,17 @@ +// ʻOumuamua's light curve, assuming little systematic error, presents its +// motion as tumbling, rather than smoothly rotating, and moving sufficiently +// fast relative to the Sun. +// +// A small number of astronomers suggested that ʻOumuamua could be a product of +// alien technology, but evidence in support of this hypothesis is weak. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Transform")] + public class NetworkTransform : NetworkTransformBase + { + protected override Transform targetComponent => transform; + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta new file mode 100644 index 0000000..a569990 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f74aedd71d9a4f55b3ce499326d45fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs new file mode 100644 index 0000000..54e77a7 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs @@ -0,0 +1,776 @@ +// NetworkTransform V2 aka project Oumuamua by vis2k (2021-07) +// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ +// +// Base class for NetworkTransform and NetworkTransformChild. +// => simple unreliable sync without any interpolation for now. +// => which means we don't need teleport detection either +// +// NOTE: several functions are virtual in case someone needs to modify a part. +// +// Channel: uses UNRELIABLE at all times. +// -> out of order packets are dropped automatically +// -> it's better than RELIABLE for several reasons: +// * head of line blocking would add delay +// * resending is mostly pointless +// * bigger data race: +// -> if we use a Cmd() at position X over reliable +// -> client gets Cmd() and X at the same time, but buffers X for bufferTime +// -> for unreliable, it would get X before the reliable Cmd(), still +// buffer for bufferTime but end up closer to the original time +// comment out the below line to quickly revert the onlySyncOnChange feature +#define onlySyncOnChange_BANDWIDTH_SAVING +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public abstract class NetworkTransformBase : NetworkBehaviour + { + // TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier? + [Header("Authority")] + [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] + public bool clientAuthority; + + // Is this a client with authority over this transform? + // This component could be on the player object or any object that has been assigned authority to this client. + protected bool IsClientWithAuthority => hasAuthority && clientAuthority; + + // target transform to sync. can be on a child. + protected abstract Transform targetComponent { get; } + + [Header("Synchronization")] + [Range(0, 1)] public float sendInterval = 0.050f; + public bool syncPosition = true; + public bool syncRotation = true; + // scale sync is rare. off by default. + public bool syncScale = false; + + double lastClientSendTime; + double lastServerSendTime; + + // not all games need to interpolate. a board game might jump to the + // final position immediately. + [Header("Interpolation")] + public bool interpolatePosition = true; + public bool interpolateRotation = true; + public bool interpolateScale = false; + + // "Experimentally I’ve found that the amount of delay that works best + // at 2-5% packet loss is 3X the packet send rate" + // NOTE: we do NOT use a dyanmically changing buffer size. + // it would come with a lot of complications, e.g. buffer time + // advantages/disadvantages for different connections. + // Glenn Fiedler's recommendation seems solid, and should cover + // the vast majority of connections. + // (a player with 2000ms latency will have issues no matter what) + [Header("Buffering")] + [Tooltip("Snapshots are buffered for sendInterval * multiplier seconds. If your expected client base is to run at non-ideal connection quality (2-5% packet loss), 3x supposedly works best.")] + public int bufferTimeMultiplier = 1; + public float bufferTime => sendInterval * bufferTimeMultiplier; + [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] + public int bufferSizeLimit = 64; + + [Tooltip("Start to accelerate interpolation if buffer size is >= threshold. Needs to be larger than bufferTimeMultiplier.")] + public int catchupThreshold = 4; + + [Tooltip("Once buffer is larger catchupThreshold, accelerate by multiplier % per excess entry.")] + [Range(0, 1)] public float catchupMultiplier = 0.10f; + +#if onlySyncOnChange_BANDWIDTH_SAVING + [Header("Sync Only If Changed")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + + // 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching. + [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")] + public float bufferResetMultiplier = 5; + + [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float positionSensitivity = 0.01f; + public float rotationSensitivity = 0.01f; + public float scaleSensitivity = 0.01f; + + protected bool positionChanged; + protected bool rotationChanged; + protected bool scaleChanged; + + // Used to store last sent snapshots + protected NTSnapshot lastSnapshot; + protected bool cachedSnapshotComparison; + protected bool hasSentUnchangedPosition; +#endif + + // snapshots sorted by timestamp + // in the original article, glenn fiedler drops any snapshots older than + // the last received snapshot. + // -> instead, we insert into a sorted buffer + // -> the higher the buffer information density, the better + // -> we still drop anything older than the first element in the buffer + // => internal for testing + // + // IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot' + // interface because List allocates through boxing + internal SortedList serverBuffer = new SortedList(); + internal SortedList clientBuffer = new SortedList(); + + // absolute interpolation time, moved along with deltaTime + // (roughly between [0, delta] where delta is snapshot B - A timestamp) + // (can be bigger than delta when overshooting) + double serverInterpolationTime; + double clientInterpolationTime; + + // only convert the static Interpolation function to Func once to + // avoid allocations + Func Interpolate = NTSnapshot.Interpolate; + + [Header("Debug")] + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + // snapshot functions ////////////////////////////////////////////////// + // construct a snapshot of the current state + // => internal for testing + protected virtual NTSnapshot ConstructSnapshot() + { + // NetworkTime.localTime for double precision until Unity has it too + return new NTSnapshot( + // our local time is what the other end uses as remote time + NetworkTime.localTime, + // the other end fills out local time itself + 0, + targetComponent.localPosition, + targetComponent.localRotation, + targetComponent.localScale + ); + } + + // apply a snapshot to the Transform. + // -> start, end, interpolated are all passed in caes they are needed + // -> a regular game would apply the 'interpolated' snapshot + // -> a board game might want to jump to 'goal' directly + // (it's easier to always interpolate and then apply selectively, + // instead of manually interpolating x, y, z, ... depending on flags) + // => internal for testing + // + // NOTE: stuck detection is unnecessary here. + // we always set transform.position anyway, we can't get stuck. + protected virtual void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated) + { + // local position/rotation for VR support + // + // if syncPosition/Rotation/Scale is disabled then we received nulls + // -> current position/rotation/scale would've been added as snapshot + // -> we still interpolated + // -> but simply don't apply it. if the user doesn't want to sync + // scale, then we should not touch scale etc. + if (syncPosition) + targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position; + + if (syncRotation) + targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation; + + if (syncScale) + targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale; + } +#if onlySyncOnChange_BANDWIDTH_SAVING + // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot) + { + positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; + rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; + scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity; + + return (!positionChanged && !rotationChanged && !scaleChanged); + } +#endif + // cmd ///////////////////////////////////////////////////////////////// + // only unreliable. see comment above of this file. + [Command(channel = Channels.Unreliable)] + void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + OnClientToServerSync(position, rotation, scale); + //For client authority, immediately pass on the client snapshot to all other + //clients instead of waiting for server to send its snapshots. + if (clientAuthority) + { + RpcServerToClientSync(position, rotation, scale); + } + } + + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (!clientAuthority) return; + + // protect against ever growing buffer size attacks + if (serverBuffer.Count >= bufferSizeLimit) return; + + // only player owned objects (with a connection) can send to + // server. we can get the timestamp from the connection. + double timestamp = connectionToClient.remoteTimeStamp; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * sendInterval; + + if (serverBuffer.Count > 0 && serverBuffer.Values[serverBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) + { + Reset(); + } + } +#endif + // position, rotation, scale can have no value if same as last time. + // saves bandwidth. + // but we still need to feed it to snapshot interpolation. we can't + // just have gaps in there if nothing has changed. for example, if + // client sends snapshot at t=0 + // client sends nothing for 10s because not moved + // client sends snapshot at t=10 + // then the server would assume that it's one super slow move and + // replay it for 10 seconds. + if (!position.HasValue) position = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].position : targetComponent.localPosition; + if (!rotation.HasValue) rotation = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].rotation : targetComponent.localRotation; + if (!scale.HasValue) scale = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].scale : targetComponent.localScale; + + // construct snapshot with batch timestamp to save bandwidth + NTSnapshot snapshot = new NTSnapshot( + timestamp, + NetworkTime.localTime, + position.Value, rotation.Value, scale.Value + ); + + // add to buffer (or drop if older than first element) + SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer); + } + + // rpc ///////////////////////////////////////////////////////////////// + // only unreliable. see comment above of this file. + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) => + OnServerToClientSync(position, rotation, scale); + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // in host mode, the server sends rpcs to all clients. + // the host client itself will receive them too. + // -> host server is always the source of truth + // -> we can ignore any rpc on the host client + // => otherwise host objects would have ever growing clientBuffers + // (rpc goes to clients. if isServer is true too then we are host) + if (isServer) return; + + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // protect against ever growing buffer size attacks + if (clientBuffer.Count >= bufferSizeLimit) return; + + // on the client, we receive rpcs for all entities. + // not all of them have a connectionToServer. + // but all of them go through NetworkClient.connection. + // we can get the timestamp from there. + double timestamp = NetworkClient.connection.remoteTimeStamp; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * sendInterval; + + if (clientBuffer.Count > 0 && clientBuffer.Values[clientBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) + { + Reset(); + } + } +#endif + // position, rotation, scale can have no value if same as last time. + // saves bandwidth. + // but we still need to feed it to snapshot interpolation. we can't + // just have gaps in there if nothing has changed. for example, if + // client sends snapshot at t=0 + // client sends nothing for 10s because not moved + // client sends snapshot at t=10 + // then the server would assume that it's one super slow move and + // replay it for 10 seconds. + if (!position.HasValue) position = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].position : targetComponent.localPosition; + if (!rotation.HasValue) rotation = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].rotation : targetComponent.localRotation; + if (!scale.HasValue) scale = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].scale : targetComponent.localScale; + + // construct snapshot with batch timestamp to save bandwidth + NTSnapshot snapshot = new NTSnapshot( + timestamp, + NetworkTime.localTime, + position.Value, rotation.Value, scale.Value + ); + + // add to buffer (or drop if older than first element) + SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer); + } + + // update ////////////////////////////////////////////////////////////// + void UpdateServer() + { + // broadcast to all clients each 'sendInterval' + // (client with authority will drop the rpc) + // NetworkTime.localTime for double precision until Unity has it too + // + // IMPORTANT: + // snapshot interpolation requires constant sending. + // DO NOT only send if position changed. for example: + // --- + // * client sends first position at t=0 + // * ... 10s later ... + // * client moves again, sends second position at t=10 + // --- + // * server gets first position at t=0 + // * server gets second position at t=10 + // * server moves from first to second within a time of 10s + // => would be a super slow move, instead of a wait & move. + // + // IMPORTANT: + // DO NOT send nulls if not changed 'since last send' either. we + // send unreliable and don't know which 'last send' the other end + // received successfully. + // + // Checks to ensure server only sends snapshots if object is + // on server authority(!clientAuthority) mode because on client + // authority mode snapshots are broadcasted right after the authoritative + // client updates server in the command function(see above), OR, + // since host does not send anything to update the server, any client + // authoritative movement done by the host will have to be broadcasted + // here by checking IsClientWithAuthority. + if (NetworkTime.localTime >= lastServerSendTime + sendInterval && + (!clientAuthority || IsClientWithAuthority)) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + NTSnapshot snapshot = ConstructSnapshot(); +#if onlySyncOnChange_BANDWIDTH_SAVING + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } +#endif + +#if onlySyncOnChange_BANDWIDTH_SAVING + RpcServerToClientSync( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); +#else + RpcServerToClientSync( + // only sync what the user wants to sync + syncPosition ? snapshot.position : default(Vector3?), + syncRotation ? snapshot.rotation : default(Quaternion?), + syncScale ? snapshot.scale : default(Vector3?) + ); +#endif + + lastServerSendTime = NetworkTime.localTime; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + lastSnapshot = snapshot; + } +#endif + } + + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> don't apply for host mode player objects either, even if in + // client authority mode. if it doesn't go over the network, + // then we don't need to do anything. + if (clientAuthority && !hasAuthority) + { + // compute snapshot interpolation & apply if any was spit out + // TODO we don't have Time.deltaTime double yet. float is fine. + if (SnapshotInterpolation.Compute( + NetworkTime.localTime, Time.deltaTime, + ref serverInterpolationTime, + bufferTime, serverBuffer, + catchupThreshold, catchupMultiplier, + Interpolate, + out NTSnapshot computed)) + { + NTSnapshot start = serverBuffer.Values[0]; + NTSnapshot goal = serverBuffer.Values[1]; + ApplySnapshot(start, goal, computed); + } + } + } + + void UpdateClient() + { + // client authority, and local player (= allowed to move myself)? + if (IsClientWithAuthority) + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // send to server each 'sendInterval' + // NetworkTime.localTime for double precision until Unity has it too + // + // IMPORTANT: + // snapshot interpolation requires constant sending. + // DO NOT only send if position changed. for example: + // --- + // * client sends first position at t=0 + // * ... 10s later ... + // * client moves again, sends second position at t=10 + // --- + // * server gets first position at t=0 + // * server gets second position at t=10 + // * server moves from first to second within a time of 10s + // => would be a super slow move, instead of a wait & move. + // + // IMPORTANT: + // DO NOT send nulls if not changed 'since last send' either. we + // send unreliable and don't know which 'last send' the other end + // received successfully. + if (NetworkTime.localTime >= lastClientSendTime + sendInterval) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + NTSnapshot snapshot = ConstructSnapshot(); +#if onlySyncOnChange_BANDWIDTH_SAVING + cachedSnapshotComparison = CompareSnapshots(snapshot); + if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } +#endif + +#if onlySyncOnChange_BANDWIDTH_SAVING + CmdClientToServerSync( + // only sync what the user wants to sync + syncPosition && positionChanged ? snapshot.position : default(Vector3?), + syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), + syncScale && scaleChanged ? snapshot.scale : default(Vector3?) + ); +#else + CmdClientToServerSync( + // only sync what the user wants to sync + syncPosition ? snapshot.position : default(Vector3?), + syncRotation ? snapshot.rotation : default(Quaternion?), + syncScale ? snapshot.scale : default(Vector3?) + ); +#endif + + lastClientSendTime = NetworkTime.localTime; +#if onlySyncOnChange_BANDWIDTH_SAVING + if (cachedSnapshotComparison) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + lastSnapshot = snapshot; + } +#endif + } + } + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer + else + { + // compute snapshot interpolation & apply if any was spit out + // TODO we don't have Time.deltaTime double yet. float is fine. + if (SnapshotInterpolation.Compute( + NetworkTime.localTime, Time.deltaTime, + ref clientInterpolationTime, + bufferTime, clientBuffer, + catchupThreshold, catchupMultiplier, + Interpolate, + out NTSnapshot computed)) + { + NTSnapshot start = clientBuffer.Values[0]; + NTSnapshot goal = clientBuffer.Values[1]; + ApplySnapshot(start, goal, computed); + } + } + } + + void Update() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + targetComponent.position = destination; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destionation as first entry? + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) + { + // reset any in-progress interpolation & buffers + Reset(); + + // set the new position. + // interpolation will automatically continue. + targetComponent.position = destination; + targetComponent.rotation = rotation; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destionation as first entry? + } + + // server->client teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination); + } + + // server->client teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [ClientRpc] + public void RpcTeleport(Vector3 destination, Quaternion rotation) + { + // NOTE: even in client authority mode, the server is always allowed + // to teleport the player. for example: + // * CmdEnterPortal() might teleport the player + // * Some people use client authority with server sided checks + // so the server should be able to reset position if needed. + + // TODO what about host mode? + OnTeleport(destination, rotation); + } + + // Deprecated 2022-01-19 + [Obsolete("Use RpcTeleport(Vector3, Quaternion) instead.")] + [ClientRpc] + public void RpcTeleportAndRotate(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + } + + // client->server teleport to force position without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination) + { + // client can only teleport objects that it has authority over. + if (!clientAuthority) return; + + // TODO what about host mode? + OnTeleport(destination); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and targetComponent.position=pos + RpcTeleport(destination); + } + + // client->server teleport to force position and rotation without interpolation. + // otherwise it would interpolate to a (far away) new position. + // => manually calling Teleport is the only 100% reliable solution. + [Command] + public void CmdTeleport(Vector3 destination, Quaternion rotation) + { + // client can only teleport objects that it has authority over. + if (!clientAuthority) return; + + // TODO what about host mode? + OnTeleport(destination, rotation); + + // if a client teleports, we need to broadcast to everyone else too + // TODO the teleported client should ignore the rpc though. + // otherwise if it already moved again after teleporting, + // the rpc would come a little bit later and reset it once. + // TODO or not? if client ONLY calls Teleport(pos), the position + // would only be set after the rpc. unless the client calls + // BOTH Teleport(pos) and targetComponent.position=pos + RpcTeleport(destination, rotation); + } + + // Deprecated 2022-01-19 + [Obsolete("Use CmdTeleport(Vector3, Quaternion) instead.")] + [Command] + public void CmdTeleportAndRotate(Vector3 destination, Quaternion rotation) + { + if (!clientAuthority) return; + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + public virtual void Reset() + { + // disabled objects aren't updated anymore. + // so let's clear the buffers. + serverBuffer.Clear(); + clientBuffer.Clear(); + + // reset interpolation time too so we start at t=0 next time + serverInterpolationTime = 0; + clientInterpolationTime = 0; + } + + protected virtual void OnDisable() => Reset(); + protected virtual void OnEnable() => Reset(); + + protected virtual void OnValidate() + { + // make sure that catchup threshold is > buffer multiplier. + // for a buffer multiplier of '3', we usually have at _least_ 3 + // buffered snapshots. often 4-5 even. + // + // catchUpThreshold should be a minimum of bufferTimeMultiplier + 3, + // to prevent clashes with SnapshotInterpolation looking for at least + // 3 old enough buffers, else catch up will be implemented while there + // is not enough old buffers, and will result in jitter. + // (validated with several real world tests by ninja & imer) + catchupThreshold = Mathf.Max(bufferTimeMultiplier + 3, catchupThreshold); + + // buffer limit should be at least multiplier to have enough in there + bufferSizeLimit = Mathf.Max(bufferTimeMultiplier, bufferSizeLimit); + } + + public override bool OnSerialize(NetworkWriter writer, bool initialState) + { + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + if (syncPosition) writer.WriteVector3(targetComponent.localPosition); + if (syncRotation) writer.WriteQuaternion(targetComponent.localRotation); + if (syncScale) writer.WriteVector3(targetComponent.localScale); + return true; + } + return false; + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // sync target component's position on spawn. + // fixes https://github.com/vis2k/Mirror/pull/3051/ + // (Spawn message wouldn't sync NTChild positions either) + if (initialState) + { + if (syncPosition) targetComponent.localPosition = reader.ReadVector3(); + if (syncRotation) targetComponent.localRotation = reader.ReadQuaternion(); + if (syncScale) targetComponent.localScale = reader.ReadVector3(); + } + } + + // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // debug /////////////////////////////////////////////////////////////// + protected virtual void OnGUI() + { + if (!showOverlay) return; + + // show data next to player for easier debugging. this is very useful! + // IMPORTANT: this is basically an ESP hack for shooter games. + // DO NOT make this available with a hotkey in release builds + if (!Debug.isDebugBuild) return; + + // project position to screen + Vector3 point = Camera.main.WorldToScreenPoint(targetComponent.position); + + // enough alpha, in front of camera and in screen? + if (point.z >= 0 && Utils.IsPointInScreen(point)) + { + // catchup is useful to show too + int serverBufferExcess = Mathf.Max(serverBuffer.Count - catchupThreshold, 0); + int clientBufferExcess = Mathf.Max(clientBuffer.Count - catchupThreshold, 0); + float serverCatchup = serverBufferExcess * catchupMultiplier; + float clientCatchup = clientBufferExcess * catchupMultiplier; + + GUI.color = overlayColor; + GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); + + // always show both client & server buffers so it's super + // obvious if we accidentally populate both. + GUILayout.Label($"Server Buffer:{serverBuffer.Count}"); + if (serverCatchup > 0) + GUILayout.Label($"Server Catchup:{serverCatchup * 100:F2}%"); + + GUILayout.Label($"Client Buffer:{clientBuffer.Count}"); + if (clientCatchup > 0) + GUILayout.Label($"Client Catchup:{clientCatchup * 100:F2}%"); + + GUILayout.EndArea(); + GUI.color = Color.white; + } + } + + protected virtual void DrawGizmos(SortedList buffer) + { + // only draw if we have at least two entries + if (buffer.Count < 2) return; + + // calcluate threshold for 'old enough' snapshots + double threshold = NetworkTime.localTime - bufferTime; + Color oldEnoughColor = new Color(0, 1, 0, 0.5f); + Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); + + // draw the whole buffer for easier debugging. + // it's worth seeing how much we have buffered ahead already + for (int i = 0; i < buffer.Count; ++i) + { + // color depends on if old enough or not + NTSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTimestamp <= threshold; + Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; + Gizmos.DrawCube(entry.position, Vector3.one); + } + + // extra: lines between start<->position<->goal + Gizmos.color = Color.green; + Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position); + Gizmos.color = Color.white; + Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position); + } + + protected virtual void OnDrawGizmos() + { + // This fires in edit mode but that spams NRE's so check isPlaying + if (!Application.isPlaying) return; + if (!showGizmos) return; + + if (isServer) DrawGizmos(serverBuffer); + if (isClient) DrawGizmos(clientBuffer); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta new file mode 100644 index 0000000..ab649d9 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e77294d8ccbc4e7cb8ca2bd0d3e99ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs new file mode 100644 index 0000000..8032506 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs @@ -0,0 +1,14 @@ +// A component to synchronize the position of child transforms of networked objects. +// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values. +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform Child")] + public class NetworkTransformChild : NetworkTransformBase + { + [Header("Target")] + public Transform target; + protected override Transform targetComponent => target; + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta new file mode 100644 index 0000000..ae36756 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 734b48bea0b204338958ee3d885e11f0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs new file mode 100644 index 0000000..efd91c0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs @@ -0,0 +1,64 @@ +// snapshot for snapshot interpolation +// https://gafferongames.com/post/snapshot_interpolation/ +// position, rotation, scale for compatibility for now. +using UnityEngine; + +namespace Mirror +{ + // NetworkTransform Snapshot + public struct NTSnapshot : Snapshot + { + // time or sequence are needed to throw away older snapshots. + // + // glenn fiedler starts with a 16 bit sequence number. + // supposedly this is meant as a simplified example. + // in the end we need the remote timestamp for accurate interpolation + // and buffering over time. + // + // note: in theory, IF server sends exactly(!) at the same interval then + // the 16 bit ushort timestamp would be enough to calculate the + // remote time (sequence * sendInterval). but Unity's update is + // not guaranteed to run on the exact intervals / do catchup. + // => remote timestamp is better for now + // + // [REMOTE TIME, NOT LOCAL TIME] + // => DOUBLE for long term accuracy & batching gives us double anyway + public double remoteTimestamp { get; set; } + // the local timestamp (when we received it) + // used to know if the first two snapshots are old enough to start. + public double localTimestamp { get; set; } + + public Vector3 position; + public Quaternion rotation; + public Vector3 scale; + + public NTSnapshot(double remoteTimestamp, double localTimestamp, Vector3 position, Quaternion rotation, Vector3 scale) + { + this.remoteTimestamp = remoteTimestamp; + this.localTimestamp = localTimestamp; + this.position = position; + this.rotation = rotation; + this.scale = scale; + } + + public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t) + { + // NOTE: + // Vector3 & Quaternion components are float anyway, so we can + // keep using the functions with 't' as float instead of double. + return new NTSnapshot( + // interpolated snapshot is applied directly. don't need timestamps. + 0, 0, + // lerp position/rotation/scale unclamped in case we ever need + // to extrapolate. atm SnapshotInterpolation never does. + Vector3.LerpUnclamped(from.position, to.position, (float)t), + // IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86. + // SlerpUnclamped(0, 60, 1.5) extrapolates to 90! + // (0, 90, 1.5) is even worse. for Lerp. + // => Slerp works way better for our euler angles. + Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t), + Vector3.LerpUnclamped(from.scale, to.scale, (float)t) + ); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta new file mode 100644 index 0000000..f43458f --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3dae77b43dc4e1dbb2012924b2da79c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor.meta b/Assets/Mirror/Editor.meta new file mode 100644 index 0000000..f679511 --- /dev/null +++ b/Assets/Mirror/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2539267b6934a4026a505690a1e1eda2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs b/Assets/Mirror/Editor/AndroidManifestHelper.cs new file mode 100644 index 0000000..78f408d --- /dev/null +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs @@ -0,0 +1,113 @@ +// Android NetworkDiscovery Multicast fix +// https://github.com/vis2k/Mirror/pull/2887 +using UnityEditor; +using UnityEngine; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using System.Xml; +using System.IO; +#if UNITY_ANDROID +using UnityEditor.Android; +#endif + + +[InitializeOnLoad] +public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport +#if UNITY_ANDROID + , IPostGenerateGradleAndroidProject +#endif +{ + public int callbackOrder { get { return 99999; } } + +#if UNITY_ANDROID + public void OnPostGenerateGradleAndroidProject(string path) + { + string manifestFolder = Path.Combine(path, "src/main"); + string sourceFile = manifestFolder + "/AndroidManifest.xml"; + // Load android manfiest file + XmlDocument doc = new XmlDocument(); + doc.Load(sourceFile); + + string androidNamepsaceURI; + XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest"); + if (element == null) + { + UnityEngine.Debug.LogError("Could not find manifest tag in android manifest."); + return; + } + + // Get android namespace URI from the manifest + androidNamepsaceURI = element.GetAttribute("xmlns:android"); + if (string.IsNullOrEmpty(androidNamepsaceURI)) + { + UnityEngine.Debug.LogError("Could not find Android Namespace in manifest."); + return; + } + AddOrRemoveTag(doc, + androidNamepsaceURI, + "/manifest", + "uses-permission", + "android.permission.CHANGE_WIFI_MULTICAST_STATE", + true, + false); + AddOrRemoveTag(doc, + androidNamepsaceURI, + "/manifest", + "uses-permission", + "android.permission.INTERNET", + true, + false); + doc.Save(sourceFile); + } +#endif + + static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs + { + var nodes = doc.SelectNodes(path + "/" + elementName); + XmlElement element = null; + foreach (XmlElement e in nodes) + { + if (name == null || name == e.GetAttribute("name", @namespace)) + { + element = e; + break; + } + } + + if (required) + { + if (element == null) + { + var parent = doc.SelectSingleNode(path); + element = doc.CreateElement(elementName); + element.SetAttribute("name", @namespace, name); + parent.AppendChild(element); + } + + for (int i = 0; i < attrs.Length; i += 2) + { + if (modifyIfFound || string.IsNullOrEmpty(element.GetAttribute(attrs[i], @namespace))) + { + if (attrs[i + 1] != null) + { + element.SetAttribute(attrs[i], @namespace, attrs[i + 1]); + } + else + { + element.RemoveAttribute(attrs[i], @namespace); + } + } + } + } + else + { + if (element != null && modifyIfFound) + { + element.ParentNode.RemoveChild(element); + } + } + } + + public void OnPostprocessBuild(BuildReport report) {} + public void OnPreprocessBuild(BuildReport report) {} +} diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta b/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta new file mode 100644 index 0000000..1281aea --- /dev/null +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80cc70189403d7444bbffd185ca28462 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/EditorHelper.cs b/Assets/Mirror/Editor/EditorHelper.cs new file mode 100644 index 0000000..b10c7b0 --- /dev/null +++ b/Assets/Mirror/Editor/EditorHelper.cs @@ -0,0 +1,31 @@ +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + public static class EditorHelper + { + public static string FindPath() + { + string typeName = typeof(T).Name; + + string[] guidsFound = AssetDatabase.FindAssets($"t:Script {typeName}"); + if (guidsFound.Length >= 1 && !string.IsNullOrWhiteSpace(guidsFound[0])) + { + if (guidsFound.Length > 1) + { + Debug.LogWarning($"Found more than one{typeName}"); + } + + string path = AssetDatabase.GUIDToAssetPath(guidsFound[0]); + return Path.GetDirectoryName(path); + } + else + { + Debug.LogError($"Could not find path of {typeName}"); + return string.Empty; + } + } + } +} diff --git a/Assets/Mirror/Editor/EditorHelper.cs.meta b/Assets/Mirror/Editor/EditorHelper.cs.meta new file mode 100644 index 0000000..a1cd814 --- /dev/null +++ b/Assets/Mirror/Editor/EditorHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dba787f167ff29c4288532af1ec3584c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty.meta b/Assets/Mirror/Editor/Empty.meta new file mode 100644 index 0000000..ee87976 --- /dev/null +++ b/Assets/Mirror/Editor/Empty.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 62c8dc5bb12bbc6428bb66ccbac57000 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs new file mode 100644 index 0000000..18ab111 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs @@ -0,0 +1 @@ +// removed 2021-12-12 diff --git a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta new file mode 100644 index 0000000..79a200d --- /dev/null +++ b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b15a0d2ca0909400eb53dd6fe894cddd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs new file mode 100644 index 0000000..82e5275 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs @@ -0,0 +1 @@ +// File moved to Mirror/Editor/Logging/LogLevelWindow.cs \ No newline at end of file diff --git a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta new file mode 100644 index 0000000..b8cbaeb --- /dev/null +++ b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f28def2148ed5194abe70af012a4e3e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging.meta b/Assets/Mirror/Editor/Empty/Logging.meta new file mode 100644 index 0000000..257467f --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4d97731cd74ac8b4b8aad808548ef9cd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta new file mode 100644 index 0000000..832876f --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3dbf48190d77d243b87962a82c3b164 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta new file mode 100644 index 0000000..3214b08 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d6ce9d62a2d2ec4d8cef8a0d22b8dd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta new file mode 100644 index 0000000..2c1fac4 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f4ecb3d81ce9ff44b91f311ee46d4ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta new file mode 100644 index 0000000..b4c277d --- /dev/null +++ b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 37fb96d5bbf965d47acfc5c8589a1b71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta new file mode 100644 index 0000000..a1a0af3 --- /dev/null +++ b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d54a29ddd5b52b4eaa07ed39c0e3e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Icon.meta b/Assets/Mirror/Editor/Icon.meta new file mode 100644 index 0000000..7338187 --- /dev/null +++ b/Assets/Mirror/Editor/Icon.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5f1356ad059a1243910a4e82cd68c5f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/InspectorHelper.cs b/Assets/Mirror/Editor/InspectorHelper.cs new file mode 100644 index 0000000..80b3a06 --- /dev/null +++ b/Assets/Mirror/Editor/InspectorHelper.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace Mirror +{ + public static class InspectorHelper + { + /// Gets all public and private fields for a type + // deepestBaseType: Stops at this base type (exclusive) + public static IEnumerable GetAllFields(Type type, Type deepestBaseType) + { + const BindingFlags publicFields = BindingFlags.Public | BindingFlags.Instance; + const BindingFlags privateFields = BindingFlags.NonPublic | BindingFlags.Instance; + + // get public fields (includes fields from base type) + FieldInfo[] allPublicFields = type.GetFields(publicFields); + foreach (FieldInfo field in allPublicFields) + { + yield return field; + } + + // get private fields in current type, then move to base type + while (type != null) + { + FieldInfo[] allPrivateFields = type.GetFields(privateFields); + foreach (FieldInfo field in allPrivateFields) + { + yield return field; + } + + type = type.BaseType; + + // stop early + if (type == deepestBaseType) + { + break; + } + } + } + + public static bool IsSyncVar(this FieldInfo field) + { + object[] fieldMarkers = field.GetCustomAttributes(typeof(SyncVarAttribute), true); + return fieldMarkers.Length > 0; + } + + public static bool IsSerializeField(this FieldInfo field) + { + object[] fieldMarkers = field.GetCustomAttributes(typeof(SerializeField), true); + return fieldMarkers.Length > 0; + } + + public static bool IsVisibleField(this FieldInfo field) + { + return field.IsPublic || IsSerializeField(field); + } + + public static bool ImplementsInterface(this FieldInfo field) + { + return typeof(T).IsAssignableFrom(field.FieldType); + } + + public static bool HasShowInInspector(this FieldInfo field) + { + object[] fieldMarkers = field.GetCustomAttributes(typeof(ShowInInspectorAttribute), true); + return fieldMarkers.Length > 0; + } + + // checks if SyncObject is public or has our custom [ShowInInspector] field + public static bool IsVisibleSyncObject(this FieldInfo field) + { + return field.IsPublic || HasShowInInspector(field); + } + } +} diff --git a/Assets/Mirror/Editor/InspectorHelper.cs.meta b/Assets/Mirror/Editor/InspectorHelper.cs.meta new file mode 100644 index 0000000..852ff71 --- /dev/null +++ b/Assets/Mirror/Editor/InspectorHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 047c894c2a5ccc1438b7e59302f62744 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Mirror/Editor/Mirror.Editor.asmdef new file mode 100644 index 0000000..0d59f9f --- /dev/null +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Mirror.Editor", + "rootNamespace": "", + "references": [ + "Mirror", + "Unity.Mirror.CodeGen" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta new file mode 100644 index 0000000..e2e6f2a --- /dev/null +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c7c33eb5480dd24c9e29a8250c1a775 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs new file mode 100644 index 0000000..54b3ae7 --- /dev/null +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -0,0 +1,95 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomEditor(typeof(NetworkBehaviour), true)] + [CanEditMultipleObjects] + public class NetworkBehaviourInspector : Editor + { + bool syncsAnything; + SyncObjectCollectionsDrawer syncObjectCollectionsDrawer; + + // does this type sync anything? otherwise we don't need to show syncInterval + bool SyncsAnything(Type scriptClass) + { + // check for all SyncVar fields, they don't have to be visible + foreach (FieldInfo field in InspectorHelper.GetAllFields(scriptClass, typeof(NetworkBehaviour))) + { + if (field.IsSyncVar()) + { + return true; + } + } + + // has OnSerialize that is not in NetworkBehaviour? + // then it either has a syncvar or custom OnSerialize. either way + // this means we have something to sync. + MethodInfo method = scriptClass.GetMethod("OnSerialize"); + if (method != null && method.DeclaringType != typeof(NetworkBehaviour)) + { + return true; + } + + // SyncObjects are serialized in NetworkBehaviour.OnSerialize, which + // is always there even if we don't use SyncObjects. so we need to + // search for SyncObjects manually. + // Any SyncObject should be added to syncObjects when unity creates an + // object so we can check length of list so see if sync objects exists + return ((NetworkBehaviour)serializedObject.targetObject).HasSyncObjects(); + } + + void OnEnable() + { + if (target == null) { Debug.LogWarning("NetworkBehaviourInspector had no target object"); return; } + + // If target's base class is changed from NetworkBehaviour to MonoBehaviour + // then Unity temporarily keep using this Inspector causing things to break + if (!(target is NetworkBehaviour)) { return; } + + Type scriptClass = target.GetType(); + + syncObjectCollectionsDrawer = new SyncObjectCollectionsDrawer(serializedObject.targetObject); + + syncsAnything = SyncsAnything(scriptClass); + } + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + DrawSyncObjectCollections(); + DrawDefaultSyncSettings(); + } + + // Draws Sync Objects that are IEnumerable + protected void DrawSyncObjectCollections() + { + // Need this check in case OnEnable returns early + if (syncObjectCollectionsDrawer == null) return; + + syncObjectCollectionsDrawer.Draw(); + } + + // Draws SyncSettings if the NetworkBehaviour has anything to sync + protected void DrawDefaultSyncSettings() + { + // does it sync anything? then show extra properties + // (no need to show it if the class only has Cmds/Rpcs and no sync) + if (!syncsAnything) + { + return; + } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel); + + EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); + + // apply + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta new file mode 100644 index 0000000..78d9fa8 --- /dev/null +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f02853db46b6346e4866594a96c3b0e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Mirror/Editor/NetworkInformationPreview.cs new file mode 100644 index 0000000..2c8874b --- /dev/null +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs @@ -0,0 +1,305 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomPreview(typeof(GameObject))] + class NetworkInformationPreview : ObjectPreview + { + struct NetworkIdentityInfo + { + public GUIContent name; + public GUIContent value; + } + + struct NetworkBehaviourInfo + { + // This is here just so we can check if it's enabled/disabled + public NetworkBehaviour behaviour; + public GUIContent name; + } + + class Styles + { + public GUIStyle labelStyle = new GUIStyle(EditorStyles.label); + public GUIStyle componentName = new GUIStyle(EditorStyles.boldLabel); + public GUIStyle disabledName = new GUIStyle(EditorStyles.miniLabel); + + public Styles() + { + Color fontColor = new Color(0.7f, 0.7f, 0.7f); + labelStyle.padding.right += 20; + labelStyle.normal.textColor = fontColor; + labelStyle.active.textColor = fontColor; + labelStyle.focused.textColor = fontColor; + labelStyle.hover.textColor = fontColor; + labelStyle.onNormal.textColor = fontColor; + labelStyle.onActive.textColor = fontColor; + labelStyle.onFocused.textColor = fontColor; + labelStyle.onHover.textColor = fontColor; + + componentName.normal.textColor = fontColor; + componentName.active.textColor = fontColor; + componentName.focused.textColor = fontColor; + componentName.hover.textColor = fontColor; + componentName.onNormal.textColor = fontColor; + componentName.onActive.textColor = fontColor; + componentName.onFocused.textColor = fontColor; + componentName.onHover.textColor = fontColor; + + disabledName.normal.textColor = fontColor; + disabledName.active.textColor = fontColor; + disabledName.focused.textColor = fontColor; + disabledName.hover.textColor = fontColor; + disabledName.onNormal.textColor = fontColor; + disabledName.onActive.textColor = fontColor; + disabledName.onFocused.textColor = fontColor; + disabledName.onHover.textColor = fontColor; + } + } + + GUIContent title; + Styles styles = new Styles(); + + public override GUIContent GetPreviewTitle() + { + if (title == null) + { + title = new GUIContent("Network Information"); + } + return title; + } + + public override bool HasPreviewGUI() + { + // need to check if target is null to stop MissingReferenceException + return target != null && target is GameObject gameObject && gameObject.GetComponent() != null; + } + + public override void OnPreviewGUI(Rect r, GUIStyle background) + { + if (Event.current.type != EventType.Repaint) + return; + + if (target == null) + return; + + GameObject targetGameObject = target as GameObject; + + if (targetGameObject == null) + return; + + NetworkIdentity identity = targetGameObject.GetComponent(); + + if (identity == null) + return; + + if (styles == null) + styles = new Styles(); + + + // padding + RectOffset previewPadding = new RectOffset(-5, -5, -5, -5); + Rect paddedr = previewPadding.Add(r); + + //Centering + float initialX = paddedr.x + 10; + float Y = paddedr.y + 10; + + Y = DrawNetworkIdentityInfo(identity, initialX, Y); + + Y = DrawNetworkBehaviors(identity, initialX, Y); + + Y = DrawObservers(identity, initialX, Y); + + _ = DrawOwner(identity, initialX, Y); + + } + + float DrawNetworkIdentityInfo(NetworkIdentity identity, float initialX, float Y) + { + IEnumerable infos = GetNetworkIdentityInfo(identity); + // Get required label size for the names of the information values we're going to show + // There are two columns, one with label for the name of the info and the next for the value + Vector2 maxNameLabelSize = new Vector2(140, 16); + Vector2 maxValueLabelSize = GetMaxNameLabelSize(infos); + + Rect labelRect = new Rect(initialX, Y, maxNameLabelSize.x, maxNameLabelSize.y); + Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y); + + foreach (NetworkIdentityInfo info in infos) + { + GUI.Label(labelRect, info.name, styles.labelStyle); + GUI.Label(idLabelRect, info.value, styles.componentName); + labelRect.y += labelRect.height; + labelRect.x = initialX; + idLabelRect.y += idLabelRect.height; + } + + return labelRect.y; + } + + float DrawNetworkBehaviors(NetworkIdentity identity, float initialX, float Y) + { + IEnumerable behavioursInfo = GetNetworkBehaviorInfo(identity); + + // Show behaviours list in a different way than the name/value pairs above + Vector2 maxBehaviourLabelSize = GetMaxBehaviourLabelSize(behavioursInfo); + Rect behaviourRect = new Rect(initialX, Y + 10, maxBehaviourLabelSize.x, maxBehaviourLabelSize.y); + + GUI.Label(behaviourRect, new GUIContent("Network Behaviours"), styles.labelStyle); + // indent names + behaviourRect.x += 20; + behaviourRect.y += behaviourRect.height; + + foreach (NetworkBehaviourInfo info in behavioursInfo) + { + if (info.behaviour == null) + { + // could be the case in the editor after existing play mode. + continue; + } + + GUI.Label(behaviourRect, info.name, info.behaviour.enabled ? styles.componentName : styles.disabledName); + behaviourRect.y += behaviourRect.height; + Y = behaviourRect.y; + } + + return Y; + } + + float DrawObservers(NetworkIdentity identity, float initialX, float Y) + { + if (identity.observers != null && identity.observers.Count > 0) + { + Rect observerRect = new Rect(initialX, Y + 10, 200, 20); + + GUI.Label(observerRect, new GUIContent("Network observers"), styles.labelStyle); + // indent names + observerRect.x += 20; + observerRect.y += observerRect.height; + + foreach (KeyValuePair kvp in identity.observers) + { + GUI.Label(observerRect, $"{kvp.Value.address}:{kvp.Value}", styles.componentName); + observerRect.y += observerRect.height; + Y = observerRect.y; + } + } + + return Y; + } + + float DrawOwner(NetworkIdentity identity, float initialX, float Y) + { + if (identity.connectionToClient != null) + { + Rect ownerRect = new Rect(initialX, Y + 10, 400, 20); + GUI.Label(ownerRect, new GUIContent($"Client Authority: {identity.connectionToClient}"), styles.labelStyle); + Y += ownerRect.height; + } + return Y; + } + + // Get the maximum size used by the value of information items + Vector2 GetMaxNameLabelSize(IEnumerable infos) + { + Vector2 maxLabelSize = Vector2.zero; + foreach (NetworkIdentityInfo info in infos) + { + Vector2 labelSize = styles.labelStyle.CalcSize(info.value); + if (maxLabelSize.x < labelSize.x) + { + maxLabelSize.x = labelSize.x; + } + if (maxLabelSize.y < labelSize.y) + { + maxLabelSize.y = labelSize.y; + } + } + return maxLabelSize; + } + + Vector2 GetMaxBehaviourLabelSize(IEnumerable behavioursInfo) + { + Vector2 maxLabelSize = Vector2.zero; + foreach (NetworkBehaviourInfo behaviour in behavioursInfo) + { + Vector2 labelSize = styles.labelStyle.CalcSize(behaviour.name); + if (maxLabelSize.x < labelSize.x) + { + maxLabelSize.x = labelSize.x; + } + if (maxLabelSize.y < labelSize.y) + { + maxLabelSize.y = labelSize.y; + } + } + return maxLabelSize; + } + + IEnumerable GetNetworkIdentityInfo(NetworkIdentity identity) + { + List infos = new List + { + GetAssetId(identity), + GetString("Scene ID", identity.sceneId.ToString("X")) + }; + + if (Application.isPlaying) + { + infos.Add(GetString("Network ID", identity.netId.ToString())); + infos.Add(GetBoolean("Is Client", identity.isClient)); + infos.Add(GetBoolean("Is Server", identity.isServer)); + infos.Add(GetBoolean("Has Authority", identity.hasAuthority)); + infos.Add(GetBoolean("Is Local Player", identity.isLocalPlayer)); + } + return infos; + } + + IEnumerable GetNetworkBehaviorInfo(NetworkIdentity identity) + { + List behaviourInfos = new List(); + + NetworkBehaviour[] behaviours = identity.GetComponents(); + foreach (NetworkBehaviour behaviour in behaviours) + { + behaviourInfos.Add(new NetworkBehaviourInfo + { + name = new GUIContent(behaviour.GetType().FullName), + behaviour = behaviour + }); + } + return behaviourInfos; + } + + NetworkIdentityInfo GetAssetId(NetworkIdentity identity) + { + string assetId = identity.assetId.ToString(); + if (string.IsNullOrWhiteSpace(assetId)) + { + assetId = ""; + } + return GetString("Asset ID", assetId); + } + + static NetworkIdentityInfo GetString(string name, string value) + { + return new NetworkIdentityInfo + { + name = new GUIContent(name), + value = new GUIContent(value) + }; + } + + static NetworkIdentityInfo GetBoolean(string name, bool value) + { + return new NetworkIdentityInfo + { + name = new GUIContent(name), + value = new GUIContent((value ? "Yes" : "No")) + }; + } + } +} diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta new file mode 100644 index 0000000..9bf2de4 --- /dev/null +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51a99294efe134232932c34606737356 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs b/Assets/Mirror/Editor/NetworkManagerEditor.cs new file mode 100644 index 0000000..94b0844 --- /dev/null +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs @@ -0,0 +1,108 @@ +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; + +namespace Mirror +{ + [CustomEditor(typeof(NetworkManager), true)] + [CanEditMultipleObjects] + public class NetworkManagerEditor : Editor + { + SerializedProperty spawnListProperty; + ReorderableList spawnList; + protected NetworkManager networkManager; + + protected void Init() + { + if (spawnList == null) + { + networkManager = target as NetworkManager; + spawnListProperty = serializedObject.FindProperty("spawnPrefabs"); + spawnList = new ReorderableList(serializedObject, spawnListProperty) + { + drawHeaderCallback = DrawHeader, + drawElementCallback = DrawChild, + onReorderCallback = Changed, + onRemoveCallback = RemoveButton, + onChangedCallback = Changed, + onAddCallback = AddButton, + // this uses a 16x16 icon. other sizes make it stretch. + elementHeight = 16 + }; + } + } + + public override void OnInspectorGUI() + { + Init(); + DrawDefaultInspector(); + EditorGUI.BeginChangeCheck(); + spawnList.DoLayoutList(); + if (EditorGUI.EndChangeCheck()) + { + serializedObject.ApplyModifiedProperties(); + } + } + + static void DrawHeader(Rect headerRect) + { + GUI.Label(headerRect, "Registered Spawnable Prefabs:"); + } + + internal void DrawChild(Rect r, int index, bool isActive, bool isFocused) + { + SerializedProperty prefab = spawnListProperty.GetArrayElementAtIndex(index); + GameObject go = (GameObject)prefab.objectReferenceValue; + + GUIContent label; + if (go == null) + { + label = new GUIContent("Empty", "Drag a prefab with a NetworkIdentity here"); + } + else + { + NetworkIdentity identity = go.GetComponent(); + label = new GUIContent(go.name, identity != null ? $"AssetId: [{identity.assetId}]" : "No Network Identity"); + } + + GameObject newGameObject = (GameObject)EditorGUI.ObjectField(r, label, go, typeof(GameObject), false); + + if (newGameObject != go) + { + if (newGameObject != null && !newGameObject.GetComponent()) + { + Debug.LogError($"Prefab {newGameObject} cannot be added as spawnable as it doesn't have a NetworkIdentity."); + return; + } + prefab.objectReferenceValue = newGameObject; + } + } + + internal void Changed(ReorderableList list) + { + EditorUtility.SetDirty(target); + } + + internal void AddButton(ReorderableList list) + { + spawnListProperty.arraySize += 1; + list.index = spawnListProperty.arraySize - 1; + + SerializedProperty obj = spawnListProperty.GetArrayElementAtIndex(spawnListProperty.arraySize - 1); + obj.objectReferenceValue = null; + + spawnList.index = spawnList.count - 1; + + Changed(list); + } + + internal void RemoveButton(ReorderableList list) + { + spawnListProperty.DeleteArrayElementAtIndex(spawnList.index); + if (list.index >= spawnListProperty.arraySize) + { + list.index = spawnListProperty.arraySize - 1; + } + } + } +} diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta new file mode 100644 index 0000000..7fe8dbc --- /dev/null +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 519712eb07f7a44039df57664811c2c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs b/Assets/Mirror/Editor/NetworkScenePostProcess.cs new file mode 100644 index 0000000..c60493d --- /dev/null +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; + +namespace Mirror +{ + public class NetworkScenePostProcess : MonoBehaviour + { + [PostProcessScene] + public static void OnPostProcessScene() + { + // find all NetworkIdentities in all scenes + // => can't limit it to GetActiveScene() because that wouldn't work + // for additive scene loads (the additively loaded scene is never + // the active scene) + // => ignore DontDestroyOnLoad scene! this avoids weird situations + // like in NetworkZones when we destroy the local player and + // load another scene afterwards, yet the local player is still + // in the FindObjectsOfType result with scene=DontDestroyOnLoad + // for some reason + // => OfTypeAll so disabled objects are included too + // => Unity 2019 returns prefabs here too, so filter them out. + IEnumerable identities = Resources.FindObjectsOfTypeAll() + .Where(identity => identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.gameObject.scene.name != "DontDestroyOnLoad" && + !Utils.IsPrefab(identity.gameObject)); + + foreach (NetworkIdentity identity in identities) + { + // if we had a [ConflictComponent] attribute that would be better than this check. + // also there is no context about which scene this is in. + if (identity.GetComponent() != null) + { + Debug.LogError("NetworkManager has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended."); + } + + // not spawned before? + // OnPostProcessScene is called after additive scene loads too, + // and we don't want to set main scene's objects inactive again + if (!identity.isClient && !identity.isServer) + { + // valid scene object? + // otherwise it might be an unopened scene that still has null + // sceneIds. builds are interrupted if they contain 0 sceneIds, + // but it's still possible that we call LoadScene in Editor + // for a previously unopened scene. + // (and only do SetActive if this was actually a scene object) + if (identity.sceneId != 0) + { + PrepareSceneObject(identity); + } + // throwing an exception would only show it for one object + // because this function would return afterwards. + else + { + // there are two cases where sceneId == 0: + // * if we have a prefab open in the prefab scene + // * if an unopened scene needs resaving + // show a proper error message in both cases so the user + // knows what to do. + string path = identity.gameObject.scene.path; + if (string.IsNullOrWhiteSpace(path)) + Debug.LogError($"{identity.name} is currently open in Prefab Edit Mode. Please open the actual scene before launching Mirror."); + else + Debug.LogError($"Scene {path} needs to be opened and resaved, because the scene object {identity.name} has no valid sceneId yet."); + + // either way we shouldn't continue. nothing good will + // happen when trying to launch with invalid sceneIds. + EditorApplication.isPlaying = false; + } + } + } + } + + static void PrepareSceneObject(NetworkIdentity identity) + { + // set scene hash + identity.SetSceneIdSceneHashPartInternal(); + + // disable it + // note: NetworkIdentity.OnDisable adds itself to the + // spawnableObjects dictionary (only if sceneId != 0) + identity.gameObject.SetActive(false); + + // safety check for prefabs with more than one NetworkIdentity +#if UNITY_2018_2_OR_NEWER + GameObject prefabGO = PrefabUtility.GetCorrespondingObjectFromSource(identity.gameObject); +#else + GameObject prefabGO = PrefabUtility.GetPrefabParent(identity.gameObject); +#endif + if (prefabGO) + { +#if UNITY_2018_3_OR_NEWER + GameObject prefabRootGO = prefabGO.transform.root.gameObject; +#else + GameObject prefabRootGO = PrefabUtility.FindPrefabRoot(prefabGO); +#endif + if (prefabRootGO != null && prefabRootGO.GetComponentsInChildren().Length > 1) + { + Debug.LogWarning($"Prefab {prefabRootGO.name} has several NetworkIdentity components attached to itself or its children, this is not supported."); + } + } + } + } +} diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta b/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta new file mode 100644 index 0000000..b567cc9 --- /dev/null +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3ec1c414d821444a9e77f18a2c130ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/SceneDrawer.cs b/Assets/Mirror/Editor/SceneDrawer.cs new file mode 100644 index 0000000..f391684 --- /dev/null +++ b/Assets/Mirror/Editor/SceneDrawer.cs @@ -0,0 +1,47 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomPropertyDrawer(typeof(SceneAttribute))] + public class SceneDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + if (property.propertyType == SerializedPropertyType.String) + { + SceneAsset sceneObject = AssetDatabase.LoadAssetAtPath(property.stringValue); + + if (sceneObject == null && !string.IsNullOrWhiteSpace(property.stringValue)) + { + // try to load it from the build settings for legacy compatibility + sceneObject = GetBuildSettingsSceneObject(property.stringValue); + } + if (sceneObject == null && !string.IsNullOrWhiteSpace(property.stringValue)) + { + Debug.LogError($"Could not find scene {property.stringValue} in {property.propertyPath}, assign the proper scenes in your NetworkManager"); + } + SceneAsset scene = (SceneAsset)EditorGUI.ObjectField(position, label, sceneObject, typeof(SceneAsset), true); + + property.stringValue = AssetDatabase.GetAssetPath(scene); + } + else + { + EditorGUI.LabelField(position, label.text, "Use [Scene] with strings."); + } + } + + protected SceneAsset GetBuildSettingsSceneObject(string sceneName) + { + foreach (EditorBuildSettingsScene buildScene in EditorBuildSettings.scenes) + { + SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath(buildScene.path); + if (sceneAsset!= null && sceneAsset.name == sceneName) + { + return sceneAsset; + } + } + return null; + } + } +} diff --git a/Assets/Mirror/Editor/SceneDrawer.cs.meta b/Assets/Mirror/Editor/SceneDrawer.cs.meta new file mode 100644 index 0000000..6a996dc --- /dev/null +++ b/Assets/Mirror/Editor/SceneDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b24704a46211b4ea294aba8f58715cea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs new file mode 100644 index 0000000..2c95bcf --- /dev/null +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs @@ -0,0 +1,83 @@ +// helper class for NetworkBehaviourInspector to draw all enumerable SyncObjects +// (SyncList/Set/Dictionary) +// 'SyncObjectCollectionsDrawer' is a nicer name than 'IEnumerableSyncObjectsDrawer' +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; + +namespace Mirror +{ + class SyncObjectCollectionField + { + public bool visible; + public readonly FieldInfo field; + public readonly string label; + + public SyncObjectCollectionField(FieldInfo field) + { + this.field = field; + visible = false; + label = $"{field.Name} [{field.FieldType.Name}]"; + } + } + + public class SyncObjectCollectionsDrawer + { + readonly UnityEngine.Object targetObject; + readonly List syncObjectCollectionFields; + + public SyncObjectCollectionsDrawer(UnityEngine.Object targetObject) + { + this.targetObject = targetObject; + syncObjectCollectionFields = new List(); + foreach (FieldInfo field in InspectorHelper.GetAllFields(targetObject.GetType(), typeof(NetworkBehaviour))) + { + // only draw SyncObjects that are IEnumerable (SyncList/Set/Dictionary) + if (field.IsVisibleSyncObject() && + field.ImplementsInterface() && + field.ImplementsInterface()) + { + syncObjectCollectionFields.Add(new SyncObjectCollectionField(field)); + } + } + } + + public void Draw() + { + if (syncObjectCollectionFields.Count == 0) { return; } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Sync Collections", EditorStyles.boldLabel); + + for (int i = 0; i < syncObjectCollectionFields.Count; i++) + { + DrawSyncObjectCollection(syncObjectCollectionFields[i]); + } + } + + void DrawSyncObjectCollection(SyncObjectCollectionField syncObjectCollectionField) + { + syncObjectCollectionField.visible = EditorGUILayout.Foldout(syncObjectCollectionField.visible, syncObjectCollectionField.label); + if (syncObjectCollectionField.visible) + { + using (new EditorGUI.IndentLevelScope()) + { + object fieldValue = syncObjectCollectionField.field.GetValue(targetObject); + if (fieldValue is IEnumerable syncObject) + { + int index = 0; + foreach (object item in syncObject) + { + string itemValue = item != null ? item.ToString() : "NULL"; + string itemLabel = $"Element {index}"; + EditorGUILayout.LabelField(itemLabel, itemValue); + + index++; + } + } + } + } + } + } +} diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta new file mode 100644 index 0000000..44ba75d --- /dev/null +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f90afab12e04f0e945d83e9d38308a3 +timeCreated: 1632556645 \ No newline at end of file diff --git a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs new file mode 100644 index 0000000..2356a5e --- /dev/null +++ b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs @@ -0,0 +1,28 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomPropertyDrawer(typeof(SyncVarAttribute))] + public class SyncVarAttributeDrawer : PropertyDrawer + { + static readonly GUIContent syncVarIndicatorContent = new GUIContent("SyncVar", "This variable has been marked with the [SyncVar] attribute."); + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + Vector2 syncVarIndicatorRect = EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent); + float valueWidth = position.width - syncVarIndicatorRect.x; + + Rect valueRect = new Rect(position.x, position.y, valueWidth, position.height); + Rect labelRect = new Rect(position.x + valueWidth, position.y, syncVarIndicatorRect.x, position.height); + + EditorGUI.PropertyField(valueRect, property, label, true); + GUI.Label(labelRect, syncVarIndicatorContent, EditorStyles.miniLabel); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUI.GetPropertyHeight(property); + } + } +} diff --git a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta new file mode 100644 index 0000000..6311f1d --- /dev/null +++ b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27821afc81c4d064d8348fbeb00c0ce8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs b/Assets/Mirror/Editor/SyncVarDrawer.cs new file mode 100644 index 0000000..b0532ae --- /dev/null +++ b/Assets/Mirror/Editor/SyncVarDrawer.cs @@ -0,0 +1,35 @@ +// SyncVar looks like this in the Inspector: +// Health +// Value: 42 +// instead, let's draw ._Value directly so it looks like this: +// Health: 42 +// +// BUG: Unity also doesn't show custom drawer for readonly fields (#1368395) +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomPropertyDrawer(typeof(SyncVar<>))] + public class SyncVarDrawer : PropertyDrawer + { + static readonly GUIContent syncVarIndicatorContent = new GUIContent("SyncVar", "This variable is a SyncVar."); + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + Vector2 syncVarIndicatorRect = EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent); + float valueWidth = position.width - syncVarIndicatorRect.x; + + Rect valueRect = new Rect(position.x, position.y, valueWidth, position.height); + Rect labelRect = new Rect(position.x + valueWidth, position.y, syncVarIndicatorRect.x, position.height); + + EditorGUI.PropertyField(valueRect, property.FindPropertyRelative("_Value"), label, true); + GUI.Label(labelRect, syncVarIndicatorContent, EditorStyles.miniLabel); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("_Value")); + } + } +} diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs.meta b/Assets/Mirror/Editor/SyncVarDrawer.cs.meta new file mode 100644 index 0000000..0ee91aa --- /dev/null +++ b/Assets/Mirror/Editor/SyncVarDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 874812594431423b84f763b987ff9681 +timeCreated: 1632553007 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver.meta b/Assets/Mirror/Editor/Weaver.meta new file mode 100644 index 0000000..121fbf4 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d9f8e6274119b4ce29e498cfb8aca8a4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs new file mode 100644 index 0000000..08b43f5 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mirror.Tests")] diff --git a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta new file mode 100644 index 0000000..d356af8 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 929924d95663264478d4238d4910d22e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty.meta b/Assets/Mirror/Editor/Weaver/Empty.meta new file mode 100644 index 0000000..6e29ee7 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 30fc290f2ff9c29498f54f63de12ca6f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs new file mode 100644 index 0000000..a88144a --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs @@ -0,0 +1 @@ +// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta new file mode 100644 index 0000000..685f914 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd67b3f7c2d66074a9bc7a23787e2ffb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs new file mode 100644 index 0000000..b38f171 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs @@ -0,0 +1 @@ +// removed Oct 5 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta new file mode 100644 index 0000000..cbea4d6 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e25c00c88fc134f6ea7ab00ae4db8083 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/Program.cs b/Assets/Mirror/Editor/Weaver/Empty/Program.cs new file mode 100644 index 0000000..a214b81 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/Program.cs @@ -0,0 +1 @@ +// Removed 05/09/20 diff --git a/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta new file mode 100644 index 0000000..0a14018 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0152994c9591626408fcfec96fcc7933 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs new file mode 100644 index 0000000..a88144a --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs @@ -0,0 +1 @@ +// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta new file mode 100644 index 0000000..0a7c2aa --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29e4a45f69822462ab0b15adda962a29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs new file mode 100644 index 0000000..2fdbc52 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs @@ -0,0 +1 @@ +// removed 2020-09 diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta new file mode 100644 index 0000000..81b9576 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5d8b25543a624384944b599e5a832a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs new file mode 100644 index 0000000..a88144a --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs @@ -0,0 +1 @@ +// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta new file mode 100644 index 0000000..b73b047 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f3445268e45d437fac325837aff3246 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint.meta b/Assets/Mirror/Editor/Weaver/EntryPoint.meta new file mode 100644 index 0000000..81827c5 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 251338e67afb4cefa38da924f8c50a6e +timeCreated: 1628851818 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs new file mode 100644 index 0000000..9016949 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs @@ -0,0 +1,189 @@ +// for Unity 2020+ we use ILPostProcessor. +// only automatically invoke it for older versions. +#if !UNITY_2020_3_OR_NEWER +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.CecilX; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEngine; +using UnityAssembly = UnityEditor.Compilation.Assembly; + +namespace Mirror.Weaver +{ + public static class CompilationFinishedHook + { + // needs to be the same as Weaver.MirrorAssemblyName! + const string MirrorRuntimeAssemblyName = "Mirror"; + const string MirrorWeaverAssemblyName = "Mirror.Weaver"; + + // global weaver define so that tests can use it + internal static Weaver weaver; + + // delegate for subscription to Weaver warning messages + public static Action OnWeaverWarning; + // delete for subscription to Weaver error messages + public static Action OnWeaverError; + + // controls whether Weaver errors are reported direct to the Unity console (tests enable this) + public static bool UnityLogEnabled = true; + + [InitializeOnLoadMethod] + public static void OnInitializeOnLoad() + { + CompilationPipeline.assemblyCompilationFinished += OnCompilationFinished; + + // We only need to run this once per session + // after that, all assemblies will be weaved by the event + if (!SessionState.GetBool("MIRROR_WEAVED", false)) + { + // reset session flag + SessionState.SetBool("MIRROR_WEAVED", true); + SessionState.SetBool("MIRROR_WEAVE_SUCCESS", true); + + WeaveExistingAssemblies(); + } + } + + public static void WeaveExistingAssemblies() + { + foreach (UnityAssembly assembly in CompilationPipeline.GetAssemblies()) + { + if (File.Exists(assembly.outputPath)) + { + OnCompilationFinished(assembly.outputPath, new CompilerMessage[0]); + } + } + +#if UNITY_2019_3_OR_NEWER + EditorUtility.RequestScriptReload(); +#else + UnityEditorInternal.InternalEditorUtility.RequestScriptReload(); +#endif + } + + static Assembly FindCompilationPipelineAssembly(string assemblyName) => + CompilationPipeline.GetAssemblies().First(assembly => assembly.name == assemblyName); + + static bool CompilerMessagesContainError(CompilerMessage[] messages) => + messages.Any(msg => msg.type == CompilerMessageType.Error); + + public static void OnCompilationFinished(string assemblyPath, CompilerMessage[] messages) + { + // Do nothing if there were compile errors on the target + if (CompilerMessagesContainError(messages)) + { + Debug.Log("Weaver: stop because compile errors on target"); + return; + } + + // Should not run on the editor only assemblies + if (assemblyPath.Contains("-Editor") || assemblyPath.Contains(".Editor")) + { + return; + } + + // don't weave mirror files + string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + if (assemblyName == MirrorRuntimeAssemblyName || assemblyName == MirrorWeaverAssemblyName) + { + return; + } + + // find Mirror.dll + Assembly mirrorAssembly = FindCompilationPipelineAssembly(MirrorRuntimeAssemblyName); + if (mirrorAssembly == null) + { + Debug.LogError("Failed to find Mirror runtime assembly"); + return; + } + + string mirrorRuntimeDll = mirrorAssembly.outputPath; + if (!File.Exists(mirrorRuntimeDll)) + { + // this is normal, it happens with any assembly that is built before mirror + // such as unity packages or your own assemblies + // those don't need to be weaved + // if any assembly depends on mirror, then it will be built after + return; + } + + // find UnityEngine.CoreModule.dll + string unityEngineCoreModuleDLL = UnityEditorInternal.InternalEditorUtility.GetEngineCoreModuleAssemblyPath(); + if (string.IsNullOrEmpty(unityEngineCoreModuleDLL)) + { + Debug.LogError("Failed to find UnityEngine assembly"); + return; + } + + HashSet dependencyPaths = GetDependencyPaths(assemblyPath); + dependencyPaths.Add(Path.GetDirectoryName(mirrorRuntimeDll)); + dependencyPaths.Add(Path.GetDirectoryName(unityEngineCoreModuleDLL)); + + if (!WeaveFromFile(assemblyPath, dependencyPaths.ToArray())) + { + // Set false...will be checked in \Editor\EnterPlayModeSettingsCheck.CheckSuccessfulWeave() + SessionState.SetBool("MIRROR_WEAVE_SUCCESS", false); + if (UnityLogEnabled) Debug.LogError($"Weaving failed for {assemblyPath}"); + } + } + + static HashSet GetDependencyPaths(string assemblyPath) + { + // build directory list for later asm/symbol resolving using CompilationPipeline refs + HashSet dependencyPaths = new HashSet + { + Path.GetDirectoryName(assemblyPath) + }; + foreach (Assembly assembly in CompilationPipeline.GetAssemblies()) + { + if (assembly.outputPath == assemblyPath) + { + foreach (string reference in assembly.compiledAssemblyReferences) + { + dependencyPaths.Add(Path.GetDirectoryName(reference)); + } + } + } + + return dependencyPaths; + } + // helper function to invoke Weaver with an AssemblyDefinition from a + // file path, with dependencies added. + static bool WeaveFromFile(string assemblyPath, string[] dependencies) + { + // resolve assembly from stream + using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver()) + using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath, new ReaderParameters{ ReadWrite = true, ReadSymbols = true, AssemblyResolver = asmResolver })) + { + // add this assembly's path and unity's assembly path + asmResolver.AddSearchDirectory(Path.GetDirectoryName(assemblyPath)); + asmResolver.AddSearchDirectory(Helpers.UnityEngineDllDirectoryName()); + + // add dependencies + if (dependencies != null) + { + foreach (string path in dependencies) + { + asmResolver.AddSearchDirectory(path); + } + } + + // create weaver with logger + weaver = new Weaver(new CompilationFinishedLogger()); + if (weaver.Weave(assembly, asmResolver, out bool modified)) + { + // write changes to file if modified + if (modified) + assembly.Write(new WriterParameters{WriteSymbols = true}); + + return true; + } + return false; + } + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta new file mode 100644 index 0000000..ed537ab --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de2aeb2e8068f421a9a1febe408f7051 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs new file mode 100644 index 0000000..e053404 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs @@ -0,0 +1,31 @@ +// logger for compilation finished hook. +// where we need a callback and Debug.Log. +// for Unity 2020+ we use ILPostProcessor. +#if !UNITY_2020_3_OR_NEWER +using Mono.CecilX; +using UnityEngine; + +namespace Mirror.Weaver +{ + public class CompilationFinishedLogger : Logger + { + public void Warning(string message) => Warning(message, null); + public void Warning(string message, MemberReference mr) + { + if (mr != null) message = $"{message} (at {mr})"; + + if (CompilationFinishedHook.UnityLogEnabled) Debug.LogWarning(message); + CompilationFinishedHook.OnWeaverWarning?.Invoke(message); + } + + public void Error(string message) => Error(message, null); + public void Error(string message, MemberReference mr) + { + if (mr != null) message = $"{message} (at {mr})"; + + if (CompilationFinishedHook.UnityLogEnabled) Debug.LogError(message); + CompilationFinishedHook.OnWeaverError?.Invoke(message); + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta new file mode 100644 index 0000000..f8c7139 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 47026732f0fa475c94bd1dd41f1de559 +timeCreated: 1629379868 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs new file mode 100644 index 0000000..5dffa97 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs @@ -0,0 +1,44 @@ +#if !UNITY_2020_3_OR_NEWER +// make sure we weaved successfully when entering play mode. +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + public class EnterPlayModeSettingsCheck : MonoBehaviour + { + [InitializeOnLoadMethod] + static void OnInitializeOnLoad() + { + // Hook this event to see if we have a good weave every time + // user attempts to enter play mode or tries to do a build + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + static void OnPlayModeStateChanged(PlayModeStateChange state) + { + // Per Unity docs, this fires "when exiting edit mode before the Editor is in play mode". + // This doesn't fire when closing the editor. + if (state == PlayModeStateChange.ExitingEditMode) + { + // Check if last weave result was successful + if (!SessionState.GetBool("MIRROR_WEAVE_SUCCESS", false)) + { + // Last weave result was a failure...try to weave again + // Faults will show in the console that may have been cleared by "Clear on Play" + SessionState.SetBool("MIRROR_WEAVE_SUCCESS", true); + Weaver.CompilationFinishedHook.WeaveExistingAssemblies(); + + // Did that clear things up for us? + if (!SessionState.GetBool("MIRROR_WEAVE_SUCCESS", false)) + { + // Nope, still failed, and console has the issues logged + Debug.LogError("Can't enter play mode until weaver issues are resolved."); + EditorApplication.isPlaying = false; + } + } + } + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta new file mode 100644 index 0000000..eca31e3 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b73d0f106ba84aa983baa5142b08a0a9 +timeCreated: 1628851346 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta new file mode 100644 index 0000000..6ef7bf3 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09082db63d1d48d9ab91320165c1b684 +timeCreated: 1628859005 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs new file mode 100644 index 0000000..e4d9de2 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs @@ -0,0 +1,31 @@ +// tests use WeaveAssembler, which uses AssemblyBuilder to Build(). +// afterwards ILPostProcessor weaves the build. +// this works on windows, but build() does not run ILPP on mac atm. +// we need to manually invoke ILPP with an assembly from file. +// +// this is in Weaver folder becuase CompilationPipeline can only be accessed +// from assemblies with the name "Unity.*.CodeGen" +using System.IO; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace Mirror.Weaver +{ + public class CompiledAssemblyFromFile : ICompiledAssembly + { + readonly string assemblyPath; + + public string Name => Path.GetFileNameWithoutExtension(assemblyPath); + public string[] References { get; set; } + public string[] Defines { get; set; } + public InMemoryAssembly InMemoryAssembly { get; } + + public CompiledAssemblyFromFile(string assemblyPath) + { + this.assemblyPath = assemblyPath; + byte[] peData = File.ReadAllBytes(assemblyPath); + string pdbFileName = Path.GetFileNameWithoutExtension(assemblyPath) + ".pdb"; + byte[] pdbData = File.ReadAllBytes(Path.Combine(Path.GetDirectoryName(assemblyPath), pdbFileName)); + InMemoryAssembly = new InMemoryAssembly(peData, pdbData); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta new file mode 100644 index 0000000..1e5091e --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9009d1db4ed44f6694a92bf8ad7738e9 +timeCreated: 1630129423 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs new file mode 100644 index 0000000..cbc8e41 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs @@ -0,0 +1,167 @@ +// based on paul's resolver from +// https://github.com/MirageNet/Mirage/commit/def64cd1db525398738f057b3d1eb1fe8afc540c?branch=def64cd1db525398738f057b3d1eb1fe8afc540c&diff=split +// +// an assembly resolver's job is to open an assembly in case we want to resolve +// a type from it. +// +// for example, while weaving MyGame.dll: if we want to resolve ArraySegment, +// then we need to open and resolve from another assembly (CoreLib). +// +// using DefaultAssemblyResolver with ILPostProcessor throws Exceptions in +// WeaverTypes.cs when resolving anything, for example: +// ArraySegment in Mirror.Tests.Dll. +// +// we need a custom resolver for ILPostProcessor. +#if UNITY_2020_3_OR_NEWER +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Mono.CecilX; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace Mirror.Weaver +{ + class ILPostProcessorAssemblyResolver : IAssemblyResolver + { + readonly string[] assemblyReferences; + readonly Dictionary assemblyCache = + new Dictionary(); + readonly ICompiledAssembly compiledAssembly; + AssemblyDefinition selfAssembly; + + Logger Log; + + public ILPostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly, Logger Log) + { + this.compiledAssembly = compiledAssembly; + assemblyReferences = compiledAssembly.References; + this.Log = Log; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Cleanup + } + + public AssemblyDefinition Resolve(AssemblyNameReference name) => + Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + + public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + lock (assemblyCache) + { + if (name.Name == compiledAssembly.Name) + return selfAssembly; + + string fileName = FindFile(name); + if (fileName == null) + { + // returning null will throw exceptions in our weaver where. + // let's make it obvious why we returned null for easier debugging. + // NOTE: if this fails for "System.Private.CoreLib": + // ILPostProcessorReflectionImporter fixes it! + Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}"); + return null; + } + + DateTime lastWriteTime = File.GetLastWriteTime(fileName); + + string cacheKey = fileName + lastWriteTime; + + if (assemblyCache.TryGetValue(cacheKey, out AssemblyDefinition result)) + return result; + + parameters.AssemblyResolver = this; + + MemoryStream ms = MemoryStreamFor(fileName); + + string pdb = fileName + ".pdb"; + if (File.Exists(pdb)) + parameters.SymbolStream = MemoryStreamFor(pdb); + + AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); + assemblyCache.Add(cacheKey, assemblyDefinition); + return assemblyDefinition; + } + } + + // find assemblyname in assembly's references + string FindFile(AssemblyNameReference name) + { + string fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".dll"); + if (fileName != null) + return fileName; + + // perhaps the type comes from an exe instead + fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".exe"); + if (fileName != null) + return fileName; + + // Unfortunately the current ICompiledAssembly API only provides direct references. + // It is very much possible that a postprocessor ends up investigating a type in a directly + // referenced assembly, that contains a field that is not in a directly referenced assembly. + // if we don't do anything special for that situation, it will fail to resolve. We should fix this + // in the ILPostProcessing API. As a workaround, we rely on the fact here that the indirect references + // are always located next to direct references, so we search in all directories of direct references we + // got passed, and if we find the file in there, we resolve to it. + foreach (string parentDir in assemblyReferences.Select(Path.GetDirectoryName).Distinct()) + { + string candidate = Path.Combine(parentDir, name.Name + ".dll"); + if (File.Exists(candidate)) + return candidate; + } + + return null; + } + + // open file as MemoryStream + // attempts multiple times, not sure why.. + static MemoryStream MemoryStreamFor(string fileName) + { + return Retry(10, TimeSpan.FromSeconds(1), () => + { + byte[] byteArray; + using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + byteArray = new byte[fs.Length]; + int readLength = fs.Read(byteArray, 0, (int)fs.Length); + if (readLength != fs.Length) + throw new InvalidOperationException("File read length is not full length of file."); + } + + return new MemoryStream(byteArray); + }); + } + + static MemoryStream Retry(int retryCount, TimeSpan waitTime, Func func) + { + try + { + return func(); + } + catch (IOException) + { + if (retryCount == 0) + throw; + Console.WriteLine($"Caught IO Exception, trying {retryCount} more times"); + Thread.Sleep(waitTime); + return Retry(retryCount - 1, waitTime, func); + } + } + + // if the CompiledAssembly's AssemblyDefinition is known, we can add it + public void SetAssemblyDefinitionForCompiledAssembly(AssemblyDefinition assemblyDefinition) + { + selfAssembly = assemblyDefinition; + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta new file mode 100644 index 0000000..07289dd --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0b3e94696e22440ead0b3a42411bbe14 +timeCreated: 1629693784 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs new file mode 100644 index 0000000..6a70641 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs @@ -0,0 +1,53 @@ +// helper function to use ILPostProcessor for an assembly from file. +// we keep this in Weaver folder because we can access CompilationPipleine here. +// in tests folder we can't, unless we rename to "Unity.*.CodeGen", +// but then tests wouldn't be weaved anymore. +#if UNITY_2020_3_OR_NEWER +using System; +using System.IO; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace Mirror.Weaver +{ + public static class ILPostProcessorFromFile + { + // read, weave, write file via ILPostProcessor + public static void ILPostProcessFile(string assemblyPath, string[] references, Action OnWarning, Action OnError) + { + // we COULD Weave() with a test logger manually. + // but for test result consistency on all platforms, + // let's invoke the ILPostProcessor here too. + CompiledAssemblyFromFile assembly = new CompiledAssemblyFromFile(assemblyPath); + assembly.References = references; + + // create ILPP and check WillProcess like Unity would. + ILPostProcessorHook ilpp = new ILPostProcessorHook(); + if (ilpp.WillProcess(assembly)) + { + //Debug.Log($"Will Process: {assembly.Name}"); + + // process it like Unity would + ILPostProcessResult result = ilpp.Process(assembly); + + // handle the error messages like Unity would + foreach (DiagnosticMessage message in result.Diagnostics) + { + if (message.DiagnosticType == DiagnosticType.Warning) + { + OnWarning(message.MessageData); + } + else if (message.DiagnosticType == DiagnosticType.Error) + { + OnError(message.MessageData); + } + } + + // save the weaved assembly to file. + // some tests open it and check for certain IL code. + File.WriteAllBytes(assemblyPath, result.InMemoryAssembly.PeData); + } + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta new file mode 100644 index 0000000..e06dfa7 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2a4b115486b74d27a9540f3c39ae2d46 +timeCreated: 1630152191 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs new file mode 100644 index 0000000..0cfb433 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs @@ -0,0 +1,143 @@ +// hook via ILPostProcessor from Unity 2020.3+ +// (2020.1 has errors https://github.com/vis2k/Mirror/issues/2912) +#if UNITY_2020_3_OR_NEWER +// Unity.CompilationPipeline reference is only resolved if assembly name is +// Unity.*.CodeGen: +// https://forum.unity.com/threads/how-does-unity-do-codegen-and-why-cant-i-do-it-myself.853867/#post-5646937 +using System.IO; +using System.Linq; +// to use Mono.CecilX here, we need to 'override references' in the +// Unity.Mirror.CodeGen assembly definition file in the Editor, and add CecilX. +// otherwise we get a reflection exception with 'file not found: CecilX'. +using Mono.CecilX; +using Mono.CecilX.Cil; +using Unity.CompilationPipeline.Common.ILPostProcessing; +// IMPORTANT: 'using UnityEngine' does not work in here. +// Unity gives "(0,0): error System.Security.SecurityException: ECall methods must be packaged into a system module." +//using UnityEngine; + +namespace Mirror.Weaver +{ + public class ILPostProcessorHook : ILPostProcessor + { + // from CompilationFinishedHook + const string MirrorRuntimeAssemblyName = "Mirror"; + + // ILPostProcessor is invoked by Unity. + // we can not tell it to ignore certain assemblies before processing. + // add a 'ignore' define for convenience. + // => WeaverTests/WeaverAssembler need it to avoid Unity running it + public const string IgnoreDefine = "ILPP_IGNORE"; + + // we can't use Debug.Log in ILPP, so we need a custom logger + ILPostProcessorLogger Log = new ILPostProcessorLogger(); + + // ??? + public override ILPostProcessor GetInstance() => this; + + // check if assembly has the 'ignore' define + static bool HasDefine(ICompiledAssembly assembly, string define) => + assembly.Defines != null && + assembly.Defines.Contains(define); + + // process Mirror, or anything that references Mirror + public override bool WillProcess(ICompiledAssembly compiledAssembly) + { + // compiledAssembly.References are file paths: + // Library/Bee/artifacts/200b0aE.dag/Mirror.CompilerSymbols.dll + // Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll + // /Applications/Unity/Hub/Editor/2021.2.0b6_apple_silicon/Unity.app/Contents/NetStandard/ref/2.1.0/netstandard.dll + // + // log them to see: + // foreach (string reference in compiledAssembly.References) + // LogDiagnostics($"{compiledAssembly.Name} references {reference}"); + bool relevant = compiledAssembly.Name == MirrorRuntimeAssemblyName || + compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == MirrorRuntimeAssemblyName); + bool ignore = HasDefine(compiledAssembly, IgnoreDefine); + return relevant && !ignore; + } + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + //Log.Warning($"Processing {compiledAssembly.Name}"); + + // load the InMemoryAssembly peData into a MemoryStream + byte[] peData = compiledAssembly.InMemoryAssembly.PeData; + //LogDiagnostics($" peData.Length={peData.Length} bytes"); + using (MemoryStream stream = new MemoryStream(peData)) + using (ILPostProcessorAssemblyResolver asmResolver = new ILPostProcessorAssemblyResolver(compiledAssembly, Log)) + { + // we need to load symbols. otherwise we get: + // "(0,0): error Mono.CecilX.Cil.SymbolsNotFoundException: No symbol found for file: " + using (MemoryStream symbols = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData)) + { + ReaderParameters readerParameters = new ReaderParameters{ + SymbolStream = symbols, + ReadWrite = true, + ReadSymbols = true, + AssemblyResolver = asmResolver, + // custom reflection importer to fix System.Private.CoreLib + // not being found in custom assembly resolver above. + ReflectionImporterProvider = new ILPostProcessorReflectionImporterProvider() + }; + using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(stream, readerParameters)) + { + // resolving a Mirror.dll type like NetworkServer while + // weaving Mirror.dll does not work. it throws a + // NullReferenceException in WeaverTypes.ctor + // when Resolve() is called on the first Mirror type. + // need to add the AssemblyDefinition itself to use. + asmResolver.SetAssemblyDefinitionForCompiledAssembly(asmDef); + + // weave this assembly. + Weaver weaver = new Weaver(Log); + if (weaver.Weave(asmDef, asmResolver, out bool modified)) + { + //Log.Warning($"Weaving succeeded for: {compiledAssembly.Name}"); + + // write if modified + if (modified) + { + // when weaving Mirror.dll with ILPostProcessor, + // Weave() -> WeaverTypes -> resolving the first + // type in Mirror.dll adds a reference to + // Mirror.dll even though we are in Mirror.dll. + // -> this would throw an exception: + // "Mirror references itself" and not compile + // -> need to detect and fix manually here + if (asmDef.MainModule.AssemblyReferences.Any(r => r.Name == asmDef.Name.Name)) + { + asmDef.MainModule.AssemblyReferences.Remove(asmDef.MainModule.AssemblyReferences.First(r => r.Name == asmDef.Name.Name)); + //Log.Warning($"fixed self referencing Assembly: {asmDef.Name.Name}"); + } + + MemoryStream peOut = new MemoryStream(); + MemoryStream pdbOut = new MemoryStream(); + WriterParameters writerParameters = new WriterParameters + { + SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolStream = pdbOut, + WriteSymbols = true + }; + + asmDef.Write(peOut, writerParameters); + + InMemoryAssembly inMemory = new InMemoryAssembly(peOut.ToArray(), pdbOut.ToArray()); + return new ILPostProcessResult(inMemory, Log.Logs); + } + } + // if anything during Weave() fails, we log an error. + // don't need to indicate 'weaving failed' again. + // in fact, this would break tests only expecting certain errors. + //else Log.Error($"Weaving failed for: {compiledAssembly.Name}"); + } + } + } + + // always return an ILPostProcessResult with Logs. + // otherwise we won't see Logs if weaving failed. + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, Log.Logs); + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta new file mode 100644 index 0000000..9d7e0a2 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5f113eb695b348b5b28cd85358c8959a +timeCreated: 1628859074 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs new file mode 100644 index 0000000..2c070cc --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Mono.CecilX; +using Unity.CompilationPipeline.Common.Diagnostics; + +namespace Mirror.Weaver +{ + public class ILPostProcessorLogger : Logger + { + // can't Debug.Log in ILPostProcessor. need to add to this list. + internal List Logs = new List(); + + void Add(string message, DiagnosticType logType) + { + Logs.Add(new DiagnosticMessage + { + // TODO add file etc. for double click opening later? + DiagnosticType = logType, // doesn't have .Log + File = null, + Line = 0, + Column = 0, + MessageData = message + }); + } + + public void LogDiagnostics(string message, DiagnosticType logType = DiagnosticType.Warning) + { + // DiagnosticMessage can't display \n for some reason. + // it just cuts it off and we don't see any stack trace. + // so let's replace all line breaks so we get the stack trace. + // (Unity 2021.2.0b6 apple silicon) + //message = message.Replace("\n", "/"); + + // lets break it into several messages instead so it's easier readable + string[] lines = message.Split('\n'); + + // if it's just one line, simply log it + if (lines.Length == 1) + { + // tests assume exact message log. + // don't include 'Weaver: ...' or similar. + Add($"{message}", logType); + } + // for multiple lines, log each line separately with start/end indicators + else + { + // first line with Weaver: ... first + Add("----------------------------------------------", logType); + foreach (string line in lines) Add(line, logType); + Add("----------------------------------------------", logType); + } + } + + public void Warning(string message) => Warning(message, null); + public void Warning(string message, MemberReference mr) + { + if (mr != null) message = $"{message} (at {mr})"; + LogDiagnostics(message, DiagnosticType.Warning); + } + + public void Error(string message) => Error(message, null); + public void Error(string message, MemberReference mr) + { + if (mr != null) message = $"{message} (at {mr})"; + LogDiagnostics(message, DiagnosticType.Error); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta new file mode 100644 index 0000000..8bb72e0 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e7b56e7826664e34a415e4b70d958f2a +timeCreated: 1629533154 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs new file mode 100644 index 0000000..e15c103 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs @@ -0,0 +1,36 @@ +// based on paul's resolver from +// https://github.com/MirageNet/Mirage/commit/def64cd1db525398738f057b3d1eb1fe8afc540c?branch=def64cd1db525398738f057b3d1eb1fe8afc540c&diff=split +// +// ILPostProcessorAssemblyRESOLVER does not find the .dll file for: +// "System.Private.CoreLib" +// we need this custom reflection importer to fix that. +using System.Linq; +using System.Reflection; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + internal class ILPostProcessorReflectionImporter : DefaultReflectionImporter + { + const string SystemPrivateCoreLib = "System.Private.CoreLib"; + readonly AssemblyNameReference fixedCoreLib; + + public ILPostProcessorReflectionImporter(ModuleDefinition module) : base(module) + { + // find the correct library for "System.Private.CoreLib". + // either mscorlib or netstandard. + // defaults to System.Private.CoreLib if not found. + fixedCoreLib = module.AssemblyReferences.FirstOrDefault(a => a.Name == "mscorlib" || a.Name == "netstandard" || a.Name == SystemPrivateCoreLib); + } + + public override AssemblyNameReference ImportReference(AssemblyName name) + { + // System.Private.CoreLib? + if (name.Name == SystemPrivateCoreLib && fixedCoreLib != null) + return fixedCoreLib; + + // otherwise import as usual + return base.ImportReference(name); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta new file mode 100644 index 0000000..d361e21 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6403a7e3b3ae4e009ae282f111d266e0 +timeCreated: 1629709256 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs new file mode 100644 index 0000000..7358e1b --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs @@ -0,0 +1,16 @@ +// based on paul's resolver from +// https://github.com/MirageNet/Mirage/commit/def64cd1db525398738f057b3d1eb1fe8afc540c?branch=def64cd1db525398738f057b3d1eb1fe8afc540c&diff=split +// +// ILPostProcessorAssemblyRESOLVER does not find the .dll file for: +// "System.Private.CoreLib" +// we need this custom reflection importer to fix that. +using Mono.CecilX; + +namespace Mirror.Weaver +{ + internal class ILPostProcessorReflectionImporterProvider : IReflectionImporterProvider + { + public IReflectionImporter GetReflectionImporter(ModuleDefinition module) => + new ILPostProcessorReflectionImporter(module); + } +} diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta new file mode 100644 index 0000000..d9b6f6b --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1003b568bad4e69b961c4c81d5afd96 +timeCreated: 1629709223 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs b/Assets/Mirror/Editor/Weaver/Extensions.cs new file mode 100644 index 0000000..e5ddb1f --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + public static class Extensions + { + public static bool Is(this TypeReference td, Type type) => + type.IsGenericType + ? td.GetElementType().FullName == type.FullName + : td.FullName == type.FullName; + + public static bool Is(this TypeReference td) => Is(td, typeof(T)); + + public static bool IsDerivedFrom(this TypeReference tr) => IsDerivedFrom(tr, typeof(T)); + + public static bool IsDerivedFrom(this TypeReference tr, Type baseClass) + { + TypeDefinition td = tr.Resolve(); + if (!td.IsClass) + return false; + + // are ANY parent classes of baseClass? + TypeReference parent = td.BaseType; + + if (parent == null) + return false; + + if (parent.Is(baseClass)) + return true; + + if (parent.CanBeResolved()) + return IsDerivedFrom(parent.Resolve(), baseClass); + + return false; + } + + public static TypeReference GetEnumUnderlyingType(this TypeDefinition td) + { + foreach (FieldDefinition field in td.Fields) + { + if (!field.IsStatic) + return field.FieldType; + } + throw new ArgumentException($"Invalid enum {td.FullName}"); + } + + public static bool ImplementsInterface(this TypeDefinition td) + { + TypeDefinition typedef = td; + + while (typedef != null) + { + if (typedef.Interfaces.Any(iface => iface.InterfaceType.Is())) + return true; + + try + { + TypeReference parent = typedef.BaseType; + typedef = parent?.Resolve(); + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + return false; + } + + public static bool IsMultidimensionalArray(this TypeReference tr) => + tr is ArrayType arrayType && arrayType.Rank > 1; + + // Does type use netId as backing field + public static bool IsNetworkIdentityField(this TypeReference tr) => + tr.Is() || + tr.Is() || + tr.IsDerivedFrom(); + + public static bool CanBeResolved(this TypeReference parent) + { + while (parent != null) + { + if (parent.Scope.Name == "Windows") + { + return false; + } + + if (parent.Scope.Name == "mscorlib") + { + TypeDefinition resolved = parent.Resolve(); + return resolved != null; + } + + try + { + parent = parent.Resolve().BaseType; + } + catch + { + return false; + } + } + return true; + } + + // Makes T => Variable and imports function + public static MethodReference MakeGeneric(this MethodReference generic, ModuleDefinition module, TypeReference variableReference) + { + GenericInstanceMethod instance = new GenericInstanceMethod(generic); + instance.GenericArguments.Add(variableReference); + + MethodReference readFunc = module.ImportReference(instance); + return readFunc; + } + + // Given a method of a generic class such as ArraySegment`T.get_Count, + // and a generic instance such as ArraySegment`int + // Creates a reference to the specialized method ArraySegment`int`.get_Count + // Note that calling ArraySegment`T.get_Count directly gives an invalid IL error + public static MethodReference MakeHostInstanceGeneric(this MethodReference self, ModuleDefinition module, GenericInstanceType instanceType) + { + MethodReference reference = new MethodReference(self.Name, self.ReturnType, instanceType) + { + CallingConvention = self.CallingConvention, + HasThis = self.HasThis, + ExplicitThis = self.ExplicitThis + }; + + foreach (ParameterDefinition parameter in self.Parameters) + reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType)); + + foreach (GenericParameter generic_parameter in self.GenericParameters) + reference.GenericParameters.Add(new GenericParameter(generic_parameter.Name, reference)); + + return module.ImportReference(reference); + } + + // needed for NetworkBehaviour support + // https://github.com/vis2k/Mirror/pull/3073/ + public static FieldReference MakeHostInstanceGeneric(this FieldReference self) + { + var declaringType = new GenericInstanceType(self.DeclaringType); + foreach (var parameter in self.DeclaringType.GenericParameters) + { + declaringType.GenericArguments.Add(parameter); + } + return new FieldReference(self.Name, self.FieldType, declaringType); + } + + // Given a field of a generic class such as Writer.write, + // and a generic instance such as ArraySegment`int + // Creates a reference to the specialized method ArraySegment`int`.get_Count + // Note that calling ArraySegment`T.get_Count directly gives an invalid IL error + public static FieldReference SpecializeField(this FieldReference self, ModuleDefinition module, GenericInstanceType instanceType) + { + FieldReference reference = new FieldReference(self.Name, self.FieldType, instanceType); + return module.ImportReference(reference); + } + + public static CustomAttribute GetCustomAttribute(this ICustomAttributeProvider method) + { + return method.CustomAttributes.FirstOrDefault(ca => ca.AttributeType.Is()); + } + + public static bool HasCustomAttribute(this ICustomAttributeProvider attributeProvider) + { + return attributeProvider.CustomAttributes.Any(attr => attr.AttributeType.Is()); + } + + public static T GetField(this CustomAttribute ca, string field, T defaultValue) + { + foreach (CustomAttributeNamedArgument customField in ca.Fields) + if (customField.Name == field) + return (T)customField.Argument.Value; + return defaultValue; + } + + public static MethodDefinition GetMethod(this TypeDefinition td, string methodName) + { + return td.Methods.FirstOrDefault(method => method.Name == methodName); + } + + public static List GetMethods(this TypeDefinition td, string methodName) + { + return td.Methods.Where(method => method.Name == methodName).ToList(); + } + + public static MethodDefinition GetMethodInBaseType(this TypeDefinition td, string methodName) + { + TypeDefinition typedef = td; + while (typedef != null) + { + foreach (MethodDefinition md in typedef.Methods) + { + if (md.Name == methodName) + return md; + } + + try + { + TypeReference parent = typedef.BaseType; + typedef = parent?.Resolve(); + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + break; + } + } + + return null; + } + + // Finds public fields in type and base type + public static IEnumerable FindAllPublicFields(this TypeReference variable) + { + return FindAllPublicFields(variable.Resolve()); + } + + // Finds public fields in type and base type + public static IEnumerable FindAllPublicFields(this TypeDefinition typeDefinition) + { + while (typeDefinition != null) + { + foreach (FieldDefinition field in typeDefinition.Fields) + { + if (field.IsStatic || field.IsPrivate) + continue; + + if (field.IsNotSerialized) + continue; + + yield return field; + } + + try + { + typeDefinition = typeDefinition.BaseType?.Resolve(); + } + catch (AssemblyResolutionException) + { + break; + } + } + } + + public static bool ContainsClass(this ModuleDefinition module, string nameSpace, string className) => + module.GetTypes().Any(td => td.Namespace == nameSpace && + td.Name == className); + + + public static AssemblyNameReference FindReference(this ModuleDefinition module, string referenceName) + { + foreach (AssemblyNameReference reference in module.AssemblyReferences) + { + if (reference.Name == referenceName) + return reference; + } + return null; + } + + // Takes generic arguments from child class and applies them to parent reference, if possible + // eg makes `Base` in Child : Base have `int` instead of `T` + // Originally by James-Frowen under MIT + // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 + public static TypeReference ApplyGenericParameters(this TypeReference parentReference, + TypeReference childReference) + { + // If the parent is not generic, we got nothing to apply + if (!parentReference.IsGenericInstance) + return parentReference; + + GenericInstanceType parentGeneric = (GenericInstanceType)parentReference; + // make new type so we can replace the args on it + // resolve it so we have non-generic instance (eg just instance with instead of ) + // if we don't cecil will make it double generic (eg INVALID IL) + GenericInstanceType generic = new GenericInstanceType(parentReference.Resolve()); + foreach (TypeReference arg in parentGeneric.GenericArguments) + generic.GenericArguments.Add(arg); + + for (int i = 0; i < generic.GenericArguments.Count; i++) + { + // if arg is not generic + // eg List would be int so not generic. + // But List would be T so is generic + if (!generic.GenericArguments[i].IsGenericParameter) + continue; + + // get the generic name, eg T + string name = generic.GenericArguments[i].Name; + // find what type T is, eg turn it into `int` if `List` + TypeReference arg = FindMatchingGenericArgument(childReference, name); + + // import just to be safe + TypeReference imported = parentReference.Module.ImportReference(arg); + // set arg on generic, parent ref will be Base instead of just Base + generic.GenericArguments[i] = imported; + } + + return generic; + } + + // Finds the type reference for a generic parameter with the provided name in the child reference + // Originally by James-Frowen under MIT + // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 + static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName) + { + TypeDefinition def = childReference.Resolve(); + // child class must be generic if we are in this part of the code + // eg Child : Base <--- child must have generic if Base has T + // vs Child : Base <--- wont be here if Base has int (we check if T exists before calling this) + if (!def.HasGenericParameters) + throw new InvalidOperationException( + "Base class had generic parameters, but could not find them in child class"); + + // go through parameters in child class, and find the generic that matches the name + for (int i = 0; i < def.GenericParameters.Count; i++) + { + GenericParameter param = def.GenericParameters[i]; + if (param.Name == paramName) + { + GenericInstanceType generic = (GenericInstanceType)childReference; + // return generic arg with same index + return generic.GenericArguments[i]; + } + } + + // this should never happen, if it does it means that this code is bugged + throw new InvalidOperationException("Did not find matching generic"); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs.meta b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta new file mode 100644 index 0000000..78660f9 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 562a5cf0254cc45738e9aa549a7100b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Helpers.cs b/Assets/Mirror/Editor/Weaver/Helpers.cs new file mode 100644 index 0000000..56b7385 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Helpers.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + static class Helpers + { + // This code is taken from SerializationWeaver + public static string UnityEngineDllDirectoryName() + { + string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase); + return directoryName?.Replace(@"file:\", ""); + } + + public static bool IsEditorAssembly(AssemblyDefinition currentAssembly) + { + // we want to add the [InitializeOnLoad] attribute if it's available + // -> usually either 'UnityEditor' or 'UnityEditor.CoreModule' + return currentAssembly.MainModule.AssemblyReferences.Any(assemblyReference => + assemblyReference.Name.StartsWith(nameof(UnityEditor)) + ); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Helpers.cs.meta b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta new file mode 100644 index 0000000..231f539 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c4ed76daf48547c5abb7c58f8d20886 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Logger.cs b/Assets/Mirror/Editor/Weaver/Logger.cs new file mode 100644 index 0000000..8be978f --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Logger.cs @@ -0,0 +1,13 @@ +using Mono.CecilX; + +namespace Mirror.Weaver +{ + // not static, because ILPostProcessor is multithreaded + public interface Logger + { + void Warning(string message); + void Warning(string message, MemberReference mr); + void Error(string message); + void Error(string message, MemberReference mr); + } +} diff --git a/Assets/Mirror/Editor/Weaver/Logger.cs.meta b/Assets/Mirror/Editor/Weaver/Logger.cs.meta new file mode 100644 index 0000000..3f62978 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Logger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2a21c60c40a4c4d679c2b71a7c40882e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors.meta b/Assets/Mirror/Editor/Weaver/Processors.meta new file mode 100644 index 0000000..eb719b4 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e538d627280d2471b8c72fdea822ca49 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs new file mode 100644 index 0000000..55893f7 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs @@ -0,0 +1,124 @@ +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + // Processes [Command] methods in NetworkBehaviour + public static class CommandProcessor + { + /* + // generates code like: + public void CmdThrust(float thrusting, int spin) + { + NetworkWriter networkWriter = new NetworkWriter(); + networkWriter.Write(thrusting); + networkWriter.WritePackedUInt32((uint)spin); + base.SendCommandInternal(cmdName, networkWriter, channel); + } + + public void CallCmdThrust(float thrusting, int spin) + { + // whatever the user was doing before + } + + Originally HLAPI put the send message code inside the Call function + and then proceeded to replace every call to CmdTrust with CallCmdTrust + + This method moves all the user's code into the "CallCmd" method + and replaces the body of the original method with the send message code. + This way we do not need to modify the code anywhere else, and this works + correctly in dependent assemblies + */ + public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Writers writers, Logger Log, TypeDefinition td, MethodDefinition md, CustomAttribute commandAttr, ref bool WeavingFailed) + { + MethodDefinition cmd = MethodProcessor.SubstituteMethod(Log, td, md, ref WeavingFailed); + + ILProcessor worker = md.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes); + + // NetworkWriter writer = new NetworkWriter(); + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); + + // write all the arguments that the user passed to the Cmd call + if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.Command, ref WeavingFailed)) + return null; + + int channel = commandAttr.GetField("channel", 0); + bool requiresAuthority = commandAttr.GetField("requiresAuthority", true); + + // invoke internal send and return + // load 'base.' to call the SendCommand function with + worker.Emit(OpCodes.Ldarg_0); + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + worker.Emit(OpCodes.Ldstr, md.FullName); + // writer + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Ldc_I4, channel); + // requiresAuthority ? 1 : 0 + worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal); + + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); + + worker.Emit(OpCodes.Ret); + return cmd; + } + + /* + // generates code like: + protected static void InvokeCmdCmdThrust(NetworkBehaviour obj, NetworkReader reader, NetworkConnection senderConnection) + { + if (!NetworkServer.active) + { + return; + } + ((ShipControl)obj).CmdThrust(reader.ReadSingle(), (int)reader.ReadPackedUInt32()); + } + */ + public static MethodDefinition ProcessCommandInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition method, MethodDefinition cmdCallFunc, ref bool WeavingFailed) + { + string cmdName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, method); + + MethodDefinition cmd = new MethodDefinition(cmdName, + MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + ILProcessor worker = cmd.Body.GetILProcessor(); + Instruction label = worker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteServerActiveCheck(worker, weaverTypes, method.Name, label, "Command"); + + // setup for reader + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Castclass, td); + + if (!NetworkBehaviourProcessor.ReadArguments(method, readers, Log, worker, RemoteCallType.Command, ref WeavingFailed)) + return null; + + AddSenderConnection(method, worker); + + // invoke actual command function + worker.Emit(OpCodes.Callvirt, cmdCallFunc); + worker.Emit(OpCodes.Ret); + + NetworkBehaviourProcessor.AddInvokeParameters(weaverTypes, cmd.Parameters); + + td.Methods.Add(cmd); + return cmd; + } + + static void AddSenderConnection(MethodDefinition method, ILProcessor worker) + { + foreach (ParameterDefinition param in method.Parameters) + { + if (NetworkBehaviourProcessor.IsSenderConnection(param, RemoteCallType.Command)) + { + // NetworkConnection is 3nd arg (arg0 is "obj" not "this" because method is static) + // example: static void InvokeCmdCmdSendCommand(NetworkBehaviour obj, NetworkReader reader, NetworkConnection connection) + worker.Emit(OpCodes.Ldarg_2); + } + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta new file mode 100644 index 0000000..20c3e15 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73f6c9cdbb9e54f65b3a0a35cc8e55c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs new file mode 100644 index 0000000..8a4c581 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs @@ -0,0 +1,139 @@ +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class MethodProcessor + { + const string RpcPrefix = "UserCode_"; + + // For a function like + // [ClientRpc] void RpcTest(int value), + // Weaver substitutes the method and moves the code to a new method: + // UserCode_RpcTest(int value) <- contains original code + // RpcTest(int value) <- serializes parameters, sends the message + // + // Note that all the calls to the method remain untouched. + // FixRemoteCallToBaseMethod replaces them afterwards. + public static MethodDefinition SubstituteMethod(Logger Log, TypeDefinition td, MethodDefinition md, ref bool WeavingFailed) + { + string newName = Weaver.GenerateMethodName(RpcPrefix, md); + + MethodDefinition cmd = new MethodDefinition(newName, md.Attributes, md.ReturnType); + + // force the substitute method to be protected. + // -> public would show in the Inspector for UnityEvents as + // User_CmdUsePotion() etc. but the user shouldn't use those. + // -> private would not allow inheriting classes to call it, see + // OverrideVirtualWithBaseCallsBothVirtualAndBase test. + // -> IL has no concept of 'protected', it's called IsFamily there. + cmd.IsPublic = false; + cmd.IsFamily = true; + + // add parameters + foreach (ParameterDefinition pd in md.Parameters) + { + cmd.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType)); + } + + // swap bodies + (cmd.Body, md.Body) = (md.Body, cmd.Body); + + // Move over all the debugging information + foreach (SequencePoint sequencePoint in md.DebugInformation.SequencePoints) + cmd.DebugInformation.SequencePoints.Add(sequencePoint); + md.DebugInformation.SequencePoints.Clear(); + + foreach (CustomDebugInformation customInfo in md.CustomDebugInformations) + cmd.CustomDebugInformations.Add(customInfo); + md.CustomDebugInformations.Clear(); + + (md.DebugInformation.Scope, cmd.DebugInformation.Scope) = (cmd.DebugInformation.Scope, md.DebugInformation.Scope); + + td.Methods.Add(cmd); + + FixRemoteCallToBaseMethod(Log, td, cmd, ref WeavingFailed); + return cmd; + } + + // For a function like + // [ClientRpc] void RpcTest(int value), + // Weaver substitutes the method and moves the code to a new method: + // UserCode_RpcTest(int value) <- contains original code + // RpcTest(int value) <- serializes parameters, sends the message + // + // FixRemoteCallToBaseMethod replaces all calls to + // RpcTest(value) + // with + // UserCode_RpcTest(value) + public static void FixRemoteCallToBaseMethod(Logger Log, TypeDefinition type, MethodDefinition method, ref bool WeavingFailed) + { + string callName = method.Name; + + // Cmd/rpc start with Weaver.RpcPrefix + // e.g. CallCmdDoSomething + if (!callName.StartsWith(RpcPrefix)) + return; + + // e.g. CmdDoSomething + string baseRemoteCallName = method.Name.Substring(RpcPrefix.Length); + + foreach (Instruction instruction in method.Body.Instructions) + { + // is this instruction a Call to a method? + // if yes, output the method so we can check it. + if (IsCallToMethod(instruction, out MethodDefinition calledMethod)) + { + // when considering if 'calledMethod' is a call to 'method', + // we originally compared .Name. + // + // to fix IL2CPP build bugs with overloaded Rpcs, we need to + // generated rpc names like + // RpcTest(string value) => RpcTestString(strig value) + // RpcTest(int value) => RpcTestInt(int value) + // to make them unique. + // + // calledMethod.Name is still "RpcTest", so we need to + // convert this to the generated name as well before comparing. + string calledMethodName_Generated = Weaver.GenerateMethodName("", calledMethod); + if (calledMethodName_Generated == baseRemoteCallName) + { + TypeDefinition baseType = type.BaseType.Resolve(); + MethodDefinition baseMethod = baseType.GetMethodInBaseType(callName); + + if (baseMethod == null) + { + Log.Error($"Could not find base method for {callName}", method); + WeavingFailed = true; + return; + } + + if (!baseMethod.IsVirtual) + { + Log.Error($"Could not find base method that was virtual {callName}", method); + WeavingFailed = true; + return; + } + + instruction.Operand = baseMethod; + } + } + } + } + + static bool IsCallToMethod(Instruction instruction, out MethodDefinition calledMethod) + { + if (instruction.OpCode == OpCodes.Call && + instruction.Operand is MethodDefinition method) + { + calledMethod = method; + return true; + } + else + { + calledMethod = null; + return false; + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta new file mode 100644 index 0000000..3c81894 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 661e1af528e3441f79e1552fb5ec4e0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs new file mode 100644 index 0000000..e88c5d6 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs @@ -0,0 +1,56 @@ +using Mono.CecilX; + +namespace Mirror.Weaver +{ + // only shows warnings in case we use SyncVars etc. for MonoBehaviour. + static class MonoBehaviourProcessor + { + public static void Process(Logger Log, TypeDefinition td, ref bool WeavingFailed) + { + ProcessSyncVars(Log, td, ref WeavingFailed); + ProcessMethods(Log, td, ref WeavingFailed); + } + + static void ProcessSyncVars(Logger Log, TypeDefinition td, ref bool WeavingFailed) + { + // find syncvars + foreach (FieldDefinition fd in td.Fields) + { + if (fd.HasCustomAttribute()) + { + Log.Error($"SyncVar {fd.Name} must be inside a NetworkBehaviour. {td.Name} is not a NetworkBehaviour", fd); + WeavingFailed = true; + } + + if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) + { + Log.Error($"{fd.Name} is a SyncObject and must be inside a NetworkBehaviour. {td.Name} is not a NetworkBehaviour", fd); + WeavingFailed = true; + } + } + } + + static void ProcessMethods(Logger Log, TypeDefinition td, ref bool WeavingFailed) + { + // find command and RPC functions + foreach (MethodDefinition md in td.Methods) + { + if (md.HasCustomAttribute()) + { + Log.Error($"Command {md.Name} must be declared inside a NetworkBehaviour", md); + WeavingFailed = true; + } + if (md.HasCustomAttribute()) + { + Log.Error($"ClientRpc {md.Name} must be declared inside a NetworkBehaviour", md); + WeavingFailed = true; + } + if (md.HasCustomAttribute()) + { + Log.Error($"TargetRpc {md.Name} must be declared inside a NetworkBehaviour", md); + WeavingFailed = true; + } + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta new file mode 100644 index 0000000..ef3f5f4 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35c16722912b64af894e4f6668f2e54c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs new file mode 100644 index 0000000..ac00f65 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -0,0 +1,1012 @@ +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public enum RemoteCallType + { + Command, + ClientRpc, + TargetRpc + } + + // processes SyncVars, Cmds, Rpcs, etc. of NetworkBehaviours + class NetworkBehaviourProcessor + { + AssemblyDefinition assembly; + WeaverTypes weaverTypes; + SyncVarAccessLists syncVarAccessLists; + SyncVarAttributeProcessor syncVarAttributeProcessor; + Writers writers; + Readers readers; + Logger Log; + + List syncVars = new List(); + List syncObjects = new List(); + // + Dictionary syncVarNetIds = new Dictionary(); + readonly List commands = new List(); + readonly List clientRpcs = new List(); + readonly List targetRpcs = new List(); + readonly List commandInvocationFuncs = new List(); + readonly List clientRpcInvocationFuncs = new List(); + readonly List targetRpcInvocationFuncs = new List(); + + readonly TypeDefinition netBehaviourSubclass; + + public struct CmdResult + { + public MethodDefinition method; + public bool requiresAuthority; + } + + public struct ClientRpcResult + { + public MethodDefinition method; + public bool includeOwner; + } + + public NetworkBehaviourProcessor(AssemblyDefinition assembly, WeaverTypes weaverTypes, SyncVarAccessLists syncVarAccessLists, Writers writers, Readers readers, Logger Log, TypeDefinition td) + { + this.assembly = assembly; + this.weaverTypes = weaverTypes; + this.syncVarAccessLists = syncVarAccessLists; + this.writers = writers; + this.readers = readers; + this.Log = Log; + syncVarAttributeProcessor = new SyncVarAttributeProcessor(assembly, weaverTypes, syncVarAccessLists, Log); + netBehaviourSubclass = td; + } + + // return true if modified + public bool Process(ref bool WeavingFailed) + { + // only process once + if (WasProcessed(netBehaviourSubclass)) + { + return false; + } + + MarkAsProcessed(netBehaviourSubclass); + + // deconstruct tuple and set fields + (syncVars, syncVarNetIds) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed); + + syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed); + + ProcessMethods(ref WeavingFailed); + if (WeavingFailed) + { + // originally Process returned true in every case, except if already processed. + // maybe return false here in the future. + return true; + } + + // inject initializations into static & instance constructor + InjectIntoStaticConstructor(ref WeavingFailed); + InjectIntoInstanceConstructor(ref WeavingFailed); + + GenerateSerialization(ref WeavingFailed); + if (WeavingFailed) + { + // originally Process returned true in every case, except if already processed. + // maybe return false here in the future. + return true; + } + + GenerateDeSerialization(ref WeavingFailed); + return true; + } + + /* + generates code like: + if (!NetworkClient.active) + Debug.LogError((object) "Command function CmdRespawn called on server."); + + which is used in InvokeCmd, InvokeRpc, etc. + */ + public static void WriteClientActiveCheck(ILProcessor worker, WeaverTypes weaverTypes, string mdName, Instruction label, string errString) + { + // client active check + worker.Emit(OpCodes.Call, weaverTypes.NetworkClientGetActive); + worker.Emit(OpCodes.Brtrue, label); + + worker.Emit(OpCodes.Ldstr, $"{errString} {mdName} called on server."); + worker.Emit(OpCodes.Call, weaverTypes.logErrorReference); + worker.Emit(OpCodes.Ret); + worker.Append(label); + } + /* + generates code like: + if (!NetworkServer.active) + Debug.LogError((object) "Command CmdMsgWhisper called on client."); + */ + public static void WriteServerActiveCheck(ILProcessor worker, WeaverTypes weaverTypes, string mdName, Instruction label, string errString) + { + // server active check + worker.Emit(OpCodes.Call, weaverTypes.NetworkServerGetActive); + worker.Emit(OpCodes.Brtrue, label); + + worker.Emit(OpCodes.Ldstr, $"{errString} {mdName} called on client."); + worker.Emit(OpCodes.Call, weaverTypes.logErrorReference); + worker.Emit(OpCodes.Ret); + worker.Append(label); + } + + public static void WriteSetupLocals(ILProcessor worker, WeaverTypes weaverTypes) + { + worker.Body.InitLocals = true; + worker.Body.Variables.Add(new VariableDefinition(weaverTypes.Import())); + } + + public static void WriteGetWriter(ILProcessor worker, WeaverTypes weaverTypes) + { + // create writer + worker.Emit(OpCodes.Call, weaverTypes.GetWriterReference); + worker.Emit(OpCodes.Stloc_0); + } + + public static void WriteReturnWriter(ILProcessor worker, WeaverTypes weaverTypes) + { + // NetworkWriterPool.Recycle(writer); + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Call, weaverTypes.ReturnWriterReference); + } + + public static bool WriteArguments(ILProcessor worker, Writers writers, Logger Log, MethodDefinition method, RemoteCallType callType, ref bool WeavingFailed) + { + // write each argument + // example result + /* + writer.WritePackedInt32(someNumber); + writer.WriteNetworkIdentity(someTarget); + */ + + bool skipFirst = callType == RemoteCallType.TargetRpc + && TargetRpcProcessor.HasNetworkConnectionParameter(method); + + // arg of calling function, arg 0 is "this" so start counting at 1 + int argNum = 1; + foreach (ParameterDefinition param in method.Parameters) + { + // NetworkConnection is not sent via the NetworkWriter so skip it here + // skip first for NetworkConnection in TargetRpc + if (argNum == 1 && skipFirst) + { + argNum += 1; + continue; + } + // skip SenderConnection in Command + if (IsSenderConnection(param, callType)) + { + argNum += 1; + continue; + } + + MethodReference writeFunc = writers.GetWriteFunc(param.ParameterType, ref WeavingFailed); + if (writeFunc == null) + { + Log.Error($"{method.Name} has invalid parameter {param}", method); + WeavingFailed = true; + return false; + } + + // use built-in writer func on writer object + // NetworkWriter object + worker.Emit(OpCodes.Ldloc_0); + // add argument to call + worker.Emit(OpCodes.Ldarg, argNum); + // call writer extension method + worker.Emit(OpCodes.Call, writeFunc); + argNum += 1; + } + return true; + } + + #region mark / check type as processed + public const string ProcessedFunctionName = "MirrorProcessed"; + + // by adding an empty MirrorProcessed() function + public static bool WasProcessed(TypeDefinition td) + { + return td.GetMethod(ProcessedFunctionName) != null; + } + + public void MarkAsProcessed(TypeDefinition td) + { + if (!WasProcessed(td)) + { + MethodDefinition versionMethod = new MethodDefinition(ProcessedFunctionName, MethodAttributes.Private, weaverTypes.Import(typeof(void))); + ILProcessor worker = versionMethod.Body.GetILProcessor(); + worker.Emit(OpCodes.Ret); + td.Methods.Add(versionMethod); + } + } + #endregion + + // helper function to remove 'Ret' from the end of the method if 'Ret' + // is the last instruction. + // returns false if there was an issue + static bool RemoveFinalRetInstruction(MethodDefinition method) + { + // remove the return opcode from end of function. will add our own later. + if (method.Body.Instructions.Count != 0) + { + Instruction retInstr = method.Body.Instructions[method.Body.Instructions.Count - 1]; + if (retInstr.OpCode == OpCodes.Ret) + { + method.Body.Instructions.RemoveAt(method.Body.Instructions.Count - 1); + return true; + } + return false; + } + + // we did nothing, but there was no error. + return true; + } + + // we need to inject several initializations into NetworkBehaviour cctor + void InjectIntoStaticConstructor(ref bool WeavingFailed) + { + if (commands.Count == 0 && clientRpcs.Count == 0 && targetRpcs.Count == 0) + return; + + // find static constructor + MethodDefinition cctor = netBehaviourSubclass.GetMethod(".cctor"); + bool cctorFound = cctor != null; + if (cctor != null) + { + // remove the return opcode from end of function. will add our own later. + if (!RemoveFinalRetInstruction(cctor)) + { + Log.Error($"{netBehaviourSubclass.Name} has invalid class constructor", cctor); + WeavingFailed = true; + return; + } + } + else + { + // make one! + cctor = new MethodDefinition(".cctor", MethodAttributes.Private | + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + weaverTypes.Import(typeof(void))); + } + + ILProcessor cctorWorker = cctor.Body.GetILProcessor(); + + // register all commands in cctor + for (int i = 0; i < commands.Count; ++i) + { + CmdResult cmdResult = commands[i]; + GenerateRegisterCommandDelegate(cctorWorker, weaverTypes.registerCommandReference, commandInvocationFuncs[i], cmdResult); + } + + // register all client rpcs in cctor + for (int i = 0; i < clientRpcs.Count; ++i) + { + ClientRpcResult clientRpcResult = clientRpcs[i]; + GenerateRegisterRemoteDelegate(cctorWorker, weaverTypes.registerRpcReference, clientRpcInvocationFuncs[i], clientRpcResult.method.FullName); + } + + // register all target rpcs in cctor + for (int i = 0; i < targetRpcs.Count; ++i) + { + GenerateRegisterRemoteDelegate(cctorWorker, weaverTypes.registerRpcReference, targetRpcInvocationFuncs[i], targetRpcs[i].FullName); + } + + // add final 'Ret' instruction to cctor + cctorWorker.Append(cctorWorker.Create(OpCodes.Ret)); + if (!cctorFound) + { + netBehaviourSubclass.Methods.Add(cctor); + } + + // in case class had no cctor, it might have BeforeFieldInit, so injected cctor would be called too late + netBehaviourSubclass.Attributes &= ~TypeAttributes.BeforeFieldInit; + } + + // we need to inject several initializations into NetworkBehaviour ctor + void InjectIntoInstanceConstructor(ref bool WeavingFailed) + { + if (syncObjects.Count == 0) + return; + + // find instance constructor + MethodDefinition ctor = netBehaviourSubclass.GetMethod(".ctor"); + if (ctor == null) + { + Log.Error($"{netBehaviourSubclass.Name} has invalid constructor", netBehaviourSubclass); + WeavingFailed = true; + return; + } + + // remove the return opcode from end of function. will add our own later. + if (!RemoveFinalRetInstruction(ctor)) + { + Log.Error($"{netBehaviourSubclass.Name} has invalid constructor", ctor); + WeavingFailed = true; + return; + } + + ILProcessor ctorWorker = ctor.Body.GetILProcessor(); + + // initialize all sync objects in ctor + foreach (FieldDefinition fd in syncObjects) + { + SyncObjectInitializer.GenerateSyncObjectInitializer(ctorWorker, weaverTypes, fd); + } + + // add final 'Ret' instruction to ctor + ctorWorker.Append(ctorWorker.Create(OpCodes.Ret)); + } + + /* + // This generates code like: + NetworkBehaviour.RegisterCommandDelegate(base.GetType(), "CmdThrust", new NetworkBehaviour.CmdDelegate(ShipControl.InvokeCmdCmdThrust)); + */ + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + void GenerateRegisterRemoteDelegate(ILProcessor worker, MethodReference registerMethod, MethodDefinition func, string functionFullName) + { + worker.Emit(OpCodes.Ldtoken, netBehaviourSubclass); + worker.Emit(OpCodes.Call, weaverTypes.getTypeFromHandleReference); + worker.Emit(OpCodes.Ldstr, functionFullName); + worker.Emit(OpCodes.Ldnull); + worker.Emit(OpCodes.Ldftn, func); + + worker.Emit(OpCodes.Newobj, weaverTypes.RemoteCallDelegateConstructor); + // + worker.Emit(OpCodes.Call, registerMethod); + } + + void GenerateRegisterCommandDelegate(ILProcessor worker, MethodReference registerMethod, MethodDefinition func, CmdResult cmdResult) + { + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + string cmdName = cmdResult.method.FullName; + bool requiresAuthority = cmdResult.requiresAuthority; + + worker.Emit(OpCodes.Ldtoken, netBehaviourSubclass); + worker.Emit(OpCodes.Call, weaverTypes.getTypeFromHandleReference); + worker.Emit(OpCodes.Ldstr, cmdName); + worker.Emit(OpCodes.Ldnull); + worker.Emit(OpCodes.Ldftn, func); + + worker.Emit(OpCodes.Newobj, weaverTypes.RemoteCallDelegateConstructor); + + // requiresAuthority ? 1 : 0 + worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + + worker.Emit(OpCodes.Call, registerMethod); + } + + void GenerateSerialization(ref bool WeavingFailed) + { + const string SerializeMethodName = "SerializeSyncVars"; + if (netBehaviourSubclass.GetMethod(SerializeMethodName) != null) + return; + + if (syncVars.Count == 0) + { + // no synvars, no need for custom OnSerialize + return; + } + + MethodDefinition serialize = new MethodDefinition(SerializeMethodName, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + weaverTypes.Import()); + + serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); + serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import())); + ILProcessor worker = serialize.Body.GetILProcessor(); + + serialize.Body.InitLocals = true; + + // loc_0, this local variable is to determine if any variable was dirty + VariableDefinition dirtyLocal = new VariableDefinition(weaverTypes.Import()); + serialize.Body.Variables.Add(dirtyLocal); + + MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName); + if (baseSerialize != null) + { + // base + worker.Emit(OpCodes.Ldarg_0); + // writer + worker.Emit(OpCodes.Ldarg_1); + // forceAll + worker.Emit(OpCodes.Ldarg_2); + worker.Emit(OpCodes.Call, baseSerialize); + // set dirtyLocal to result of base.OnSerialize() + worker.Emit(OpCodes.Stloc_0); + } + + // Generates: if (forceAll); + Instruction initialStateLabel = worker.Create(OpCodes.Nop); + // forceAll + worker.Emit(OpCodes.Ldarg_2); + worker.Emit(OpCodes.Brfalse, initialStateLabel); + + foreach (FieldDefinition syncVarDef in syncVars) + { + FieldReference syncVar = syncVarDef; + if (netBehaviourSubclass.HasGenericParameters) + { + syncVar = syncVarDef.MakeHostInstanceGeneric(); + } + // Generates a writer call for each sync variable + // writer + worker.Emit(OpCodes.Ldarg_1); + // this + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, syncVar); + MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + if (writeFunc != null) + { + worker.Emit(OpCodes.Call, writeFunc); + } + else + { + Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar); + WeavingFailed = true; + return; + } + } + + // always return true if forceAll + + // Generates: return true + worker.Emit(OpCodes.Ldc_I4_1); + worker.Emit(OpCodes.Ret); + + // Generates: end if (forceAll); + worker.Append(initialStateLabel); + + // write dirty bits before the data fields + // Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ()); + // writer + worker.Emit(OpCodes.Ldarg_1); + // base + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + worker.Emit(OpCodes.Call, writeUint64Func); + + // generate a writer call for any dirty variable in this class + + // start at number of syncvars in parent + int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); + foreach (FieldDefinition syncVarDef in syncVars) + { + + FieldReference syncVar = syncVarDef; + if (netBehaviourSubclass.HasGenericParameters) + { + syncVar = syncVarDef.MakeHostInstanceGeneric(); + } + Instruction varLabel = worker.Create(OpCodes.Nop); + + // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) + // base + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + // 8 bytes = long + worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit); + worker.Emit(OpCodes.And); + worker.Emit(OpCodes.Brfalse, varLabel); + + // Generates a call to the writer for that field + // writer + worker.Emit(OpCodes.Ldarg_1); + // base + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, syncVar); + + MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + if (writeFunc != null) + { + worker.Emit(OpCodes.Call, writeFunc); + } + else + { + Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar); + WeavingFailed = true; + return; + } + + // something was dirty + worker.Emit(OpCodes.Ldc_I4_1); + // set dirtyLocal to true + worker.Emit(OpCodes.Stloc_0); + + worker.Append(varLabel); + dirtyBit += 1; + } + + // add a log message if needed for debugging + //worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}"); + //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); + + // generate: return dirtyLocal + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Ret); + netBehaviourSubclass.Methods.Add(serialize); + } + + void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool WeavingFailed) + { + // put 'this.' onto stack for 'this.syncvar' below + worker.Append(worker.Create(OpCodes.Ldarg_0)); + + // push 'ref T this.field' + worker.Emit(OpCodes.Ldarg_0); + // if the netbehaviour class is generic, we need to make the field reference generic as well for correct IL + if (netBehaviourSubclass.HasGenericParameters) + { + worker.Emit(OpCodes.Ldflda, syncVar.MakeHostInstanceGeneric()); + } + else + { + worker.Emit(OpCodes.Ldflda, syncVar); + } + + // hook? then push 'new Action(Hook)' onto stack + MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed); + if (hookMethod != null) + { + syncVarAttributeProcessor.GenerateNewActionFromHookMethod(syncVar, worker, hookMethod); + } + // otherwise push 'null' as hook + else + { + worker.Emit(OpCodes.Ldnull); + } + + // call GeneratedSyncVarDeserialize. + // special cases for GameObject/NetworkIdentity/NetworkBehaviour + // passing netId too for persistence. + if (syncVar.FieldType.Is()) + { + // reader + worker.Emit(OpCodes.Ldarg_1); + + // GameObject setter needs one more parameter: netId field ref + FieldDefinition netIdField = syncVarNetIds[syncVar]; + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdField); + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject); + } + else if (syncVar.FieldType.Is()) + { + // reader + worker.Emit(OpCodes.Ldarg_1); + + // NetworkIdentity deserialize needs one more parameter: netId field ref + FieldDefinition netIdField = syncVarNetIds[syncVar]; + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdField); + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); + } + // TODO this only uses the persistent netId for types DERIVED FROM NB. + // not if the type is just 'NetworkBehaviour'. + // this is what original implementation did too. fix it after. + else if (syncVar.FieldType.IsDerivedFrom()) + { + // reader + worker.Emit(OpCodes.Ldarg_1); + + // NetworkIdentity deserialize needs one more parameter: netId field ref + // (actually its a NetworkBehaviourSyncVar type) + FieldDefinition netIdField = syncVarNetIds[syncVar]; + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdField); + // make generic version of GeneratedSyncVarSetter_NetworkBehaviour + MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, getFunc); + } + else + { + // T value = reader.ReadT(); + // this is still in IL because otherwise weaver generated + // readers/writers don't seem to work in tests. + // besides, this also avoids reader.Read overhead. + MethodReference readFunc = readers.GetReadFunc(syncVar.FieldType, ref WeavingFailed); + if (readFunc == null) + { + Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar); + WeavingFailed = true; + return; + } + // reader. for 'reader.Read()' below + worker.Emit(OpCodes.Ldarg_1); + // reader.Read() + worker.Emit(OpCodes.Call, readFunc); + + // make generic version of GeneratedSyncVarDeserialize + MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, generic); + } + } + + void GenerateDeSerialization(ref bool WeavingFailed) + { + const string DeserializeMethodName = "DeserializeSyncVars"; + if (netBehaviourSubclass.GetMethod(DeserializeMethodName) != null) + return; + + if (syncVars.Count == 0) + { + // no synvars, no need for custom OnDeserialize + return; + } + + MethodDefinition serialize = new MethodDefinition(DeserializeMethodName, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + serialize.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, weaverTypes.Import())); + serialize.Parameters.Add(new ParameterDefinition("initialState", ParameterAttributes.None, weaverTypes.Import())); + ILProcessor serWorker = serialize.Body.GetILProcessor(); + // setup local for dirty bits + serialize.Body.InitLocals = true; + VariableDefinition dirtyBitsLocal = new VariableDefinition(weaverTypes.Import()); + serialize.Body.Variables.Add(dirtyBitsLocal); + + MethodReference baseDeserialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, DeserializeMethodName); + if (baseDeserialize != null) + { + // base + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + // reader + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + // initialState + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); + serWorker.Append(serWorker.Create(OpCodes.Call, baseDeserialize)); + } + + // Generates: if (initialState); + Instruction initialStateLabel = serWorker.Create(OpCodes.Nop); + + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); + serWorker.Append(serWorker.Create(OpCodes.Brfalse, initialStateLabel)); + + foreach (FieldDefinition syncVar in syncVars) + { + DeserializeField(syncVar, serWorker, ref WeavingFailed); + } + + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + // Generates: end if (initialState); + serWorker.Append(initialStateLabel); + + // get dirty bits + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, readers.GetReadFunc(weaverTypes.Import(), ref WeavingFailed))); + serWorker.Append(serWorker.Create(OpCodes.Stloc_0)); + + // conditionally read each syncvar + // start at number of syncvars in parent + int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); + foreach (FieldDefinition syncVar in syncVars) + { + Instruction varLabel = serWorker.Create(OpCodes.Nop); + + // check if dirty bit is set + serWorker.Append(serWorker.Create(OpCodes.Ldloc_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldc_I8, 1L << dirtyBit)); + serWorker.Append(serWorker.Create(OpCodes.And)); + serWorker.Append(serWorker.Create(OpCodes.Brfalse, varLabel)); + + DeserializeField(syncVar, serWorker, ref WeavingFailed); + + serWorker.Append(varLabel); + dirtyBit += 1; + } + + // add a log message if needed for debugging + //serWorker.Append(serWorker.Create(OpCodes.Ldstr, $"Injected Deserialize {netBehaviourSubclass.Name}")); + //serWorker.Append(serWorker.Create(OpCodes.Call, WeaverTypes.logErrorReference)); + + serWorker.Append(serWorker.Create(OpCodes.Ret)); + netBehaviourSubclass.Methods.Add(serialize); + } + + public static bool ReadArguments(MethodDefinition method, Readers readers, Logger Log, ILProcessor worker, RemoteCallType callType, ref bool WeavingFailed) + { + // read each argument + // example result + /* + CallCmdDoSomething(reader.ReadPackedInt32(), reader.ReadNetworkIdentity()); + */ + + bool skipFirst = callType == RemoteCallType.TargetRpc + && TargetRpcProcessor.HasNetworkConnectionParameter(method); + + // arg of calling function, arg 0 is "this" so start counting at 1 + int argNum = 1; + foreach (ParameterDefinition param in method.Parameters) + { + // NetworkConnection is not sent via the NetworkWriter so skip it here + // skip first for NetworkConnection in TargetRpc + if (argNum == 1 && skipFirst) + { + argNum += 1; + continue; + } + // skip SenderConnection in Command + if (IsSenderConnection(param, callType)) + { + argNum += 1; + continue; + } + + + MethodReference readFunc = readers.GetReadFunc(param.ParameterType, ref WeavingFailed); + + if (readFunc == null) + { + Log.Error($"{method.Name} has invalid parameter {param}. Unsupported type {param.ParameterType}, use a supported Mirror type instead", method); + WeavingFailed = true; + return false; + } + + worker.Emit(OpCodes.Ldarg_1); + worker.Emit(OpCodes.Call, readFunc); + + // conversion.. is this needed? + if (param.ParameterType.Is()) + { + worker.Emit(OpCodes.Conv_R4); + } + else if (param.ParameterType.Is()) + { + worker.Emit(OpCodes.Conv_R8); + } + } + return true; + } + + public static void AddInvokeParameters(WeaverTypes weaverTypes, ICollection collection) + { + collection.Add(new ParameterDefinition("obj", ParameterAttributes.None, weaverTypes.Import())); + collection.Add(new ParameterDefinition("reader", ParameterAttributes.None, weaverTypes.Import())); + // senderConnection is only used for commands but NetworkBehaviour.CmdDelegate is used for all remote calls + collection.Add(new ParameterDefinition("senderConnection", ParameterAttributes.None, weaverTypes.Import())); + } + + // check if a Command/TargetRpc/Rpc function & parameters are valid for weaving + public bool ValidateRemoteCallAndParameters(MethodDefinition method, RemoteCallType callType, ref bool WeavingFailed) + { + if (method.IsStatic) + { + Log.Error($"{method.Name} must not be static", method); + WeavingFailed = true; + return false; + } + + return ValidateFunction(method, ref WeavingFailed) && + ValidateParameters(method, callType, ref WeavingFailed); + } + + // check if a Command/TargetRpc/Rpc function is valid for weaving + bool ValidateFunction(MethodReference md, ref bool WeavingFailed) + { + if (md.ReturnType.Is()) + { + Log.Error($"{md.Name} cannot be a coroutine", md); + WeavingFailed = true; + return false; + } + if (!md.ReturnType.Is(typeof(void))) + { + Log.Error($"{md.Name} cannot return a value. Make it void instead", md); + WeavingFailed = true; + return false; + } + if (md.HasGenericParameters) + { + Log.Error($"{md.Name} cannot have generic parameters", md); + WeavingFailed = true; + return false; + } + return true; + } + + // check if all Command/TargetRpc/Rpc function's parameters are valid for weaving + bool ValidateParameters(MethodReference method, RemoteCallType callType, ref bool WeavingFailed) + { + for (int i = 0; i < method.Parameters.Count; ++i) + { + ParameterDefinition param = method.Parameters[i]; + if (!ValidateParameter(method, param, callType, i == 0, ref WeavingFailed)) + { + return false; + } + } + return true; + } + + // validate parameters for a remote function call like Rpc/Cmd + bool ValidateParameter(MethodReference method, ParameterDefinition param, RemoteCallType callType, bool firstParam, ref bool WeavingFailed) + { + // need to check this before any type lookups since those will fail since generic types don't resolve + if (param.ParameterType.IsGenericParameter) + { + Log.Error($"{method.Name} cannot have generic parameters", method); + WeavingFailed = true; + return false; + } + + bool isNetworkConnection = param.ParameterType.Is(); + bool isSenderConnection = IsSenderConnection(param, callType); + + if (param.IsOut) + { + Log.Error($"{method.Name} cannot have out parameters", method); + WeavingFailed = true; + return false; + } + + // if not SenderConnection And not TargetRpc NetworkConnection first param + if (!isSenderConnection && isNetworkConnection && !(callType == RemoteCallType.TargetRpc && firstParam)) + { + if (callType == RemoteCallType.Command) + { + Log.Error($"{method.Name} has invalid parameter {param}, Cannot pass NetworkConnections. Instead use 'NetworkConnectionToClient conn = null' to get the sender's connection on the server", method); + } + else + { + Log.Error($"{method.Name} has invalid parameter {param}. Cannot pass NetworkConnections", method); + } + WeavingFailed = true; + return false; + } + + // sender connection can be optional + if (param.IsOptional && !isSenderConnection) + { + Log.Error($"{method.Name} cannot have optional parameters", method); + WeavingFailed = true; + return false; + } + + return true; + } + + public static bool IsSenderConnection(ParameterDefinition param, RemoteCallType callType) + { + if (callType != RemoteCallType.Command) + { + return false; + } + + TypeReference type = param.ParameterType; + + return type.Is() + || type.Resolve().IsDerivedFrom(); + } + + void ProcessMethods(ref bool WeavingFailed) + { + HashSet names = new HashSet(); + + // copy the list of methods because we will be adding methods in the loop + List methods = new List(netBehaviourSubclass.Methods); + // find command and RPC functions + foreach (MethodDefinition md in methods) + { + foreach (CustomAttribute ca in md.CustomAttributes) + { + if (ca.AttributeType.Is()) + { + ProcessCommand(names, md, ca, ref WeavingFailed); + break; + } + + if (ca.AttributeType.Is()) + { + ProcessTargetRpc(names, md, ca, ref WeavingFailed); + break; + } + + if (ca.AttributeType.Is()) + { + ProcessClientRpc(names, md, ca, ref WeavingFailed); + break; + } + } + } + } + + void ProcessClientRpc(HashSet names, MethodDefinition md, CustomAttribute clientRpcAttr, ref bool WeavingFailed) + { + if (md.IsAbstract) + { + Log.Error("Abstract ClientRpc are currently not supported, use virtual method instead", md); + WeavingFailed = true; + return; + } + + if (!ValidateRemoteCallAndParameters(md, RemoteCallType.ClientRpc, ref WeavingFailed)) + { + return; + } + + bool includeOwner = clientRpcAttr.GetField("includeOwner", true); + + names.Add(md.Name); + clientRpcs.Add(new ClientRpcResult + { + method = md, + includeOwner = includeOwner + }); + + MethodDefinition rpcCallFunc = RpcProcessor.ProcessRpcCall(weaverTypes, writers, Log, netBehaviourSubclass, md, clientRpcAttr, ref WeavingFailed); + // need null check here because ProcessRpcCall returns null if it can't write all the args + if (rpcCallFunc == null) { return; } + + MethodDefinition rpcFunc = RpcProcessor.ProcessRpcInvoke(weaverTypes, writers, readers, Log, netBehaviourSubclass, md, rpcCallFunc, ref WeavingFailed); + if (rpcFunc != null) + { + clientRpcInvocationFuncs.Add(rpcFunc); + } + } + + void ProcessTargetRpc(HashSet names, MethodDefinition md, CustomAttribute targetRpcAttr, ref bool WeavingFailed) + { + if (md.IsAbstract) + { + Log.Error("Abstract TargetRpc are currently not supported, use virtual method instead", md); + WeavingFailed = true; + return; + } + + if (!ValidateRemoteCallAndParameters(md, RemoteCallType.TargetRpc, ref WeavingFailed)) + return; + + names.Add(md.Name); + targetRpcs.Add(md); + + MethodDefinition rpcCallFunc = TargetRpcProcessor.ProcessTargetRpcCall(weaverTypes, writers, Log, netBehaviourSubclass, md, targetRpcAttr, ref WeavingFailed); + + MethodDefinition rpcFunc = TargetRpcProcessor.ProcessTargetRpcInvoke(weaverTypes, readers, Log, netBehaviourSubclass, md, rpcCallFunc, ref WeavingFailed); + if (rpcFunc != null) + { + targetRpcInvocationFuncs.Add(rpcFunc); + } + } + + void ProcessCommand(HashSet names, MethodDefinition md, CustomAttribute commandAttr, ref bool WeavingFailed) + { + if (md.IsAbstract) + { + Log.Error("Abstract Commands are currently not supported, use virtual method instead", md); + WeavingFailed = true; + return; + } + + if (!ValidateRemoteCallAndParameters(md, RemoteCallType.Command, ref WeavingFailed)) + return; + + bool requiresAuthority = commandAttr.GetField("requiresAuthority", true); + + names.Add(md.Name); + commands.Add(new CmdResult + { + method = md, + requiresAuthority = requiresAuthority + }); + + MethodDefinition cmdCallFunc = CommandProcessor.ProcessCommandCall(weaverTypes, writers, Log, netBehaviourSubclass, md, commandAttr, ref WeavingFailed); + + MethodDefinition cmdFunc = CommandProcessor.ProcessCommandInvoke(weaverTypes, readers, Log, netBehaviourSubclass, md, cmdCallFunc, ref WeavingFailed); + if (cmdFunc != null) + { + commandInvocationFuncs.Add(cmdFunc); + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta new file mode 100644 index 0000000..67c27dc --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8118d606be3214e5d99943ec39530dd8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs new file mode 100644 index 0000000..280240c --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs @@ -0,0 +1,216 @@ +// finds all readers and writers and register them +using System.Linq; +using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Rocks; +using UnityEngine; + +namespace Mirror.Weaver +{ + public static class ReaderWriterProcessor + { + public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed) + { + // find NetworkReader/Writer extensions from Mirror.dll first. + // and NetworkMessage custom writer/reader extensions. + // NOTE: do not include this result in our 'modified' return value, + // otherwise Unity crashes when running tests + ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed); + + // find readers/writers in the assembly we are in right now. + return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed); + } + + static void ProcessMirrorAssemblyClasses(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed) + { + // find Mirror.dll in assembly's references. + // those are guaranteed to be resolvable and correct. + // after all, it references them :) + AssemblyNameReference mirrorAssemblyReference = CurrentAssembly.MainModule.FindReference(Weaver.MirrorAssemblyName); + if (mirrorAssemblyReference != null) + { + // resolve the assembly to load the AssemblyDefinition. + // we need to search all types in it. + // if we only were to resolve one known type like in WeaverTypes, + // then we wouldn't need it. + AssemblyDefinition mirrorAssembly = resolver.Resolve(mirrorAssemblyReference); + if (mirrorAssembly != null) + { + ProcessAssemblyClasses(CurrentAssembly, mirrorAssembly, writers, readers, ref WeavingFailed); + } + else Log.Error($"Failed to resolve {mirrorAssemblyReference}"); + } + else Log.Error("Failed to find Mirror AssemblyNameReference. Can't register Mirror.dll readers/writers."); + } + + static bool ProcessAssemblyClasses(AssemblyDefinition CurrentAssembly, AssemblyDefinition assembly, Writers writers, Readers readers, ref bool WeavingFailed) + { + bool modified = false; + foreach (TypeDefinition klass in assembly.MainModule.Types) + { + // extension methods only live in static classes + // static classes are represented as sealed and abstract + if (klass.IsAbstract && klass.IsSealed) + { + // if assembly has any declared writers then it is "modified" + modified |= LoadDeclaredWriters(CurrentAssembly, klass, writers); + modified |= LoadDeclaredReaders(CurrentAssembly, klass, readers); + } + } + + foreach (TypeDefinition klass in assembly.MainModule.Types) + { + // if assembly has any network message then it is modified + modified |= LoadMessageReadWriter(CurrentAssembly.MainModule, writers, readers, klass, ref WeavingFailed); + } + return modified; + } + + static bool LoadMessageReadWriter(ModuleDefinition module, Writers writers, Readers readers, TypeDefinition klass, ref bool WeavingFailed) + { + bool modified = false; + if (!klass.IsAbstract && !klass.IsInterface && klass.ImplementsInterface()) + { + readers.GetReadFunc(module.ImportReference(klass), ref WeavingFailed); + writers.GetWriteFunc(module.ImportReference(klass), ref WeavingFailed); + modified = true; + } + + foreach (TypeDefinition td in klass.NestedTypes) + { + modified |= LoadMessageReadWriter(module, writers, readers, td, ref WeavingFailed); + } + return modified; + } + + static bool LoadDeclaredWriters(AssemblyDefinition currentAssembly, TypeDefinition klass, Writers writers) + { + // register all the writers in this class. Skip the ones with wrong signature + bool modified = false; + foreach (MethodDefinition method in klass.Methods) + { + if (method.Parameters.Count != 2) + continue; + + if (!method.Parameters[0].ParameterType.Is()) + continue; + + if (!method.ReturnType.Is(typeof(void))) + continue; + + if (!method.HasCustomAttribute()) + continue; + + if (method.HasGenericParameters) + continue; + + TypeReference dataType = method.Parameters[1].ParameterType; + writers.Register(dataType, currentAssembly.MainModule.ImportReference(method)); + modified = true; + } + return modified; + } + + static bool LoadDeclaredReaders(AssemblyDefinition currentAssembly, TypeDefinition klass, Readers readers) + { + // register all the reader in this class. Skip the ones with wrong signature + bool modified = false; + foreach (MethodDefinition method in klass.Methods) + { + if (method.Parameters.Count != 1) + continue; + + if (!method.Parameters[0].ParameterType.Is()) + continue; + + if (method.ReturnType.Is(typeof(void))) + continue; + + if (!method.HasCustomAttribute()) + continue; + + if (method.HasGenericParameters) + continue; + + readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method)); + modified = true; + } + return modified; + } + + // helper function to add [RuntimeInitializeOnLoad] attribute to method + static void AddRuntimeInitializeOnLoadAttribute(AssemblyDefinition assembly, WeaverTypes weaverTypes, MethodDefinition method) + { + // NOTE: previously we used reflection because according paul, + // 'weaving Mirror.dll caused unity to rebuild all dlls but in wrong + // order, which breaks rewired' + // it's not obvious why importing an attribute via reflection instead + // of cecil would break anything. let's use cecil. + + // to add a CustomAttribute, we need the attribute's constructor. + // in this case, there are two: empty, and RuntimeInitializeOnLoadType. + // we want the last one, with the type parameter. + MethodDefinition ctor = weaverTypes.runtimeInitializeOnLoadMethodAttribute.GetConstructors().Last(); + //MethodDefinition ctor = weaverTypes.runtimeInitializeOnLoadMethodAttribute.GetConstructors().First(); + // using ctor directly throws: ArgumentException: Member 'System.Void UnityEditor.InitializeOnLoadMethodAttribute::.ctor()' is declared in another module and needs to be imported + // we need to import it first. + CustomAttribute attribute = new CustomAttribute(assembly.MainModule.ImportReference(ctor)); + // add the RuntimeInitializeLoadType.BeforeSceneLoad argument to ctor + attribute.ConstructorArguments.Add(new CustomAttributeArgument(weaverTypes.Import(), RuntimeInitializeLoadType.BeforeSceneLoad)); + method.CustomAttributes.Add(attribute); + } + + // helper function to add [InitializeOnLoad] attribute to method + // (only works in Editor assemblies. check IsEditorAssembly first.) + static void AddInitializeOnLoadAttribute(AssemblyDefinition assembly, WeaverTypes weaverTypes, MethodDefinition method) + { + // NOTE: previously we used reflection because according paul, + // 'weaving Mirror.dll caused unity to rebuild all dlls but in wrong + // order, which breaks rewired' + // it's not obvious why importing an attribute via reflection instead + // of cecil would break anything. let's use cecil. + + // to add a CustomAttribute, we need the attribute's constructor. + // in this case, there's only one - and it's an empty constructor. + MethodDefinition ctor = weaverTypes.initializeOnLoadMethodAttribute.GetConstructors().First(); + // using ctor directly throws: ArgumentException: Member 'System.Void UnityEditor.InitializeOnLoadMethodAttribute::.ctor()' is declared in another module and needs to be imported + // we need to import it first. + CustomAttribute attribute = new CustomAttribute(assembly.MainModule.ImportReference(ctor)); + method.CustomAttributes.Add(attribute); + } + + // adds Mirror.GeneratedNetworkCode.InitReadWriters() method that + // registers all generated writers into Mirror.Writer static class. + // -> uses [RuntimeInitializeOnLoad] attribute so it's invoke at runtime + // -> uses [InitializeOnLoad] if UnityEditor is referenced so it works + // in Editor and in tests too + // + // use ILSpy to see the result (it's in the DLL's 'Mirror' namespace) + public static void InitializeReaderAndWriters(AssemblyDefinition currentAssembly, WeaverTypes weaverTypes, Writers writers, Readers readers, TypeDefinition GeneratedCodeClass) + { + MethodDefinition initReadWriters = new MethodDefinition("InitReadWriters", MethodAttributes.Public | + MethodAttributes.Static, + weaverTypes.Import(typeof(void))); + + // add [RuntimeInitializeOnLoad] in any case + AddRuntimeInitializeOnLoadAttribute(currentAssembly, weaverTypes, initReadWriters); + + // add [InitializeOnLoad] if UnityEditor is referenced + if (Helpers.IsEditorAssembly(currentAssembly)) + { + AddInitializeOnLoadAttribute(currentAssembly, weaverTypes, initReadWriters); + } + + // fill function body with reader/writer initializers + ILProcessor worker = initReadWriters.Body.GetILProcessor(); + // for debugging: add a log to see if initialized on load + //worker.Emit(OpCodes.Ldstr, $"[InitReadWriters] called!"); + //worker.Emit(OpCodes.Call, Weaver.weaverTypes.logWarningReference); + writers.InitializeWriters(worker); + readers.InitializeReaders(worker); + worker.Emit(OpCodes.Ret); + + GeneratedCodeClass.Methods.Add(initReadWriters); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta new file mode 100644 index 0000000..c14d6fa --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3263602f0a374ecd8d08588b1fc2f76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs new file mode 100644 index 0000000..df44f20 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs @@ -0,0 +1,99 @@ +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + // Processes [Rpc] methods in NetworkBehaviour + public static class RpcProcessor + { + public static MethodDefinition ProcessRpcInvoke(WeaverTypes weaverTypes, Writers writers, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) + { + string rpcName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md); + + MethodDefinition rpc = new MethodDefinition(rpcName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + ILProcessor worker = rpc.Body.GetILProcessor(); + Instruction label = worker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteClientActiveCheck(worker, weaverTypes, md.Name, label, "RPC"); + + // setup for reader + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Castclass, td); + + if (!NetworkBehaviourProcessor.ReadArguments(md, readers, Log, worker, RemoteCallType.ClientRpc, ref WeavingFailed)) + return null; + + // invoke actual command function + worker.Emit(OpCodes.Callvirt, rpcCallFunc); + worker.Emit(OpCodes.Ret); + + NetworkBehaviourProcessor.AddInvokeParameters(weaverTypes, rpc.Parameters); + td.Methods.Add(rpc); + return rpc; + } + + /* + * generates code like: + + public void RpcTest (int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32((uint)param); + base.SendRPCInternal(typeof(class),"RpcTest", writer, 0); + } + public void CallRpcTest (int param) + { + // whatever the user did before + } + + Originally HLAPI put the send message code inside the Call function + and then proceeded to replace every call to RpcTest with CallRpcTest + + This method moves all the user's code into the "CallRpc" method + and replaces the body of the original method with the send message code. + This way we do not need to modify the code anywhere else, and this works + correctly in dependent assemblies + */ + public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers writers, Logger Log, TypeDefinition td, MethodDefinition md, CustomAttribute clientRpcAttr, ref bool WeavingFailed) + { + MethodDefinition rpc = MethodProcessor.SubstituteMethod(Log, td, md, ref WeavingFailed); + + ILProcessor worker = md.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes); + + // add a log message if needed for debugging + //worker.Emit(OpCodes.Ldstr, $"Call ClientRpc function {md.Name}"); + //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); + + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); + + // write all the arguments that the user passed to the Rpc call + if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.ClientRpc, ref WeavingFailed)) + return null; + + int channel = clientRpcAttr.GetField("channel", 0); + bool includeOwner = clientRpcAttr.GetField("includeOwner", true); + + // invoke SendInternal and return + // this + worker.Emit(OpCodes.Ldarg_0); + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + worker.Emit(OpCodes.Ldstr, md.FullName); + // writer + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Ldc_I4, channel); + // includeOwner ? 1 : 0 + worker.Emit(includeOwner ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + worker.Emit(OpCodes.Callvirt, weaverTypes.sendRpcInternal); + + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); + + worker.Emit(OpCodes.Ret); + + return rpc; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta new file mode 100644 index 0000000..22375ba --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3cb7051ff41947e59bba58bdd2b73fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs new file mode 100644 index 0000000..50df598 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs @@ -0,0 +1,154 @@ +// Injects server/client active checks for [Server/Client] attributes +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + static class ServerClientAttributeProcessor + { + public static bool Process(WeaverTypes weaverTypes, Logger Log, TypeDefinition td, ref bool WeavingFailed) + { + bool modified = false; + foreach (MethodDefinition md in td.Methods) + { + modified |= ProcessSiteMethod(weaverTypes, Log, md, ref WeavingFailed); + } + + foreach (TypeDefinition nested in td.NestedTypes) + { + modified |= Process(weaverTypes, Log, nested, ref WeavingFailed); + } + return modified; + } + + static bool ProcessSiteMethod(WeaverTypes weaverTypes, Logger Log, MethodDefinition md, ref bool WeavingFailed) + { + if (md.Name == ".cctor" || + md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || + md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + return false; + + if (md.IsAbstract) + { + if (HasServerClientAttribute(md)) + { + Log.Error("Server or Client Attributes can't be added to abstract method. Server and Client Attributes are not inherited so they need to be applied to the override methods instead.", md); + WeavingFailed = true; + } + return false; + } + + if (md.Body != null && md.Body.Instructions != null) + { + return ProcessMethodAttributes(weaverTypes, md); + } + return false; + } + + public static bool HasServerClientAttribute(MethodDefinition md) + { + foreach (CustomAttribute attr in md.CustomAttributes) + { + switch (attr.Constructor.DeclaringType.ToString()) + { + case "Mirror.ServerAttribute": + case "Mirror.ServerCallbackAttribute": + case "Mirror.ClientAttribute": + case "Mirror.ClientCallbackAttribute": + return true; + default: + break; + } + } + return false; + } + + public static bool ProcessMethodAttributes(WeaverTypes weaverTypes, MethodDefinition md) + { + if (md.HasCustomAttribute()) + InjectServerGuard(weaverTypes, md, true); + else if (md.HasCustomAttribute()) + InjectServerGuard(weaverTypes, md, false); + else if (md.HasCustomAttribute()) + InjectClientGuard(weaverTypes, md, true); + else if (md.HasCustomAttribute()) + InjectClientGuard(weaverTypes, md, false); + else + return false; + + return true; + } + + static void InjectServerGuard(WeaverTypes weaverTypes, MethodDefinition md, bool logWarning) + { + ILProcessor worker = md.Body.GetILProcessor(); + Instruction top = md.Body.Instructions[0]; + + worker.InsertBefore(top, worker.Create(OpCodes.Call, weaverTypes.NetworkServerGetActive)); + worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top)); + if (logWarning) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldstr, $"[Server] function '{md.FullName}' called when server was not active")); + worker.InsertBefore(top, worker.Create(OpCodes.Call, weaverTypes.logWarningReference)); + } + InjectGuardParameters(md, worker, top); + InjectGuardReturnValue(md, worker, top); + worker.InsertBefore(top, worker.Create(OpCodes.Ret)); + } + + static void InjectClientGuard(WeaverTypes weaverTypes, MethodDefinition md, bool logWarning) + { + ILProcessor worker = md.Body.GetILProcessor(); + Instruction top = md.Body.Instructions[0]; + + worker.InsertBefore(top, worker.Create(OpCodes.Call, weaverTypes.NetworkClientGetActive)); + worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top)); + if (logWarning) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldstr, $"[Client] function '{md.FullName}' called when client was not active")); + worker.InsertBefore(top, worker.Create(OpCodes.Call, weaverTypes.logWarningReference)); + } + + InjectGuardParameters(md, worker, top); + InjectGuardReturnValue(md, worker, top); + worker.InsertBefore(top, worker.Create(OpCodes.Ret)); + } + + // this is required to early-out from a function with "ref" or "out" parameters + static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instruction top) + { + int offset = md.Resolve().IsStatic ? 0 : 1; + for (int index = 0; index < md.Parameters.Count; index++) + { + ParameterDefinition param = md.Parameters[index]; + if (param.IsOut) + { + TypeReference elementType = param.ParameterType.GetElementType(); + + md.Body.Variables.Add(new VariableDefinition(elementType)); + md.Body.InitLocals = true; + + worker.InsertBefore(top, worker.Create(OpCodes.Ldarg, index + offset)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloca_S, (byte)(md.Body.Variables.Count - 1))); + worker.InsertBefore(top, worker.Create(OpCodes.Initobj, elementType)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloc, md.Body.Variables.Count - 1)); + worker.InsertBefore(top, worker.Create(OpCodes.Stobj, elementType)); + } + } + } + + // this is required to early-out from a function with a return value. + static void InjectGuardReturnValue(MethodDefinition md, ILProcessor worker, Instruction top) + { + if (!md.ReturnType.Is(typeof(void))) + { + md.Body.Variables.Add(new VariableDefinition(md.ReturnType)); + md.Body.InitLocals = true; + + worker.InsertBefore(top, worker.Create(OpCodes.Ldloca_S, (byte)(md.Body.Variables.Count - 1))); + worker.InsertBefore(top, worker.Create(OpCodes.Initobj, md.ReturnType)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloc, md.Body.Variables.Count - 1)); + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta new file mode 100644 index 0000000..5a5451d --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 024f251bf693bb345b90b9177892d534 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs new file mode 100644 index 0000000..a174403 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs @@ -0,0 +1,39 @@ +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncObjectInitializer + { + // generates code like: + // this.InitSyncObject(m_sizes); + public static void GenerateSyncObjectInitializer(ILProcessor worker, WeaverTypes weaverTypes, FieldDefinition fd) + { + // register syncobject in network behaviour + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, fd); + worker.Emit(OpCodes.Call, weaverTypes.InitSyncObjectReference); + } + + public static bool ImplementsSyncObject(TypeReference typeRef) + { + try + { + // value types cant inherit from SyncObject + if (typeRef.IsValueType) + { + return false; + } + + return typeRef.Resolve().IsDerivedFrom(); + } + catch + { + // sometimes this will fail if we reference a weird library that can't be resolved, so we just swallow that exception and return false + } + + return false; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta new file mode 100644 index 0000000..22f976e --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d02219b00b3674e59a2151f41e791688 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs new file mode 100644 index 0000000..2cec8a4 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + public static class SyncObjectProcessor + { + // ulong = 64 bytes + const int SyncObjectsLimit = 64; + + // Finds SyncObjects fields in a type + // Type should be a NetworkBehaviour + public static List FindSyncObjectsFields(Writers writers, Readers readers, Logger Log, TypeDefinition td, ref bool WeavingFailed) + { + List syncObjects = new List(); + + foreach (FieldDefinition fd in td.Fields) + { + if (fd.FieldType.IsGenericParameter) + { + // can't call .Resolve on generic ones + continue; + } + + if (fd.FieldType.Resolve().IsDerivedFrom()) + { + if (fd.IsStatic) + { + Log.Error($"{fd.Name} cannot be static", fd); + WeavingFailed = true; + continue; + } + + // SyncObjects always needs to be readonly to guarantee. + // Weaver calls InitSyncObject on them for dirty bits etc. + // Reassigning at runtime would cause undefined behaviour. + // (C# 'readonly' is called 'initonly' in IL code.) + // + // NOTE: instead of forcing readonly, we could also scan all + // instructions for SyncObject assignments. this would + // make unit tests very difficult though. + if (!fd.IsInitOnly) + { + // just a warning for now. + // many people might still use non-readonly SyncObjects. + Log.Warning($"{fd.Name} should have a 'readonly' keyword in front of the variable because {typeof(SyncObject)}s always need to be initialized by the Weaver.", fd); + + // only log, but keep weaving. no need to break projects. + //WeavingFailed = true; + } + + GenerateReadersAndWriters(writers, readers, fd.FieldType, ref WeavingFailed); + + syncObjects.Add(fd); + } + } + + // SyncObjects dirty mask is 64 bit. can't sync more than 64. + if (syncObjects.Count > 64) + { + Log.Error($"{td.Name} has > {SyncObjectsLimit} SyncObjects (SyncLists etc). Consider refactoring your class into multiple components", td); + WeavingFailed = true; + } + + + return syncObjects; + } + + // Generates serialization methods for synclists + static void GenerateReadersAndWriters(Writers writers, Readers readers, TypeReference tr, ref bool WeavingFailed) + { + if (tr is GenericInstanceType genericInstance) + { + foreach (TypeReference argument in genericInstance.GenericArguments) + { + if (!argument.IsGenericParameter) + { + readers.GetReadFunc(argument, ref WeavingFailed); + writers.GetWriteFunc(argument, ref WeavingFailed); + } + } + } + + if (tr != null) + { + GenerateReadersAndWriters(writers, readers, tr.Resolve().BaseType, ref WeavingFailed); + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta new file mode 100644 index 0000000..0efe434 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78f71efc83cde4917b7d21efa90bcc9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs new file mode 100644 index 0000000..0a6f376 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs @@ -0,0 +1,174 @@ +// [SyncVar] int health; +// is replaced with: +// public int Networkhealth { get; set; } properties. +// this class processes all access to 'health' and replaces it with 'Networkhealth' +using System; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncVarAttributeAccessReplacer + { + // process the module + public static void Process(ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) + { + DateTime startTime = DateTime.Now; + + // process all classes in this module + foreach (TypeDefinition td in moduleDef.Types) + { + if (td.IsClass) + { + ProcessClass(syncVarAccessLists, td); + } + } + + Console.WriteLine($" ProcessSitesModule {moduleDef.Name} elapsed time:{(DateTime.Now - startTime)}"); + } + + static void ProcessClass(SyncVarAccessLists syncVarAccessLists, TypeDefinition td) + { + //Console.WriteLine($" ProcessClass {td}"); + + // process all methods in this class + foreach (MethodDefinition md in td.Methods) + { + ProcessMethod(syncVarAccessLists, md); + } + + // processes all nested classes in this class recursively + foreach (TypeDefinition nested in td.NestedTypes) + { + ProcessClass(syncVarAccessLists, nested); + } + } + + static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinition md) + { + // process all references to replaced members with properties + //Log.Warning($" ProcessSiteMethod {md}"); + + // skip static constructor, "MirrorProcessed", "InvokeUserCode_" + if (md.Name == ".cctor" || + md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || + md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + return; + + // skip abstract + if (md.IsAbstract) + { + return; + } + + // go through all instructions of this method + if (md.Body != null && md.Body.Instructions != null) + { + for (int i = 0; i < md.Body.Instructions.Count;) + { + Instruction instr = md.Body.Instructions[i]; + i += ProcessInstruction(syncVarAccessLists, md, instr, i); + } + } + } + + static int ProcessInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) + { + // stfld (sets value of a field)? + if (instr.OpCode == OpCodes.Stfld && instr.Operand is FieldDefinition opFieldst) + { + ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); + } + + // ldfld (load value of a field)? + if (instr.OpCode == OpCodes.Ldfld && instr.Operand is FieldDefinition opFieldld) + { + // this instruction gets the value of a field. cache the field reference. + ProcessGetInstruction(syncVarAccessLists, md, instr, opFieldld); + } + + // ldflda (load field address aka reference) + if (instr.OpCode == OpCodes.Ldflda && instr.Operand is FieldDefinition opFieldlda) + { + // watch out for initobj instruction + // see https://github.com/vis2k/Mirror/issues/696 + return ProcessLoadAddressInstruction(syncVarAccessLists, md, instr, opFieldlda, iCount); + } + + // we processed one instruction (instr) + return 1; + } + + // replaces syncvar write access with the NetworkXYZ.set property calls + static void ProcessSetInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction i, FieldDefinition opField) + { + // don't replace property call sites in constructors + if (md.Name == ".ctor") + return; + + // does it set a field that we replaced? + if (syncVarAccessLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + //replace with property + //Log.Warning($" replacing {md.Name}:{i}", opField); + i.OpCode = OpCodes.Call; + i.Operand = replacement; + //Log.Warning($" replaced {md.Name}:{i}", opField); + } + } + + // replaces syncvar read access with the NetworkXYZ.get property calls + static void ProcessGetInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction i, FieldDefinition opField) + { + // don't replace property call sites in constructors + if (md.Name == ".ctor") + return; + + // does it set a field that we replaced? + if (syncVarAccessLists.replacementGetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + //replace with property + //Log.Warning($" replacing {md.Name}:{i}"); + i.OpCode = OpCodes.Call; + i.Operand = replacement; + //Log.Warning($" replaced {md.Name}:{i}"); + } + } + + static int ProcessLoadAddressInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, FieldDefinition opField, int iCount) + { + // don't replace property call sites in constructors + if (md.Name == ".ctor") + return 1; + + // does it set a field that we replaced? + if (syncVarAccessLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + // we have a replacement for this property + // is the next instruction a initobj? + Instruction nextInstr = md.Body.Instructions[iCount + 1]; + + if (nextInstr.OpCode == OpCodes.Initobj) + { + // we need to replace this code with: + // var tmp = new MyStruct(); + // this.set_Networkxxxx(tmp); + ILProcessor worker = md.Body.GetILProcessor(); + VariableDefinition tmpVariable = new VariableDefinition(opField.FieldType); + md.Body.Variables.Add(tmpVariable); + + worker.InsertBefore(instr, worker.Create(OpCodes.Ldloca, tmpVariable)); + worker.InsertBefore(instr, worker.Create(OpCodes.Initobj, opField.FieldType)); + worker.InsertBefore(instr, worker.Create(OpCodes.Ldloc, tmpVariable)); + worker.InsertBefore(instr, worker.Create(OpCodes.Call, replacement)); + + worker.Remove(instr); + worker.Remove(nextInstr); + return 4; + } + } + + return 1; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta new file mode 100644 index 0000000..e8c2500 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d48f1ab125e9940a995603796bccc59e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs new file mode 100644 index 0000000..a5f95cd --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -0,0 +1,483 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Rocks; + +namespace Mirror.Weaver +{ + // Processes [SyncVar] attribute fields in NetworkBehaviour + // not static, because ILPostProcessor is multithreaded + public class SyncVarAttributeProcessor + { + // ulong = 64 bytes + const int SyncVarLimit = 64; + + AssemblyDefinition assembly; + WeaverTypes weaverTypes; + SyncVarAccessLists syncVarAccessLists; + Logger Log; + + string HookParameterMessage(string hookName, TypeReference ValueType) => + $"void {hookName}({ValueType} oldValue, {ValueType} newValue)"; + + public SyncVarAttributeProcessor(AssemblyDefinition assembly, WeaverTypes weaverTypes, SyncVarAccessLists syncVarAccessLists, Logger Log) + { + this.assembly = assembly; + this.weaverTypes = weaverTypes; + this.syncVarAccessLists = syncVarAccessLists; + this.Log = Log; + } + + // Get hook method if any + public MethodDefinition GetHookMethod(TypeDefinition td, FieldDefinition syncVar, ref bool WeavingFailed) + { + CustomAttribute syncVarAttr = syncVar.GetCustomAttribute(); + + if (syncVarAttr == null) + return null; + + string hookFunctionName = syncVarAttr.GetField("hook", null); + + if (hookFunctionName == null) + return null; + + return FindHookMethod(td, syncVar, hookFunctionName, ref WeavingFailed); + } + + // push hook from GetHookMethod() onto the stack as a new Action. + // allows for reuse without handling static/virtual cases every time. + public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor worker, MethodDefinition hookMethod) + { + // IL_000a: ldarg.0 + // IL_000b: ldftn instance void Mirror.Examples.Tanks.Tank::ExampleHook(int32, int32) + // IL_0011: newobj instance void class [netstandard]System.Action`2::.ctor(object, native int) + + // we support static hook sand instance hooks. + if (hookMethod.IsStatic) + { + // for static hooks, we need to push 'null' first. + // we can't just push nothing. + // stack would get out of balance because we already pushed + // other stuff above. + worker.Emit(OpCodes.Ldnull); + } + else + { + // for instance hooks, we need to push 'this.' first. + worker.Emit(OpCodes.Ldarg_0); + } + + MethodReference hookMethodReference; + // if the network behaviour class is generic, we need to make the method reference generic for correct IL + if (hookMethod.DeclaringType.HasGenericParameters) + { + hookMethodReference = hookMethod.MakeHostInstanceGeneric(hookMethod.Module, hookMethod.DeclaringType.MakeGenericInstanceType(hookMethod.DeclaringType.GenericParameters.ToArray())); + } + else + { + hookMethodReference = hookMethod; + } + + // we support regular and virtual hook functions. + if (hookMethod.IsVirtual) + { + // for virtual / overwritten hooks, we need different IL. + // this is from simply testing Action = VirtualHook; in C#. + worker.Emit(OpCodes.Dup); + worker.Emit(OpCodes.Ldvirtftn, hookMethodReference); + } + else + { + worker.Emit(OpCodes.Ldftn, hookMethodReference); + } + + // call 'new Action()' constructor to convert the function to an action + // we need to make an instance of the generic Action. + // + // TODO this allocates a new 'Action' for every SyncVar hook call. + // we should allocate it once and store it somewhere in the future. + // hooks are only called on the client though, so it's not too bad for now. + TypeReference actionRef = assembly.MainModule.ImportReference(typeof(Action<,>)); + GenericInstanceType genericInstance = actionRef.MakeGenericInstanceType(syncVar.FieldType, syncVar.FieldType); + worker.Emit(OpCodes.Newobj, weaverTypes.ActionT_T.MakeHostInstanceGeneric(assembly.MainModule, genericInstance)); + } + + MethodDefinition FindHookMethod(TypeDefinition td, FieldDefinition syncVar, string hookFunctionName, ref bool WeavingFailed) + { + List methods = td.GetMethods(hookFunctionName); + + List methodsWith2Param = new List(methods.Where(m => m.Parameters.Count == 2)); + + if (methodsWith2Param.Count == 0) + { + Log.Error($"Could not find hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " + + $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}", + syncVar); + WeavingFailed = true; + + return null; + } + + foreach (MethodDefinition method in methodsWith2Param) + { + if (MatchesParameters(syncVar, method)) + { + return method; + } + } + + Log.Error($"Wrong type for Parameter in hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " + + $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}", + syncVar); + WeavingFailed = true; + + return null; + } + + bool MatchesParameters(FieldDefinition syncVar, MethodDefinition method) + { + // matches void onValueChange(T oldValue, T newValue) + return method.Parameters[0].ParameterType.FullName == syncVar.FieldType.FullName && + method.Parameters[1].ParameterType.FullName == syncVar.FieldType.FullName; + } + + public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string originalName, FieldDefinition netFieldId) + { + //Create the get method + MethodDefinition get = new MethodDefinition( + $"get_Network{originalName}", MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + fd.FieldType); + + ILProcessor worker = get.Body.GetILProcessor(); + + FieldReference fr; + if (fd.DeclaringType.HasGenericParameters) + { + fr = fd.MakeHostInstanceGeneric(); + } + else + { + fr = fd; + } + + FieldReference netIdFieldReference = null; + if (netFieldId != null) + { + if (netFieldId.DeclaringType.HasGenericParameters) + { + netIdFieldReference = netFieldId.MakeHostInstanceGeneric(); + } + else + { + netIdFieldReference = netFieldId; + } + } + + // [SyncVar] GameObject? + if (fd.FieldType.Is()) + { + // return this.GetSyncVarGameObject(ref field, uint netId); + // this. + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, netIdFieldReference); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, fr); + worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference); + worker.Emit(OpCodes.Ret); + } + // [SyncVar] NetworkIdentity? + else if (fd.FieldType.Is()) + { + // return this.GetSyncVarNetworkIdentity(ref field, uint netId); + // this. + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, netIdFieldReference); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, fr); + worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference); + worker.Emit(OpCodes.Ret); + } + else if (fd.FieldType.IsDerivedFrom()) + { + // return this.GetSyncVarNetworkBehaviour(ref field, uint netId); + // this. + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, netIdFieldReference); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, fr); + MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType); + worker.Emit(OpCodes.Call, getFunc); + worker.Emit(OpCodes.Ret); + } + // [SyncVar] int, string, etc. + else + { + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, fr); + worker.Emit(OpCodes.Ret); + } + + get.Body.Variables.Add(new VariableDefinition(fd.FieldType)); + get.Body.InitLocals = true; + get.SemanticsAttributes = MethodSemanticsAttributes.Getter; + + return get; + } + + // for [SyncVar] health, weaver generates + // + // NetworkHealth + // { + // get => health; + // set => GeneratedSyncVarSetter(...) + // } + // + // the setter used to be manually IL generated, but we moved it to C# :) + public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId, ref bool WeavingFailed) + { + //Create the set method + MethodDefinition set = new MethodDefinition($"set_Network{originalName}", MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + ILProcessor worker = set.Body.GetILProcessor(); + FieldReference fr; + if (fd.DeclaringType.HasGenericParameters) + { + fr = fd.MakeHostInstanceGeneric(); + } + else + { + fr = fd; + } + + FieldReference netIdFieldReference = null; + if (netFieldId != null) + { + if (netFieldId.DeclaringType.HasGenericParameters) + { + netIdFieldReference = netFieldId.MakeHostInstanceGeneric(); + } + else + { + netIdFieldReference = netFieldId; + } + } + + // if (!SyncVarEqual(value, ref playerData)) + Instruction endOfMethod = worker.Create(OpCodes.Nop); + + // NOTE: SyncVar...Equal functions are static. + // don't Emit Ldarg_0 aka 'this'. + + // call WeaverSyncVarSetter(T value, ref T field, ulong dirtyBit, Action OnChanged = null) + // IL_0000: ldarg.0 + // IL_0001: ldarg.1 + // IL_0002: ldarg.0 + // IL_0003: ldflda int32 Mirror.Examples.Tanks.Tank::health + // IL_0008: ldc.i4.1 + // IL_0009: conv.i8 + // IL_000a: ldnull + // IL_000b: call instance void [Mirror]Mirror.NetworkBehaviour::GeneratedSyncVarSetter(!!0, !!0&, uint64, class [netstandard]System.Action`2) + // IL_0010: ret + + // 'this.' for the call + worker.Emit(OpCodes.Ldarg_0); + + // first push 'value' + worker.Emit(OpCodes.Ldarg_1); + + // push 'ref T this.field' + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, fr); + + // push the dirty bit for this SyncVar + worker.Emit(OpCodes.Ldc_I8, dirtyBit); + + // hook? then push 'new Action(Hook)' onto stack + MethodDefinition hookMethod = GetHookMethod(td, fd, ref WeavingFailed); + if (hookMethod != null) + { + GenerateNewActionFromHookMethod(fd, worker, hookMethod); + } + // otherwise push 'null' as hook + else + { + worker.Emit(OpCodes.Ldnull); + } + + // call GeneratedSyncVarSetter. + // special cases for GameObject/NetworkIdentity/NetworkBehaviour + // passing netId too for persistence. + if (fd.FieldType.Is()) + { + // GameObject setter needs one more parameter: netId field ref + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdFieldReference); + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_GameObject); + } + else if (fd.FieldType.Is()) + { + // NetworkIdentity setter needs one more parameter: netId field ref + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdFieldReference); + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity); + } + // TODO this only uses the persistent netId for types DERIVED FROM NB. + // not if the type is just 'NetworkBehaviour'. + // this is what original implementation did too. fix it after. + else if (fd.FieldType.IsDerivedFrom()) + { + // NetworkIdentity setter needs one more parameter: netId field ref + // (actually its a NetworkBehaviourSyncVar type) + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldflda, netIdFieldReference); + // make generic version of GeneratedSyncVarSetter_NetworkBehaviour + MethodReference getFunc = weaverTypes.generatedSyncVarSetter_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, fd.FieldType); + worker.Emit(OpCodes.Call, getFunc); + } + else + { + // make generic version of GeneratedSyncVarSetter + MethodReference generic = weaverTypes.generatedSyncVarSetter.MakeGeneric(assembly.MainModule, fd.FieldType); + worker.Emit(OpCodes.Call, generic); + } + + worker.Append(endOfMethod); + + worker.Emit(OpCodes.Ret); + + set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.In, fd.FieldType)); + set.SemanticsAttributes = MethodSemanticsAttributes.Setter; + + return set; + } + + public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, long dirtyBit, ref bool WeavingFailed) + { + string originalName = fd.Name; + + // GameObject/NetworkIdentity SyncVars have a new field for netId + FieldDefinition netIdField = null; + // NetworkBehaviour has different field type than other NetworkIdentityFields + if (fd.FieldType.IsDerivedFrom()) + { + netIdField = new FieldDefinition($"___{fd.Name}NetId", + FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed + weaverTypes.Import()); + netIdField.DeclaringType = td; + + syncVarNetIds[fd] = netIdField; + } + else if (fd.FieldType.IsNetworkIdentityField()) + { + netIdField = new FieldDefinition($"___{fd.Name}NetId", + FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed + weaverTypes.Import()); + netIdField.DeclaringType = td; + + syncVarNetIds[fd] = netIdField; + } + + MethodDefinition get = GenerateSyncVarGetter(fd, originalName, netIdField); + MethodDefinition set = GenerateSyncVarSetter(td, fd, originalName, dirtyBit, netIdField, ref WeavingFailed); + + //NOTE: is property even needed? Could just use a setter function? + //create the property + PropertyDefinition propertyDefinition = new PropertyDefinition($"Network{originalName}", PropertyAttributes.None, fd.FieldType) + { + GetMethod = get, + SetMethod = set + }; + + //add the methods and property to the type. + td.Methods.Add(get); + td.Methods.Add(set); + td.Properties.Add(propertyDefinition); + syncVarAccessLists.replacementSetterProperties[fd] = set; + + // replace getter field if GameObject/NetworkIdentity so it uses + // netId instead + // -> only for GameObjects, otherwise an int syncvar's getter would + // end up in recursion. + if (fd.FieldType.IsNetworkIdentityField()) + { + syncVarAccessLists.replacementGetterProperties[fd] = get; + } + } + + public (List syncVars, Dictionary syncVarNetIds) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed) + { + List syncVars = new List(); + Dictionary syncVarNetIds = new Dictionary(); + + // the mapping of dirtybits to sync-vars is implicit in the order of the fields here. this order is recorded in m_replacementProperties. + // start assigning syncvars at the place the base class stopped, if any + int dirtyBitCounter = syncVarAccessLists.GetSyncVarStart(td.BaseType.FullName); + + // find syncvars + foreach (FieldDefinition fd in td.Fields) + { + if (fd.HasCustomAttribute()) + { + if ((fd.Attributes & FieldAttributes.Static) != 0) + { + Log.Error($"{fd.Name} cannot be static", fd); + WeavingFailed = true; + continue; + } + + if (fd.FieldType.IsGenericParameter) + { + Log.Error($"{fd.Name} has generic type. Generic SyncVars are not supported", fd); + WeavingFailed = true; + continue; + } + + if (fd.FieldType.IsArray) + { + Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd); + WeavingFailed = true; + continue; + } + + if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) + { + Log.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd); + } + else + { + syncVars.Add(fd); + + ProcessSyncVar(td, fd, syncVarNetIds, 1L << dirtyBitCounter, ref WeavingFailed); + dirtyBitCounter += 1; + + if (dirtyBitCounter > SyncVarLimit) + { + Log.Error($"{td.Name} has > {SyncVarLimit} SyncVars. Consider refactoring your class into multiple components", td); + WeavingFailed = true; + continue; + } + } + } + } + + // add all the new SyncVar __netId fields + foreach (FieldDefinition fd in syncVarNetIds.Values) + { + td.Fields.Add(fd); + } + syncVarAccessLists.SetNumSyncVars(td.FullName, syncVars.Count); + + return (syncVars, syncVarNetIds); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta new file mode 100644 index 0000000..982f768 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f52c39bddd95d42b88f9cd554dfd9198 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs new file mode 100644 index 0000000..8afba94 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs @@ -0,0 +1,137 @@ +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + // Processes [TargetRpc] methods in NetworkBehaviour + public static class TargetRpcProcessor + { + // helper functions to check if the method has a NetworkConnection parameter + public static bool HasNetworkConnectionParameter(MethodDefinition md) + { + return md.Parameters.Count > 0 && + md.Parameters[0].ParameterType.Is(); + } + + public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) + { + string trgName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md); + + MethodDefinition rpc = new MethodDefinition(trgName, MethodAttributes.Family | + MethodAttributes.Static | + MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + ILProcessor worker = rpc.Body.GetILProcessor(); + Instruction label = worker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteClientActiveCheck(worker, weaverTypes, md.Name, label, "TargetRPC"); + + // setup for reader + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Castclass, td); + + // NetworkConnection parameter is optional + if (HasNetworkConnectionParameter(md)) + { + // on server, the NetworkConnection parameter is a connection to client. + // when the rpc is invoked on the client, it still has the same + // function signature. we pass in the connection to server, + // which is cleaner than just passing null) + //NetworkClient.readyconnection + // + // TODO + // a) .connectionToServer = best solution. no doubt. + // b) NetworkClient.connection for now. add TODO to not use static later. + worker.Emit(OpCodes.Call, weaverTypes.NetworkClientConnectionReference); + } + + // process reader parameters and skip first one if first one is NetworkConnection + if (!NetworkBehaviourProcessor.ReadArguments(md, readers, Log, worker, RemoteCallType.TargetRpc, ref WeavingFailed)) + return null; + + // invoke actual command function + worker.Emit(OpCodes.Callvirt, rpcCallFunc); + worker.Emit(OpCodes.Ret); + + NetworkBehaviourProcessor.AddInvokeParameters(weaverTypes, rpc.Parameters); + td.Methods.Add(rpc); + return rpc; + } + + /* generates code like: + public void TargetTest (NetworkConnection conn, int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32 ((uint)param); + base.SendTargetRPCInternal (conn, typeof(class), "TargetTest", val); + } + public void CallTargetTest (NetworkConnection conn, int param) + { + // whatever the user did before + } + + or if optional: + public void TargetTest (int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32 ((uint)param); + base.SendTargetRPCInternal (null, typeof(class), "TargetTest", val); + } + public void CallTargetTest (int param) + { + // whatever the user did before + } + + Originally HLAPI put the send message code inside the Call function + and then proceeded to replace every call to TargetTest with CallTargetTest + + This method moves all the user's code into the "CallTargetRpc" method + and replaces the body of the original method with the send message code. + This way we do not need to modify the code anywhere else, and this works + correctly in dependent assemblies + + */ + public static MethodDefinition ProcessTargetRpcCall(WeaverTypes weaverTypes, Writers writers, Logger Log, TypeDefinition td, MethodDefinition md, CustomAttribute targetRpcAttr, ref bool WeavingFailed) + { + MethodDefinition rpc = MethodProcessor.SubstituteMethod(Log, td, md, ref WeavingFailed); + + ILProcessor worker = md.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes); + + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); + + // write all the arguments that the user passed to the TargetRpc call + // (skip first one if first one is NetworkConnection) + if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.TargetRpc, ref WeavingFailed)) + return null; + + // invoke SendInternal and return + // this + worker.Emit(OpCodes.Ldarg_0); + if (HasNetworkConnectionParameter(md)) + { + // connection + worker.Emit(OpCodes.Ldarg_1); + } + else + { + // null + worker.Emit(OpCodes.Ldnull); + } + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + worker.Emit(OpCodes.Ldstr, md.FullName); + // writer + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Ldc_I4, targetRpcAttr.GetField("channel", 0)); + worker.Emit(OpCodes.Callvirt, weaverTypes.sendTargetRpcInternal); + + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); + + worker.Emit(OpCodes.Ret); + + return rpc; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta new file mode 100644 index 0000000..0ff7cc5 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb3ce6c6f3f2942ae88178b86f5a8282 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs b/Assets/Mirror/Editor/Weaver/Readers.cs new file mode 100644 index 0000000..fa888c9 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Readers.cs @@ -0,0 +1,383 @@ +using System; +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; +// to use Mono.CecilX.Rocks here, we need to 'override references' in the +// Unity.Mirror.CodeGen assembly definition file in the Editor, and add CecilX.Rocks. +// otherwise we get an unknown import exception. +using Mono.CecilX.Rocks; + +namespace Mirror.Weaver +{ + // not static, because ILPostProcessor is multithreaded + public class Readers + { + // Readers are only for this assembly. + // can't be used from another assembly, otherwise we will get: + // "System.ArgumentException: Member ... is declared in another module and needs to be imported" + AssemblyDefinition assembly; + WeaverTypes weaverTypes; + TypeDefinition GeneratedCodeClass; + Logger Log; + + Dictionary readFuncs = + new Dictionary(new TypeReferenceComparer()); + + public Readers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinition GeneratedCodeClass, Logger Log) + { + this.assembly = assembly; + this.weaverTypes = weaverTypes; + this.GeneratedCodeClass = GeneratedCodeClass; + this.Log = Log; + } + + internal void Register(TypeReference dataType, MethodReference methodReference) + { + if (readFuncs.ContainsKey(dataType)) + { + // TODO enable this again later. + // Reader has some obsolete functions that were renamed. + // Don't want weaver warnings for all of them. + //Log.Warning($"Registering a Read method for {dataType.FullName} when one already exists", methodReference); + } + + // we need to import type when we Initialize Readers so import here in case it is used anywhere else + TypeReference imported = assembly.MainModule.ImportReference(dataType); + readFuncs[imported] = methodReference; + } + + void RegisterReadFunc(TypeReference typeReference, MethodDefinition newReaderFunc) + { + Register(typeReference, newReaderFunc); + GeneratedCodeClass.Methods.Add(newReaderFunc); + } + + // Finds existing reader for type, if non exists trys to create one + public MethodReference GetReadFunc(TypeReference variable, ref bool WeavingFailed) + { + if (readFuncs.TryGetValue(variable, out MethodReference foundFunc)) + return foundFunc; + + TypeReference importedVariable = assembly.MainModule.ImportReference(variable); + return GenerateReader(importedVariable, ref WeavingFailed); + } + + MethodReference GenerateReader(TypeReference variableReference, ref bool WeavingFailed) + { + // Arrays are special, if we resolve them, we get the element type, + // so the following ifs might choke on it for scriptable objects + // or other objects that require a custom serializer + // thus check if it is an array and skip all the checks. + if (variableReference.IsArray) + { + if (variableReference.IsMultidimensionalArray()) + { + Log.Error($"{variableReference.Name} is an unsupported type. Multidimensional arrays are not supported", variableReference); + WeavingFailed = true; + return null; + } + + return GenerateReadCollection(variableReference, variableReference.GetElementType(), nameof(NetworkReaderExtensions.ReadArray), ref WeavingFailed); + } + + TypeDefinition variableDefinition = variableReference.Resolve(); + + // check if the type is completely invalid + if (variableDefinition == null) + { + Log.Error($"{variableReference.Name} is not a supported type", variableReference); + WeavingFailed = true; + return null; + } + else if (variableReference.IsByReference) + { + // error?? + Log.Error($"Cannot pass type {variableReference.Name} by reference", variableReference); + WeavingFailed = true; + return null; + } + + // use existing func for known types + if (variableDefinition.IsEnum) + { + return GenerateEnumReadFunc(variableReference, ref WeavingFailed); + } + else if (variableDefinition.Is(typeof(ArraySegment<>))) + { + return GenerateArraySegmentReadFunc(variableReference, ref WeavingFailed); + } + else if (variableDefinition.Is(typeof(List<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadList), ref WeavingFailed); + } + else if (variableReference.IsDerivedFrom()) + { + return GetNetworkBehaviourReader(variableReference); + } + + // check if reader generation is applicable on this type + if (variableDefinition.IsDerivedFrom()) + { + Log.Error($"Cannot generate reader for component type {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + if (variableReference.Is()) + { + Log.Error($"Cannot generate reader for {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + if (variableReference.Is()) + { + Log.Error($"Cannot generate reader for {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + if (variableDefinition.HasGenericParameters) + { + Log.Error($"Cannot generate reader for generic variable {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + if (variableDefinition.IsInterface) + { + Log.Error($"Cannot generate reader for interface {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + if (variableDefinition.IsAbstract) + { + Log.Error($"Cannot generate reader for abstract class {variableReference.Name}. Use a supported type or provide a custom reader", variableReference); + WeavingFailed = true; + return null; + } + + return GenerateClassOrStructReadFunction(variableReference, ref WeavingFailed); + } + + MethodReference GetNetworkBehaviourReader(TypeReference variableReference) + { + // uses generic ReadNetworkBehaviour rather than having weaver create one for each NB + MethodReference generic = weaverTypes.readNetworkBehaviourGeneric; + + MethodReference readFunc = generic.MakeGeneric(assembly.MainModule, variableReference); + + // register function so it is added to Reader + // use Register instead of RegisterWriteFunc because this is not a generated function + Register(variableReference, readFunc); + + return readFunc; + } + + MethodDefinition GenerateEnumReadFunc(TypeReference variable, ref bool WeavingFailed) + { + MethodDefinition readerFunc = GenerateReaderFunction(variable); + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + worker.Emit(OpCodes.Ldarg_0); + + TypeReference underlyingType = variable.Resolve().GetEnumUnderlyingType(); + MethodReference underlyingFunc = GetReadFunc(underlyingType, ref WeavingFailed); + + worker.Emit(OpCodes.Call, underlyingFunc); + worker.Emit(OpCodes.Ret); + return readerFunc; + } + + MethodDefinition GenerateArraySegmentReadFunc(TypeReference variable, ref bool WeavingFailed) + { + GenericInstanceType genericInstance = (GenericInstanceType)variable; + TypeReference elementType = genericInstance.GenericArguments[0]; + + MethodDefinition readerFunc = GenerateReaderFunction(variable); + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + // $array = reader.Read<[T]>() + ArrayType arrayType = elementType.MakeArrayType(); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Call, GetReadFunc(arrayType, ref WeavingFailed)); + + // return new ArraySegment($array); + worker.Emit(OpCodes.Newobj, weaverTypes.ArraySegmentConstructorReference.MakeHostInstanceGeneric(assembly.MainModule, genericInstance)); + worker.Emit(OpCodes.Ret); + return readerFunc; + } + + MethodDefinition GenerateReaderFunction(TypeReference variable) + { + string functionName = $"_Read_{variable.FullName}"; + + // create new reader for this type + MethodDefinition readerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + variable); + + readerFunc.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, weaverTypes.Import())); + readerFunc.Body.InitLocals = true; + RegisterReadFunc(variable, readerFunc); + + return readerFunc; + } + + MethodDefinition GenerateReadCollection(TypeReference variable, TypeReference elementType, string readerFunction, ref bool WeavingFailed) + { + MethodDefinition readerFunc = GenerateReaderFunction(variable); + // generate readers for the element + GetReadFunc(elementType, ref WeavingFailed); + + ModuleDefinition module = assembly.MainModule; + TypeReference readerExtensions = module.ImportReference(typeof(NetworkReaderExtensions)); + MethodReference listReader = Resolvers.ResolveMethod(readerExtensions, assembly, Log, readerFunction, ref WeavingFailed); + + GenericInstanceMethod methodRef = new GenericInstanceMethod(listReader); + methodRef.GenericArguments.Add(elementType); + + // generates + // return reader.ReadList(); + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + worker.Emit(OpCodes.Ldarg_0); // reader + worker.Emit(OpCodes.Call, methodRef); // Read + + worker.Emit(OpCodes.Ret); + + return readerFunc; + } + + MethodDefinition GenerateClassOrStructReadFunction(TypeReference variable, ref bool WeavingFailed) + { + MethodDefinition readerFunc = GenerateReaderFunction(variable); + + // create local for return value + readerFunc.Body.Variables.Add(new VariableDefinition(variable)); + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + TypeDefinition td = variable.Resolve(); + + if (!td.IsValueType) + GenerateNullCheck(worker, ref WeavingFailed); + + CreateNew(variable, worker, td, ref WeavingFailed); + ReadAllFields(variable, worker, ref WeavingFailed); + + worker.Emit(OpCodes.Ldloc_0); + worker.Emit(OpCodes.Ret); + return readerFunc; + } + + void GenerateNullCheck(ILProcessor worker, ref bool WeavingFailed) + { + // if (!reader.ReadBoolean()) { + // return null; + // } + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Call, GetReadFunc(weaverTypes.Import(), ref WeavingFailed)); + + Instruction labelEmptyArray = worker.Create(OpCodes.Nop); + worker.Emit(OpCodes.Brtrue, labelEmptyArray); + // return null + worker.Emit(OpCodes.Ldnull); + worker.Emit(OpCodes.Ret); + worker.Append(labelEmptyArray); + } + + // Initialize the local variable with a new instance + void CreateNew(TypeReference variable, ILProcessor worker, TypeDefinition td, ref bool WeavingFailed) + { + if (variable.IsValueType) + { + // structs are created with Initobj + worker.Emit(OpCodes.Ldloca, 0); + worker.Emit(OpCodes.Initobj, variable); + } + else if (td.IsDerivedFrom()) + { + GenericInstanceMethod genericInstanceMethod = new GenericInstanceMethod(weaverTypes.ScriptableObjectCreateInstanceMethod); + genericInstanceMethod.GenericArguments.Add(variable); + worker.Emit(OpCodes.Call, genericInstanceMethod); + worker.Emit(OpCodes.Stloc_0); + } + else + { + // classes are created with their constructor + MethodDefinition ctor = Resolvers.ResolveDefaultPublicCtor(variable); + if (ctor == null) + { + Log.Error($"{variable.Name} can't be deserialized because it has no default constructor. Don't use {variable.Name} in [SyncVar]s, Rpcs, Cmds, etc.", variable); + WeavingFailed = true; + return; + } + + MethodReference ctorRef = assembly.MainModule.ImportReference(ctor); + + worker.Emit(OpCodes.Newobj, ctorRef); + worker.Emit(OpCodes.Stloc_0); + } + } + + void ReadAllFields(TypeReference variable, ILProcessor worker, ref bool WeavingFailed) + { + foreach (FieldDefinition field in variable.FindAllPublicFields()) + { + // mismatched ldloca/ldloc for struct/class combinations is invalid IL, which causes crash at runtime + OpCode opcode = variable.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc; + worker.Emit(opcode, 0); + MethodReference readFunc = GetReadFunc(field.FieldType, ref WeavingFailed); + if (readFunc != null) + { + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Call, readFunc); + } + else + { + Log.Error($"{field.Name} has an unsupported type", field); + WeavingFailed = true; + } + FieldReference fieldRef = assembly.MainModule.ImportReference(field); + + worker.Emit(OpCodes.Stfld, fieldRef); + } + } + + // Save a delegate for each one of the readers into Reader.read + internal void InitializeReaders(ILProcessor worker) + { + ModuleDefinition module = assembly.MainModule; + + TypeReference genericReaderClassRef = module.ImportReference(typeof(Reader<>)); + + System.Reflection.FieldInfo fieldInfo = typeof(Reader<>).GetField(nameof(Reader.read)); + FieldReference fieldRef = module.ImportReference(fieldInfo); + TypeReference networkReaderRef = module.ImportReference(typeof(NetworkReader)); + TypeReference funcRef = module.ImportReference(typeof(Func<,>)); + MethodReference funcConstructorRef = module.ImportReference(typeof(Func<,>).GetConstructors()[0]); + + foreach (KeyValuePair kvp in readFuncs) + { + TypeReference targetType = kvp.Key; + MethodReference readFunc = kvp.Value; + + // create a Func delegate + worker.Emit(OpCodes.Ldnull); + worker.Emit(OpCodes.Ldftn, readFunc); + GenericInstanceType funcGenericInstance = funcRef.MakeGenericInstanceType(networkReaderRef, targetType); + MethodReference funcConstructorInstance = funcConstructorRef.MakeHostInstanceGeneric(assembly.MainModule, funcGenericInstance); + worker.Emit(OpCodes.Newobj, funcConstructorInstance); + + // save it in Reader.read + GenericInstanceType genericInstance = genericReaderClassRef.MakeGenericInstanceType(targetType); + FieldReference specializedField = fieldRef.SpecializeField(assembly.MainModule, genericInstance); + worker.Emit(OpCodes.Stsfld, specializedField); + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs.meta b/Assets/Mirror/Editor/Weaver/Readers.cs.meta new file mode 100644 index 0000000..838ff59 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Readers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be40277098a024539bf63d0205cae824 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs b/Assets/Mirror/Editor/Weaver/Resolvers.cs new file mode 100644 index 0000000..a9d551b --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs @@ -0,0 +1,94 @@ +// all the resolve functions for the weaver +// NOTE: these functions should be made extensions, but right now they still +// make heavy use of Weaver.fail and we'd have to check each one's return +// value for null otherwise. +// (original FieldType.Resolve returns null if not found too, so +// exceptions would be a bit inconsistent here) +using Mono.CecilX; + +namespace Mirror.Weaver +{ + public static class Resolvers + { + public static MethodReference ResolveMethod(TypeReference tr, AssemblyDefinition assembly, Logger Log, string name, ref bool WeavingFailed) + { + if (tr == null) + { + Log.Error($"Cannot resolve method {name} without a class"); + WeavingFailed = true; + return null; + } + MethodReference method = ResolveMethod(tr, assembly, Log, m => m.Name == name, ref WeavingFailed); + if (method == null) + { + Log.Error($"Method not found with name {name} in type {tr.Name}", tr); + WeavingFailed = true; + } + return method; + } + + public static MethodReference ResolveMethod(TypeReference t, AssemblyDefinition assembly, Logger Log, System.Func predicate, ref bool WeavingFailed) + { + foreach (MethodDefinition methodRef in t.Resolve().Methods) + { + if (predicate(methodRef)) + { + return assembly.MainModule.ImportReference(methodRef); + } + } + + Log.Error($"Method not found in type {t.Name}", t); + WeavingFailed = true; + return null; + } + + public static MethodReference TryResolveMethodInParents(TypeReference tr, AssemblyDefinition assembly, string name) + { + if (tr == null) + { + return null; + } + foreach (MethodDefinition methodDef in tr.Resolve().Methods) + { + if (methodDef.Name == name) + { + MethodReference methodRef = methodDef; + if (tr.IsGenericInstance) + { + methodRef = methodRef.MakeHostInstanceGeneric(tr.Module, (GenericInstanceType)tr); + } + return assembly.MainModule.ImportReference(methodRef); + } + } + + // Could not find the method in this class, try the parent + return TryResolveMethodInParents(tr.Resolve().BaseType.ApplyGenericParameters(tr), assembly, name); + } + + public static MethodDefinition ResolveDefaultPublicCtor(TypeReference variable) + { + foreach (MethodDefinition methodRef in variable.Resolve().Methods) + { + if (methodRef.Name == ".ctor" && + methodRef.Resolve().IsPublic && + methodRef.Parameters.Count == 0) + { + return methodRef; + } + } + return null; + } + + public static MethodReference ResolveProperty(TypeReference tr, AssemblyDefinition assembly, string name) + { + foreach (PropertyDefinition pd in tr.Resolve().Properties) + { + if (pd.Name == name) + { + return assembly.MainModule.ImportReference(pd.GetMethod); + } + } + return null; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta new file mode 100644 index 0000000..f4f6602 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3039a59c76aec43c797ad66930430367 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs new file mode 100644 index 0000000..fa7a682 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs @@ -0,0 +1,32 @@ +// tracks SyncVar read/write access when processing NetworkBehaviour, +// to later be replaced by SyncVarAccessReplacer. +using System.Collections.Generic; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + // This data is flushed each time - if we are run multiple times in the same process/domain + public class SyncVarAccessLists + { + // setter functions that replace [SyncVar] member variable references. dict + public Dictionary replacementSetterProperties = + new Dictionary(); + + // getter functions that replace [SyncVar] member variable references. dict + public Dictionary replacementGetterProperties = + new Dictionary(); + + // amount of SyncVars per class. dict + // necessary for SyncVar dirty bits, where inheriting classes start + // their dirty bits at base class SyncVar amount. + public Dictionary numSyncVars = new Dictionary(); + + public int GetSyncVarStart(string className) => + numSyncVars.TryGetValue(className, out int value) ? value : 0; + + public void SetNumSyncVars(string className, int num) + { + numSyncVars[className] = num; + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta new file mode 100644 index 0000000..9a4da44 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6905230c3c4c4e158760065a93380e83 +timeCreated: 1629348618 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs new file mode 100644 index 0000000..e3c31d5 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + // Compares TypeReference using FullName + public class TypeReferenceComparer : IEqualityComparer + { + public bool Equals(TypeReference x, TypeReference y) => + x.FullName == y.FullName; + + public int GetHashCode(TypeReference obj) => + obj.FullName.GetHashCode(); + } +} diff --git a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta new file mode 100644 index 0000000..890b4dc --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55eb9eb8794946f4da7ad39788c9920b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef new file mode 100644 index 0000000..4566bb2 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef @@ -0,0 +1,21 @@ +{ + "name": "Unity.Mirror.CodeGen", + "rootNamespace": "", + "references": [ + "Mirror" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "Mono.CecilX.dll", + "Mono.CecilX.Rocks.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta new file mode 100644 index 0000000..b65a0cd --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1d0b9d21c3ff546a4aa32399dfd33474 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs b/Assets/Mirror/Editor/Weaver/Weaver.cs new file mode 100644 index 0000000..2644e68 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + // not static, because ILPostProcessor is multithreaded + internal class Weaver + { + public const string InvokeRpcPrefix = "InvokeUserCode_"; + + // generated code class + public const string GeneratedCodeNamespace = "Mirror"; + public const string GeneratedCodeClassName = "GeneratedNetworkCode"; + TypeDefinition GeneratedCodeClass; + + // for resolving Mirror.dll in ReaderWriterProcessor, we need to know + // Mirror.dll name + public const string MirrorAssemblyName = "Mirror"; + + WeaverTypes weaverTypes; + SyncVarAccessLists syncVarAccessLists; + AssemblyDefinition CurrentAssembly; + Writers writers; + Readers readers; + + // in case of weaver errors, we don't stop immediately. + // we log all errors and then eventually return false if + // weaving has failed. + // this way the user can fix multiple errors at once, instead of having + // to fix -> recompile -> fix -> recompile for one error at a time. + bool WeavingFailed; + + // logger functions can be set from the outside. + // for example, Debug.Log or ILPostProcessor Diagnostics log for + // multi threaded logging. + public Logger Log; + + // remote actions now support overloads, + // -> but IL2CPP doesnt like it when two generated methods + // -> have the same signature, + // -> so, append the signature to the generated method name, + // -> to create a unique name + // Example: + // RpcTeleport(Vector3 position) -> InvokeUserCode_RpcTeleport__Vector3() + // RpcTeleport(Vector3 position, Quaternion rotation) -> InvokeUserCode_RpcTeleport__Vector3Quaternion() + // fixes https://github.com/vis2k/Mirror/issues/3060 + public static string GenerateMethodName(string initialPrefix, MethodDefinition md) + { + initialPrefix += md.Name; + + for (int i = 0; i < md.Parameters.Count; ++i) + { + // with __ so it's more obvious that this is the parameter suffix. + // otherwise RpcTest(int) => RpcTestInt(int) which is not obvious. + initialPrefix += $"__{md.Parameters[i].ParameterType.Name}"; + } + + return initialPrefix; + } + + public Weaver(Logger Log) + { + this.Log = Log; + } + + // returns 'true' if modified (=if we did anything) + bool WeaveNetworkBehavior(TypeDefinition td) + { + if (!td.IsClass) + return false; + + if (!td.IsDerivedFrom()) + { + if (td.IsDerivedFrom()) + MonoBehaviourProcessor.Process(Log, td, ref WeavingFailed); + return false; + } + + // process this and base classes from parent to child order + + List behaviourClasses = new List(); + + TypeDefinition parent = td; + while (parent != null) + { + if (parent.Is()) + { + break; + } + + try + { + behaviourClasses.Insert(0, parent); + parent = parent.BaseType.Resolve(); + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + bool modified = false; + foreach (TypeDefinition behaviour in behaviourClasses) + { + modified |= new NetworkBehaviourProcessor(CurrentAssembly, weaverTypes, syncVarAccessLists, writers, readers, Log, behaviour).Process(ref WeavingFailed); + } + return modified; + } + + bool WeaveModule(ModuleDefinition moduleDefinition) + { + bool modified = false; + + Stopwatch watch = Stopwatch.StartNew(); + watch.Start(); + + foreach (TypeDefinition td in moduleDefinition.Types) + { + if (td.IsClass && td.BaseType.CanBeResolved()) + { + modified |= WeaveNetworkBehavior(td); + modified |= ServerClientAttributeProcessor.Process(weaverTypes, Log, td, ref WeavingFailed); + } + } + + watch.Stop(); + Console.WriteLine($"Weave behaviours and messages took {watch.ElapsedMilliseconds} milliseconds"); + + return modified; + } + + void CreateGeneratedCodeClass() + { + // create "Mirror.GeneratedNetworkCode" class which holds all + // Readers and Writers + GeneratedCodeClass = new TypeDefinition(GeneratedCodeNamespace, GeneratedCodeClassName, + TypeAttributes.BeforeFieldInit | TypeAttributes.Class | TypeAttributes.AnsiClass | TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.Abstract | TypeAttributes.Sealed, + weaverTypes.Import()); + } + + // Weave takes an AssemblyDefinition to be compatible with both old and + // new weavers: + // * old takes a filepath, new takes a in-memory byte[] + // * old uses DefaultAssemblyResolver with added dependencies paths, + // new uses ...? + // + // => assembly: the one we are currently weaving (MyGame.dll) + // => resolver: useful in case we need to resolve any of the assembly's + // assembly.MainModule.AssemblyReferences. + // -> we can resolve ANY of them given that the resolver + // works properly (need custom one for ILPostProcessor) + // -> IMPORTANT: .Resolve() takes an AssemblyNameReference. + // those from assembly.MainModule.AssemblyReferences are + // guaranteed to be resolve-able. + // Parsing from a string for Library/.../Mirror.dll + // would not be guaranteed to be resolve-able because + // for ILPostProcessor we can't assume where Mirror.dll + // is etc. + public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out bool modified) + { + WeavingFailed = false; + modified = false; + try + { + CurrentAssembly = assembly; + + // fix "No writer found for ..." error + // https://github.com/vis2k/Mirror/issues/2579 + // -> when restarting Unity, weaver would try to weave a DLL + // again + // -> resulting in two GeneratedNetworkCode classes (see ILSpy) + // -> the second one wouldn't have all the writer types setup + if (CurrentAssembly.MainModule.ContainsClass(GeneratedCodeNamespace, GeneratedCodeClassName)) + { + //Log.Warning($"Weaver: skipping {CurrentAssembly.Name} because already weaved"); + return true; + } + + weaverTypes = new WeaverTypes(CurrentAssembly, Log, ref WeavingFailed); + + // weaverTypes are needed for CreateGeneratedCodeClass + CreateGeneratedCodeClass(); + + // WeaverList depends on WeaverTypes setup because it uses Import + syncVarAccessLists = new SyncVarAccessLists(); + + // initialize readers & writers with this assembly. + // we need to do this in every Process() call. + // otherwise we would get + // "System.ArgumentException: Member ... is declared in another module and needs to be imported" + // errors when still using the previous module's reader/writer funcs. + writers = new Writers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log); + readers = new Readers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log); + + Stopwatch rwstopwatch = Stopwatch.StartNew(); + // Need to track modified from ReaderWriterProcessor too because it could find custom read/write functions or create functions for NetworkMessages + modified = ReaderWriterProcessor.Process(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed); + rwstopwatch.Stop(); + Console.WriteLine($"Find all reader and writers took {rwstopwatch.ElapsedMilliseconds} milliseconds"); + + ModuleDefinition moduleDefinition = CurrentAssembly.MainModule; + Console.WriteLine($"Script Module: {moduleDefinition.Name}"); + + modified |= WeaveModule(moduleDefinition); + + if (WeavingFailed) + { + return false; + } + + if (modified) + { + SyncVarAttributeAccessReplacer.Process(moduleDefinition, syncVarAccessLists); + + // add class that holds read/write functions + moduleDefinition.Types.Add(GeneratedCodeClass); + + ReaderWriterProcessor.InitializeReaderAndWriters(CurrentAssembly, weaverTypes, writers, readers, GeneratedCodeClass); + + // DO NOT WRITE here. + // CompilationFinishedHook writes to the file. + // ILPostProcessor writes to in-memory assembly. + // it depends on the caller. + //CurrentAssembly.Write(new WriterParameters{ WriteSymbols = true }); + } + + return true; + } + catch (Exception e) + { + Log.Error($"Exception :{e}"); + WeavingFailed = true; + return false; + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs.meta b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta new file mode 100644 index 0000000..0ea2dfe --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de160f52931054064852f2afd7e7a86f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs new file mode 100644 index 0000000..efedd27 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.Serialization; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + [Serializable] + public abstract class WeaverException : Exception + { + public MemberReference MemberReference { get; } + + protected WeaverException(string message, MemberReference member) : base(message) + { + MemberReference = member; + } + + protected WeaverException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) {} + } + + [Serializable] + public class GenerateWriterException : WeaverException + { + public GenerateWriterException(string message, MemberReference member) : base(message, member) {} + protected GenerateWriterException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) {} + } +} diff --git a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta new file mode 100644 index 0000000..68643b2 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8aaaf6193bad7424492677f8e81f1b30 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs new file mode 100644 index 0000000..173af58 --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -0,0 +1,164 @@ +using System; +using Mono.CecilX; +using UnityEditor; +using UnityEngine; + +namespace Mirror.Weaver +{ + // not static, because ILPostProcessor is multithreaded + public class WeaverTypes + { + public MethodReference ScriptableObjectCreateInstanceMethod; + + public MethodReference NetworkBehaviourDirtyBitsReference; + public MethodReference GetWriterReference; + public MethodReference ReturnWriterReference; + + public MethodReference NetworkClientConnectionReference; + + public MethodReference RemoteCallDelegateConstructor; + + public MethodReference NetworkServerGetActive; + public MethodReference NetworkClientGetActive; + + // custom attribute types + public MethodReference InitSyncObjectReference; + + // array segment + public MethodReference ArraySegmentConstructorReference; + + // Action for SyncVar Hooks + public MethodReference ActionT_T; + + // syncvar + public MethodReference generatedSyncVarSetter; + public MethodReference generatedSyncVarSetter_GameObject; + public MethodReference generatedSyncVarSetter_NetworkIdentity; + public MethodReference generatedSyncVarSetter_NetworkBehaviour_T; + public MethodReference generatedSyncVarDeserialize; + public MethodReference generatedSyncVarDeserialize_GameObject; + public MethodReference generatedSyncVarDeserialize_NetworkIdentity; + public MethodReference generatedSyncVarDeserialize_NetworkBehaviour_T; + public MethodReference getSyncVarGameObjectReference; + public MethodReference getSyncVarNetworkIdentityReference; + public MethodReference getSyncVarNetworkBehaviourReference; + public MethodReference registerCommandReference; + public MethodReference registerRpcReference; + public MethodReference getTypeFromHandleReference; + public MethodReference logErrorReference; + public MethodReference logWarningReference; + public MethodReference sendCommandInternal; + public MethodReference sendRpcInternal; + public MethodReference sendTargetRpcInternal; + + public MethodReference readNetworkBehaviourGeneric; + + // attributes + public TypeDefinition initializeOnLoadMethodAttribute; + public TypeDefinition runtimeInitializeOnLoadMethodAttribute; + + AssemblyDefinition assembly; + + public TypeReference Import() => Import(typeof(T)); + + public TypeReference Import(Type t) => assembly.MainModule.ImportReference(t); + + // constructor resolves the types and stores them in fields + public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFailed) + { + // system types + this.assembly = assembly; + + TypeReference ArraySegmentType = Import(typeof(ArraySegment<>)); + ArraySegmentConstructorReference = Resolvers.ResolveMethod(ArraySegmentType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference ActionType = Import(typeof(Action<,>)); + ActionT_T = Resolvers.ResolveMethod(ActionType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference NetworkServerType = Import(typeof(NetworkServer)); + NetworkServerGetActive = Resolvers.ResolveMethod(NetworkServerType, assembly, Log, "get_active", ref WeavingFailed); + TypeReference NetworkClientType = Import(typeof(NetworkClient)); + NetworkClientGetActive = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_active", ref WeavingFailed); + + TypeReference RemoteCallDelegateType = Import(); + RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference NetworkBehaviourType = Import(); + TypeReference RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); + + TypeReference ScriptableObjectType = Import(); + + ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( + ScriptableObjectType, assembly, Log, + md => md.Name == "CreateInstance" && md.HasGenericParameters, + ref WeavingFailed); + + NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, assembly, "syncVarDirtyBits"); + TypeReference NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); + GetWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Get", ref WeavingFailed); + ReturnWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Return", ref WeavingFailed); + + NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", ref WeavingFailed); + + generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed); + generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed); + generatedSyncVarSetter_NetworkIdentity = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_NetworkIdentity", ref WeavingFailed); + generatedSyncVarSetter_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_NetworkBehaviour", ref WeavingFailed); + + generatedSyncVarDeserialize_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_GameObject", ref WeavingFailed); + generatedSyncVarDeserialize = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize", ref WeavingFailed); + generatedSyncVarDeserialize_NetworkIdentity = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkIdentity", ref WeavingFailed); + generatedSyncVarDeserialize_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkBehaviour", ref WeavingFailed); + + getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarGameObject", ref WeavingFailed); + getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); + getSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkBehaviour", ref WeavingFailed); + + registerCommandReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterCommand", ref WeavingFailed); + registerRpcReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterRpc", ref WeavingFailed); + + TypeReference unityDebug = Import(typeof(UnityEngine.Debug)); + // these have multiple methods with same name, so need to check parameters too + logErrorReference = Resolvers.ResolveMethod(unityDebug, assembly, Log, md => + md.Name == "LogError" && + md.Parameters.Count == 1 && + md.Parameters[0].ParameterType.FullName == typeof(object).FullName, + ref WeavingFailed); + + logWarningReference = Resolvers.ResolveMethod(unityDebug, assembly, Log, md => + md.Name == "LogWarning" && + md.Parameters.Count == 1 && + md.Parameters[0].ParameterType.FullName == typeof(object).FullName, + ref WeavingFailed); + + TypeReference typeType = Import(typeof(Type)); + getTypeFromHandleReference = Resolvers.ResolveMethod(typeType, assembly, Log, "GetTypeFromHandle", ref WeavingFailed); + sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed); + sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendRPCInternal", ref WeavingFailed); + sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendTargetRPCInternal", ref WeavingFailed); + + InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "InitSyncObject", ref WeavingFailed); + + TypeReference readerExtensions = Import(typeof(NetworkReaderExtensions)); + readNetworkBehaviourGeneric = Resolvers.ResolveMethod(readerExtensions, assembly, Log, (md => + { + return md.Name == nameof(NetworkReaderExtensions.ReadNetworkBehaviour) && + md.HasGenericParameters; + }), + ref WeavingFailed); + + // [InitializeOnLoadMethod] + // 'UnityEditor' is not available in builds. + // we can only import this attribute if we are in an Editor assembly. + if (Helpers.IsEditorAssembly(assembly)) + { + TypeReference initializeOnLoadMethodAttributeRef = Import(typeof(InitializeOnLoadMethodAttribute)); + initializeOnLoadMethodAttribute = initializeOnLoadMethodAttributeRef.Resolve(); + } + + // [RuntimeInitializeOnLoadMethod] + TypeReference runtimeInitializeOnLoadMethodAttributeRef = Import(typeof(RuntimeInitializeOnLoadMethodAttribute)); + runtimeInitializeOnLoadMethodAttribute = runtimeInitializeOnLoadMethodAttributeRef.Resolve(); + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta new file mode 100644 index 0000000..d71c33f --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2585961bf7fe4c10a9143f4087efdf6f +timeCreated: 1596486854 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs b/Assets/Mirror/Editor/Weaver/Writers.cs new file mode 100644 index 0000000..51d514e --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Writers.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; +// to use Mono.CecilX.Rocks here, we need to 'override references' in the +// Unity.Mirror.CodeGen assembly definition file in the Editor, and add CecilX.Rocks. +// otherwise we get an unknown import exception. +using Mono.CecilX.Rocks; + +namespace Mirror.Weaver +{ + // not static, because ILPostProcessor is multithreaded + public class Writers + { + // Writers are only for this assembly. + // can't be used from another assembly, otherwise we will get: + // "System.ArgumentException: Member ... is declared in another module and needs to be imported" + AssemblyDefinition assembly; + WeaverTypes weaverTypes; + TypeDefinition GeneratedCodeClass; + Logger Log; + + Dictionary writeFuncs = + new Dictionary(new TypeReferenceComparer()); + + public Writers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinition GeneratedCodeClass, Logger Log) + { + this.assembly = assembly; + this.weaverTypes = weaverTypes; + this.GeneratedCodeClass = GeneratedCodeClass; + this.Log = Log; + } + + public void Register(TypeReference dataType, MethodReference methodReference) + { + if (writeFuncs.ContainsKey(dataType)) + { + // TODO enable this again later. + // Writer has some obsolete functions that were renamed. + // Don't want weaver warnings for all of them. + //Log.Warning($"Registering a Write method for {dataType.FullName} when one already exists", methodReference); + } + + // we need to import type when we Initialize Writers so import here in case it is used anywhere else + TypeReference imported = assembly.MainModule.ImportReference(dataType); + writeFuncs[imported] = methodReference; + } + + void RegisterWriteFunc(TypeReference typeReference, MethodDefinition newWriterFunc) + { + Register(typeReference, newWriterFunc); + GeneratedCodeClass.Methods.Add(newWriterFunc); + } + + // Finds existing writer for type, if non exists trys to create one + public MethodReference GetWriteFunc(TypeReference variable, ref bool WeavingFailed) + { + if (writeFuncs.TryGetValue(variable, out MethodReference foundFunc)) + return foundFunc; + + // this try/catch will be removed in future PR and make `GetWriteFunc` throw instead + try + { + TypeReference importedVariable = assembly.MainModule.ImportReference(variable); + return GenerateWriter(importedVariable, ref WeavingFailed); + } + catch (GenerateWriterException e) + { + Log.Error(e.Message, e.MemberReference); + WeavingFailed = true; + return null; + } + } + + //Throws GenerateWriterException when writer could not be generated for type + MethodReference GenerateWriter(TypeReference variableReference, ref bool WeavingFailed) + { + if (variableReference.IsByReference) + { + throw new GenerateWriterException($"Cannot pass {variableReference.Name} by reference", variableReference); + } + + // Arrays are special, if we resolve them, we get the element type, + // e.g. int[] resolves to int + // therefore process this before checks below + if (variableReference.IsArray) + { + if (variableReference.IsMultidimensionalArray()) + { + throw new GenerateWriterException($"{variableReference.Name} is an unsupported type. Multidimensional arrays are not supported", variableReference); + } + TypeReference elementType = variableReference.GetElementType(); + return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteArray), ref WeavingFailed); + } + + if (variableReference.Resolve()?.IsEnum ?? false) + { + // serialize enum as their base type + return GenerateEnumWriteFunc(variableReference, ref WeavingFailed); + } + + // check for collections + if (variableReference.Is(typeof(ArraySegment<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteArraySegment), ref WeavingFailed); + } + if (variableReference.Is(typeof(List<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteList), ref WeavingFailed); + } + + if (variableReference.IsDerivedFrom()) + { + return GetNetworkBehaviourWriter(variableReference); + } + + // check for invalid types + TypeDefinition variableDefinition = variableReference.Resolve(); + if (variableDefinition == null) + { + throw new GenerateWriterException($"{variableReference.Name} is not a supported type. Use a supported type or provide a custom writer", variableReference); + } + if (variableDefinition.IsDerivedFrom()) + { + throw new GenerateWriterException($"Cannot generate writer for component type {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + if (variableReference.Is()) + { + throw new GenerateWriterException($"Cannot generate writer for {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + if (variableReference.Is()) + { + throw new GenerateWriterException($"Cannot generate writer for {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + if (variableDefinition.HasGenericParameters) + { + throw new GenerateWriterException($"Cannot generate writer for generic type {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + if (variableDefinition.IsInterface) + { + throw new GenerateWriterException($"Cannot generate writer for interface {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + if (variableDefinition.IsAbstract) + { + throw new GenerateWriterException($"Cannot generate writer for abstract class {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); + } + + // generate writer for class/struct + return GenerateClassOrStructWriterFunction(variableReference, ref WeavingFailed); + } + + MethodReference GetNetworkBehaviourWriter(TypeReference variableReference) + { + // all NetworkBehaviours can use the same write function + if (writeFuncs.TryGetValue(weaverTypes.Import(), out MethodReference func)) + { + // register function so it is added to writer + // use Register instead of RegisterWriteFunc because this is not a generated function + Register(variableReference, func); + + return func; + } + else + { + // this exception only happens if mirror is missing the WriteNetworkBehaviour method + throw new MissingMethodException($"Could not find writer for NetworkBehaviour"); + } + } + + MethodDefinition GenerateEnumWriteFunc(TypeReference variable, ref bool WeavingFailed) + { + MethodDefinition writerFunc = GenerateWriterFunc(variable); + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + + MethodReference underlyingWriter = GetWriteFunc(variable.Resolve().GetEnumUnderlyingType(), ref WeavingFailed); + + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_1); + worker.Emit(OpCodes.Call, underlyingWriter); + + worker.Emit(OpCodes.Ret); + return writerFunc; + } + + MethodDefinition GenerateWriterFunc(TypeReference variable) + { + string functionName = $"_Write_{variable.FullName}"; + // create new writer for this type + MethodDefinition writerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + weaverTypes.Import(typeof(void))); + + writerFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); + writerFunc.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, variable)); + writerFunc.Body.InitLocals = true; + + RegisterWriteFunc(variable, writerFunc); + return writerFunc; + } + + MethodDefinition GenerateClassOrStructWriterFunction(TypeReference variable, ref bool WeavingFailed) + { + MethodDefinition writerFunc = GenerateWriterFunc(variable); + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + + if (!variable.Resolve().IsValueType) + WriteNullCheck(worker, ref WeavingFailed); + + if (!WriteAllFields(variable, worker, ref WeavingFailed)) + return null; + + worker.Emit(OpCodes.Ret); + return writerFunc; + } + + void WriteNullCheck(ILProcessor worker, ref bool WeavingFailed) + { + // if (value == null) + // { + // writer.WriteBoolean(false); + // return; + // } + // + + Instruction labelNotNull = worker.Create(OpCodes.Nop); + worker.Emit(OpCodes.Ldarg_1); + worker.Emit(OpCodes.Brtrue, labelNotNull); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldc_I4_0); + worker.Emit(OpCodes.Call, GetWriteFunc(weaverTypes.Import(), ref WeavingFailed)); + worker.Emit(OpCodes.Ret); + worker.Append(labelNotNull); + + // write.WriteBoolean(true); + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldc_I4_1); + worker.Emit(OpCodes.Call, GetWriteFunc(weaverTypes.Import(), ref WeavingFailed)); + } + + // Find all fields in type and write them + bool WriteAllFields(TypeReference variable, ILProcessor worker, ref bool WeavingFailed) + { + foreach (FieldDefinition field in variable.FindAllPublicFields()) + { + MethodReference writeFunc = GetWriteFunc(field.FieldType, ref WeavingFailed); + // need this null check till later PR when GetWriteFunc throws exception instead + if (writeFunc == null) { return false; } + + FieldReference fieldRef = assembly.MainModule.ImportReference(field); + + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldarg_1); + worker.Emit(OpCodes.Ldfld, fieldRef); + worker.Emit(OpCodes.Call, writeFunc); + } + + return true; + } + + MethodDefinition GenerateCollectionWriter(TypeReference variable, TypeReference elementType, string writerFunction, ref bool WeavingFailed) + { + + MethodDefinition writerFunc = GenerateWriterFunc(variable); + + MethodReference elementWriteFunc = GetWriteFunc(elementType, ref WeavingFailed); + MethodReference intWriterFunc = GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + + // need this null check till later PR when GetWriteFunc throws exception instead + if (elementWriteFunc == null) + { + Log.Error($"Cannot generate writer for {variable}. Use a supported type or provide a custom writer", variable); + WeavingFailed = true; + return writerFunc; + } + + ModuleDefinition module = assembly.MainModule; + TypeReference readerExtensions = module.ImportReference(typeof(NetworkWriterExtensions)); + MethodReference collectionWriter = Resolvers.ResolveMethod(readerExtensions, assembly, Log, writerFunction, ref WeavingFailed); + + GenericInstanceMethod methodRef = new GenericInstanceMethod(collectionWriter); + methodRef.GenericArguments.Add(elementType); + + // generates + // reader.WriteArray(array); + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + worker.Emit(OpCodes.Ldarg_0); // writer + worker.Emit(OpCodes.Ldarg_1); // collection + + worker.Emit(OpCodes.Call, methodRef); // WriteArray + + worker.Emit(OpCodes.Ret); + + return writerFunc; + } + + // Save a delegate for each one of the writers into Writer{T}.write + internal void InitializeWriters(ILProcessor worker) + { + ModuleDefinition module = assembly.MainModule; + + TypeReference genericWriterClassRef = module.ImportReference(typeof(Writer<>)); + + System.Reflection.FieldInfo fieldInfo = typeof(Writer<>).GetField(nameof(Writer.write)); + FieldReference fieldRef = module.ImportReference(fieldInfo); + TypeReference networkWriterRef = module.ImportReference(typeof(NetworkWriter)); + TypeReference actionRef = module.ImportReference(typeof(Action<,>)); + MethodReference actionConstructorRef = module.ImportReference(typeof(Action<,>).GetConstructors()[0]); + + foreach (KeyValuePair kvp in writeFuncs) + { + TypeReference targetType = kvp.Key; + MethodReference writeFunc = kvp.Value; + + // create a Action delegate + worker.Emit(OpCodes.Ldnull); + worker.Emit(OpCodes.Ldftn, writeFunc); + GenericInstanceType actionGenericInstance = actionRef.MakeGenericInstanceType(networkWriterRef, targetType); + MethodReference actionRefInstance = actionConstructorRef.MakeHostInstanceGeneric(assembly.MainModule, actionGenericInstance); + worker.Emit(OpCodes.Newobj, actionRefInstance); + + // save it in Writer.write + GenericInstanceType genericInstance = genericWriterClassRef.MakeGenericInstanceType(targetType); + FieldReference specializedField = fieldRef.SpecializeField(assembly.MainModule, genericInstance); + worker.Emit(OpCodes.Stsfld, specializedField); + } + } + } +} diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs.meta b/Assets/Mirror/Editor/Weaver/Writers.cs.meta new file mode 100644 index 0000000..3769f7f --- /dev/null +++ b/Assets/Mirror/Editor/Weaver/Writers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a90060ad76ea044aba613080dd922709 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins.meta b/Assets/Mirror/Plugins.meta new file mode 100644 index 0000000..9504239 --- /dev/null +++ b/Assets/Mirror/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 05eb4061e2eb94061b9a08c918fff99b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil.meta b/Assets/Mirror/Plugins/Mono.Cecil.meta new file mode 100644 index 0000000..a104e2e --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ce126b4e1a7d13b4c865cd92929f13c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil/License.txt b/Assets/Mirror/Plugins/Mono.Cecil/License.txt new file mode 100644 index 0000000..2a9b88f --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/License.txt @@ -0,0 +1,25 @@ +Copyright (c) 2008 - 2015 Jb Evain +Copyright (c) 2008 - 2011 Novell, Inc. +Copyright vis2k + +https://github.com/jbevain/cecil +https://github.com/vis2k/cecil <- X + +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. diff --git a/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta new file mode 100644 index 0000000..9477cb6 --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ab858db5ebbb0d542a9acd197669cb5a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll new file mode 100644 index 0000000..f6815d3 Binary files /dev/null and b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll differ diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta new file mode 100644 index 0000000..d5555bf --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: a078fc7c0dc14d047a28dea9c93fd259 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll new file mode 100644 index 0000000..3b58436 Binary files /dev/null and b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll differ diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta new file mode 100644 index 0000000..3ab420f --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: 534d998d93b238041bddcd864f7f1088 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll new file mode 100644 index 0000000..7d820f0 Binary files /dev/null and b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll differ diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta new file mode 100644 index 0000000..aff0237 --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta @@ -0,0 +1,92 @@ +fileFormatVersion: 2 +guid: 7526641fb3ae25144aa0a96aad853745 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll new file mode 100644 index 0000000..c042418 Binary files /dev/null and b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll differ diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta new file mode 100644 index 0000000..f87dc69 --- /dev/null +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta @@ -0,0 +1,94 @@ +fileFormatVersion: 2 +guid: 307911e5ad044dd42b1649eb8637aaf3 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Readme.txt b/Assets/Mirror/Readme.txt new file mode 100644 index 0000000..5e9a4ec --- /dev/null +++ b/Assets/Mirror/Readme.txt @@ -0,0 +1,15 @@ +Mirror is a MMO Scale Networking library for Unity, used in uMMORPG, uSurvival and several MMO projects in development. + +*** IMPORTANT *** +You must restart Unity after importing Mirror for the Components Menu to update! + +Requirements: + Unity 2019 / 2020 LTS + Runtime .Net 4.x (Project Settings > Player > Other Settings) + +Documentation: + https://mirror-networking.gitbook.io/docs/ + +Support: + Discord: https://discordapp.com/invite/N9QVxbM + Bug Reports: https://github.com/vis2k/Mirror/issues diff --git a/Assets/Mirror/Readme.txt.meta b/Assets/Mirror/Readme.txt.meta new file mode 100644 index 0000000..d52ccce --- /dev/null +++ b/Assets/Mirror/Readme.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f6d84e019c68446f28415a923b460a03 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime.meta b/Assets/Mirror/Runtime.meta new file mode 100644 index 0000000..85ee3eb --- /dev/null +++ b/Assets/Mirror/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9f4328ccc5f724e45afe2215d275b5d5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs b/Assets/Mirror/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..f342716 --- /dev/null +++ b/Assets/Mirror/Runtime/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mirror.Tests.Common")] +[assembly: InternalsVisibleTo("Mirror.Tests")] +// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline +// for ILPostProcessor tests. +[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")] +[assembly: InternalsVisibleTo("Mirror.Tests.Generated")] +[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")] +[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")] +[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")] +[assembly: InternalsVisibleTo("Mirror.Editor")] diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..50cc028 --- /dev/null +++ b/Assets/Mirror/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e28d5f410e25b42e6a76a2ffc10e4675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Attributes.cs b/Assets/Mirror/Runtime/Attributes.cs new file mode 100644 index 0000000..39b06fd --- /dev/null +++ b/Assets/Mirror/Runtime/Attributes.cs @@ -0,0 +1,85 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// SyncVars are used to synchronize a variable from the server to all clients automatically. + /// Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server. + /// + [AttributeUsage(AttributeTargets.Field)] + public class SyncVarAttribute : PropertyAttribute + { + public string hook; + } + + /// + /// Call this from a client to run this function on the server. + /// Make sure to validate input etc. It's not possible to call this from a server. + /// + [AttributeUsage(AttributeTargets.Method)] + public class CommandAttribute : Attribute + { + public int channel = Channels.Reliable; + public bool requiresAuthority = true; + } + + /// + /// The server uses a Remote Procedure Call (RPC) to run this function on clients. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ClientRpcAttribute : Attribute + { + public int channel = Channels.Reliable; + public bool includeOwner = true; + } + + /// + /// The server uses a Remote Procedure Call (RPC) to run this function on a specific client. + /// + [AttributeUsage(AttributeTargets.Method)] + public class TargetRpcAttribute : Attribute + { + public int channel = Channels.Reliable; + } + + /// + /// Prevents clients from running this method. + /// Prints a warning if a client tries to execute this method. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ServerAttribute : Attribute {} + + /// + /// Prevents clients from running this method. + /// No warning is thrown. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ServerCallbackAttribute : Attribute {} + + /// + /// Prevents the server from running this method. + /// Prints a warning if the server tries to execute this method. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ClientAttribute : Attribute {} + + /// + /// Prevents the server from running this method. + /// No warning is printed. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ClientCallbackAttribute : Attribute {} + + /// + /// Converts a string property into a Scene property in the inspector + /// + public class SceneAttribute : PropertyAttribute {} + + /// + /// Used to show private SyncList in the inspector, + /// Use instead of SerializeField for non Serializable types + /// + [AttributeUsage(AttributeTargets.Field)] + public class ShowInInspectorAttribute : Attribute {} +} diff --git a/Assets/Mirror/Runtime/Attributes.cs.meta b/Assets/Mirror/Runtime/Attributes.cs.meta new file mode 100644 index 0000000..c50a489 --- /dev/null +++ b/Assets/Mirror/Runtime/Attributes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c04c722ee2ffd49c8a56ab33667b10b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Batching.meta b/Assets/Mirror/Runtime/Batching.meta new file mode 100644 index 0000000..bf23600 --- /dev/null +++ b/Assets/Mirror/Runtime/Batching.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c38e1bebe9947f8b842a8a57aa2b71c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs b/Assets/Mirror/Runtime/Batching/Batcher.cs new file mode 100644 index 0000000..3a8d457 --- /dev/null +++ b/Assets/Mirror/Runtime/Batching/Batcher.cs @@ -0,0 +1,127 @@ +// batching functionality encapsulated into one class. +// -> less complexity +// -> easy to test +// +// IMPORTANT: we use THRESHOLD batching, not MAXED SIZE batching. +// see threshold comments below. +// +// includes timestamp for tick batching. +// -> allows NetworkTransform etc. to use timestamp without including it in +// every single message +using System; +using System.Collections.Generic; + +namespace Mirror +{ + public class Batcher + { + // batching threshold instead of max size. + // -> small messages are fit into threshold sized batches + // -> messages larger than threshold are single batches + // + // in other words, we fit up to 'threshold' but still allow larger ones + // for two reasons: + // 1.) data races: skipping batching for larger messages would send a + // large spawn message immediately, while others are batched and + // only flushed at the end of the frame + // 2) timestamp batching: if each batch is expected to contain a + // timestamp, then large messages have to be a batch too. otherwise + // they would not contain a timestamp + readonly int threshold; + + // TimeStamp header size for those who need it + public const int HeaderSize = sizeof(double); + + // full batches ready to be sent. + // DO NOT queue NetworkMessage, it would box. + // DO NOT queue each serialization separately. + // it would allocate too many writers. + // https://github.com/vis2k/Mirror/pull/3127 + // => best to build batches on the fly. + Queue batches = new Queue(); + + // current batch in progress + NetworkWriterPooled batch; + + public Batcher(int threshold) + { + this.threshold = threshold; + } + + // add a message for batching + // we allow any sized messages. + // caller needs to make sure they are within max packet size. + public void AddMessage(ArraySegment message, double timeStamp) + { + // when appending to a batch in progress, check final size. + // if it expands beyond threshold, then we should finalize it first. + // => less than or exactly threshold is fine. + // GetBatch() will finalize it. + // => see unit tests. + if (batch != null && + batch.Position + message.Count > threshold) + { + batches.Enqueue(batch); + batch = null; + } + + // initialize a new batch if necessary + if (batch == null) + { + // borrow from pool. we return it in GetBatch. + batch = NetworkWriterPool.Get(); + + // write timestamp first. + // -> double precision for accuracy over long periods of time + // -> batches are per-frame, it doesn't matter which message's + // timestamp we use. + batch.WriteDouble(timeStamp); + } + + // add serialization to current batch. even if > threshold. + // -> we do allow > threshold sized messages as single batch + // -> WriteBytes instead of WriteSegment because the latter + // would add a size header. we want to write directly. + batch.WriteBytes(message.Array, message.Offset, message.Count); + } + + // helper function to copy a batch to writer and return it to pool + static void CopyAndReturn(NetworkWriterPooled batch, NetworkWriter writer) + { + // make sure the writer is fresh to avoid uncertain situations + if (writer.Position != 0) + throw new ArgumentException($"GetBatch needs a fresh writer!"); + + // copy to the target writer + ArraySegment segment = batch.ToArraySegment(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + + // return batch to pool for reuse + NetworkWriterPool.Return(batch); + } + + // get the next batch which is available for sending (if any). + // TODO safely get & return a batch instead of copying to writer? + // TODO could return pooled writer & use GetBatch in a 'using' statement! + public bool GetBatch(NetworkWriter writer) + { + // get first batch from queue (if any) + if (batches.TryDequeue(out NetworkWriterPooled first)) + { + CopyAndReturn(first, writer); + return true; + } + + // if queue was empty, we can send the batch in progress. + if (batch != null) + { + CopyAndReturn(batch, writer); + batch = null; + return true; + } + + // nothing was written + return false; + } + } +} diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta b/Assets/Mirror/Runtime/Batching/Batcher.cs.meta new file mode 100644 index 0000000..a774908 --- /dev/null +++ b/Assets/Mirror/Runtime/Batching/Batcher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0afaaa611a2142d48a07bdd03b68b2b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs b/Assets/Mirror/Runtime/Batching/Unbatcher.cs new file mode 100644 index 0000000..495ada9 --- /dev/null +++ b/Assets/Mirror/Runtime/Batching/Unbatcher.cs @@ -0,0 +1,142 @@ +// un-batching functionality encapsulated into one class. +// -> less complexity +// -> easy to test +// +// includes timestamp for tick batching. +// -> allows NetworkTransform etc. to use timestamp without including it in +// every single message +using System; +using System.Collections.Generic; + +namespace Mirror +{ + public class Unbatcher + { + // supporting adding multiple batches before GetNextMessage is called. + // just in case. + Queue batches = new Queue(); + + public int BatchesCount => batches.Count; + + // NetworkReader is only created once, + // then pointed to the first batch. + NetworkReader reader = new NetworkReader(new byte[0]); + + // timestamp that was written into the batch remotely. + // for the batch that our reader is currently pointed at. + double readerRemoteTimeStamp; + + // helper function to start reading a batch. + void StartReadingBatch(NetworkWriterPooled batch) + { + // point reader to it + reader.SetBuffer(batch.ToArraySegment()); + + // read remote timestamp (double) + // -> AddBatch quarantees that we have at least 8 bytes to read + readerRemoteTimeStamp = reader.ReadDouble(); + } + + // add a new batch. + // returns true if valid. + // returns false if not, in which case the connection should be disconnected. + public bool AddBatch(ArraySegment batch) + { + // IMPORTANT: ArraySegment is only valid until returning. we copy it! + // + // NOTE: it's not possible to create empty ArraySegments, so we + // don't need to check against that. + + // make sure we have at least 8 bytes to read for tick timestamp + if (batch.Count < Batcher.HeaderSize) + return false; + + // put into a (pooled) writer + // -> WriteBytes instead of WriteSegment because the latter + // would add a size header. we want to write directly. + // -> will be returned to pool when sending! + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteBytes(batch.Array, batch.Offset, batch.Count); + + // first batch? then point reader there + if (batches.Count == 0) + StartReadingBatch(writer); + + // add batch + batches.Enqueue(writer); + //Debug.Log($"Adding Batch {BitConverter.ToString(batch.Array, batch.Offset, batch.Count)} => batches={batches.Count} reader={reader}"); + return true; + } + + // get next message, unpacked from batch (if any) + // timestamp is the REMOTE time when the batch was created remotely. + public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp) + { + // getting messages would be easy via + // <> + // but to save A LOT of bandwidth, we use + // < + // in other words, we don't know where the current message ends + // + // BUT: it doesn't matter! + // -> we simply return the reader + // * if we have one yet + // * and if there's more to read + // -> the caller can then read one message from it + // -> when the end is reached, we retire the batch! + // + // for example: + // while (GetNextMessage(out message)) + // ProcessMessage(message); + // + message = null; + + // do nothing if we don't have any batches. + // otherwise the below queue.Dequeue() would throw an + // InvalidOperationException if operating on empty queue. + if (batches.Count == 0) + { + remoteTimeStamp = 0; + return false; + } + + // was our reader pointed to anything yet? + if (reader.Length == 0) + { + remoteTimeStamp = 0; + return false; + } + + // no more data to read? + if (reader.Remaining == 0) + { + // retire the batch + NetworkWriterPooled writer = batches.Dequeue(); + NetworkWriterPool.Return(writer); + + // do we have another batch? + if (batches.Count > 0) + { + // point reader to the next batch. + // we'll return the reader below. + NetworkWriterPooled next = batches.Peek(); + StartReadingBatch(next); + } + // otherwise there's nothing more to read + else + { + remoteTimeStamp = 0; + return false; + } + } + + // use the current batch's remote timestamp + // AFTER potentially moving to the next batch ABOVE! + remoteTimeStamp = readerRemoteTimeStamp; + + // if we got here, then we have more data to read. + message = reader; + return true; + } + } +} diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta b/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta new file mode 100644 index 0000000..26038b0 --- /dev/null +++ b/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 328562d71e1c45c58581b958845aa7a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Compression.cs b/Assets/Mirror/Runtime/Compression.cs new file mode 100644 index 0000000..3e4b0f6 --- /dev/null +++ b/Assets/Mirror/Runtime/Compression.cs @@ -0,0 +1,361 @@ +// Quaternion compression from DOTSNET +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + /// Functions to Compress Quaternions and Floats + public static class Compression + { + // quaternion compression ////////////////////////////////////////////// + // smallest three: https://gafferongames.com/post/snapshot_compression/ + // compresses 16 bytes quaternion into 4 bytes + + // helper function to find largest absolute element + // returns the index of the largest one + public static int LargestAbsoluteComponentIndex(Vector4 value, out float largestAbs, out Vector3 withoutLargest) + { + // convert to abs + Vector4 abs = new Vector4(Mathf.Abs(value.x), Mathf.Abs(value.y), Mathf.Abs(value.z), Mathf.Abs(value.w)); + + // set largest to first abs (x) + largestAbs = abs.x; + withoutLargest = new Vector3(value.y, value.z, value.w); + int largestIndex = 0; + + // compare to the others, starting at second value + // performance for 100k calls + // for-loop: 25ms + // manual checks: 22ms + if (abs.y > largestAbs) + { + largestIndex = 1; + largestAbs = abs.y; + withoutLargest = new Vector3(value.x, value.z, value.w); + } + if (abs.z > largestAbs) + { + largestIndex = 2; + largestAbs = abs.z; + withoutLargest = new Vector3(value.x, value.y, value.w); + } + if (abs.w > largestAbs) + { + largestIndex = 3; + largestAbs = abs.w; + withoutLargest = new Vector3(value.x, value.y, value.z); + } + + return largestIndex; + } + + // scale a float within min/max range to an ushort between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + // max ushort - min ushort only fits into something bigger + int targetRange = maxTarget - minTarget; + float valueRange = maxValue - minValue; + float valueRelative = value - minValue; + return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange)); + } + + // scale an ushort within min/max range to a float between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + float targetRange = maxTarget - minTarget; + ushort valueRange = (ushort)(maxValue - minValue); + ushort valueRelative = (ushort)(value - minValue); + return minTarget + (valueRelative / (float)valueRange * targetRange); + } + + const float QuaternionMinRange = -0.707107f; + const float QuaternionMaxRange = 0.707107f; + const ushort TenBitsMax = 0x3FF; + + // helper function to access 'nth' component of quaternion + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static float QuaternionElement(Quaternion q, int element) + { + switch (element) + { + case 0: return q.x; + case 1: return q.y; + case 2: return q.z; + case 3: return q.w; + default: return 0; + } + } + + // note: assumes normalized quaternions + public static uint CompressQuaternion(Quaternion q) + { + // note: assuming normalized quaternions is enough. no need to force + // normalize here. we already normalize when decompressing. + + // find the largest component index [0,3] + value + int largestIndex = LargestAbsoluteComponentIndex(new Vector4(q.x, q.y, q.z, q.w), out float _, out Vector3 withoutLargest); + + // from here on, we work with the 3 components without largest! + + // "You might think you need to send a sign bit for [largest] in + // case it is negative, but you don’t, because you can make + // [largest] always positive by negating the entire quaternion if + // [largest] is negative. in quaternion space (x,y,z,w) and + // (-x,-y,-z,-w) represent the same rotation." + if (QuaternionElement(q, largestIndex) < 0) + withoutLargest = -withoutLargest; + + // put index & three floats into one integer. + // => index is 2 bits (4 values require 2 bits to store them) + // => the three floats are between [-0.707107,+0.707107] because: + // "If v is the absolute value of the largest quaternion + // component, the next largest possible component value occurs + // when two components have the same absolute value and the + // other two components are zero. The length of that quaternion + // (v,v,0,0) is 1, therefore v^2 + v^2 = 1, 2v^2 = 1, + // v = 1/sqrt(2). This means you can encode the smallest three + // components in [-0.707107,+0.707107] instead of [-1,+1] giving + // you more precision with the same number of bits." + // => the article recommends storing each float in 9 bits + // => our uint has 32 bits, so we might as well store in (32-2)/3=10 + // 10 bits max value: 1023=0x3FF (use OSX calc to flip 10 bits) + ushort aScaled = ScaleFloatToUShort(withoutLargest.x, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax); + ushort bScaled = ScaleFloatToUShort(withoutLargest.y, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax); + ushort cScaled = ScaleFloatToUShort(withoutLargest.z, QuaternionMinRange, QuaternionMaxRange, 0, TenBitsMax); + + // now we just need to pack them into one integer + // -> index is 2 bit and needs to be shifted to 31..32 + // -> a is 10 bit and needs to be shifted 20..30 + // -> b is 10 bit and needs to be shifted 10..20 + // -> c is 10 bit and needs to be at 0..10 + return (uint)(largestIndex << 30 | aScaled << 20 | bScaled << 10 | cScaled); + } + + // Quaternion normalizeSAFE from ECS math.normalizesafe() + // => useful to produce valid quaternions even if client sends invalid + // data + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Quaternion QuaternionNormalizeSafe(Quaternion value) + { + // The smallest positive normal number representable in a float. + const float FLT_MIN_NORMAL = 1.175494351e-38F; + + Vector4 v = new Vector4(value.x, value.y, value.z, value.w); + float length = Vector4.Dot(v, v); + return length > FLT_MIN_NORMAL + ? value.normalized + : Quaternion.identity; + } + + // note: gives normalized quaternions + public static Quaternion DecompressQuaternion(uint data) + { + // get cScaled which is at 0..10 and ignore the rest + ushort cScaled = (ushort)(data & TenBitsMax); + + // get bScaled which is at 10..20 and ignore the rest + ushort bScaled = (ushort)((data >> 10) & TenBitsMax); + + // get aScaled which is at 20..30 and ignore the rest + ushort aScaled = (ushort)((data >> 20) & TenBitsMax); + + // get 2 bit largest index, which is at 31..32 + int largestIndex = (int)(data >> 30); + + // scale back to floats + float a = ScaleUShortToFloat(aScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange); + float b = ScaleUShortToFloat(bScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange); + float c = ScaleUShortToFloat(cScaled, 0, TenBitsMax, QuaternionMinRange, QuaternionMaxRange); + + // calculate the omitted component based on a²+b²+c²+d²=1 + float d = Mathf.Sqrt(1 - a*a - b*b - c*c); + + // reconstruct based on largest index + Vector4 value; + switch (largestIndex) + { + case 0: value = new Vector4(d, a, b, c); break; + case 1: value = new Vector4(a, d, b, c); break; + case 2: value = new Vector4(a, b, d, c); break; + default: value = new Vector4(a, b, c, d); break; + } + + // ECS Rotation only works with normalized quaternions. + // make sure that's always the case here to avoid ECS bugs where + // everything stops moving if the quaternion isn't normalized. + // => NormalizeSafe returns a normalized quaternion even if we pass + // in NaN from deserializing invalid values! + return QuaternionNormalizeSafe(new Quaternion(value.x, value.y, value.z, value.w)); + } + + // varint compression ////////////////////////////////////////////////// + // compress ulong varint. + // same result for int, short and byte. only need one function. + // NOT an extension. otherwise weaver might accidentally use it. + public static void CompressVarUInt(NetworkWriter writer, ulong value) + { + if (value <= 240) + { + writer.WriteByte((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByte((byte)(((value - 240) >> 8) + 241)); + writer.WriteByte((byte)((value - 240) & 0xFF)); + return; + } + if (value <= 67823) + { + writer.WriteByte((byte)249); + writer.WriteByte((byte)((value - 2288) >> 8)); + writer.WriteByte((byte)((value - 2288) & 0xFF)); + return; + } + if (value <= 16777215) + { + writer.WriteByte((byte)250); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + return; + } + if (value <= 4294967295) + { + writer.WriteByte((byte)251); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + return; + } + if (value <= 1099511627775) + { + writer.WriteByte((byte)252); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + return; + } + if (value <= 281474976710655) + { + writer.WriteByte((byte)253); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + return; + } + if (value <= 72057594037927935) + { + writer.WriteByte((byte)254); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + writer.WriteByte((byte)((value >> 48) & 0xFF)); + return; + } + + // all others + { + writer.WriteByte((byte)255); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + writer.WriteByte((byte)((value >> 48) & 0xFF)); + writer.WriteByte((byte)((value >> 56) & 0xFF)); + } + } + + // zigzag encoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CompressVarInt(NetworkWriter writer, long i) + { + ulong zigzagged = (ulong)((i >> 63) ^ (i << 1)); + CompressVarUInt(writer, zigzagged); + } + + // NOT an extension. otherwise weaver might accidentally use it. + public static ulong DecompressVarUInt(NetworkReader reader) + { + byte a0 = reader.ReadByte(); + if (a0 < 241) + { + return a0; + } + + byte a1 = reader.ReadByte(); + if (a0 <= 248) + { + return 240 + ((a0 - (ulong)241) << 8) + a1; + } + + byte a2 = reader.ReadByte(); + if (a0 == 249) + { + return 2288 + ((ulong)a1 << 8) + a2; + } + + byte a3 = reader.ReadByte(); + if (a0 == 250) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16); + } + + byte a4 = reader.ReadByte(); + if (a0 == 251) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24); + } + + byte a5 = reader.ReadByte(); + if (a0 == 252) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32); + } + + byte a6 = reader.ReadByte(); + if (a0 == 253) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40); + } + + byte a7 = reader.ReadByte(); + if (a0 == 254) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48); + } + + byte a8 = reader.ReadByte(); + if (a0 == 255) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48) + (((ulong)a8) << 56); + } + + throw new IndexOutOfRangeException($"DecompressVarInt failure: {a0}"); + } + + // zigzag decoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long DecompressVarInt(NetworkReader reader) + { + ulong data = DecompressVarUInt(reader); + return ((long)(data >> 1)) ^ -((long)data & 1); + } + } +} diff --git a/Assets/Mirror/Runtime/Compression.cs.meta b/Assets/Mirror/Runtime/Compression.cs.meta new file mode 100644 index 0000000..e35474b --- /dev/null +++ b/Assets/Mirror/Runtime/Compression.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c28963f9c4b97e418252a55500fb91e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty.meta b/Assets/Mirror/Runtime/Empty.meta new file mode 100644 index 0000000..e702402 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a99666a026b14cf6ba1a2b65946b1b27 +timeCreated: 1615288671 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs b/Assets/Mirror/Runtime/Empty/ClientScene.cs new file mode 100644 index 0000000..0d1b96e --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/ClientScene.cs @@ -0,0 +1 @@ +// moved into NetworkClient on 2021-03-07 diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta b/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta new file mode 100644 index 0000000..82b617e --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96fc7967f813e4960b9119d7c2118494 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud.meta b/Assets/Mirror/Runtime/Empty/Cloud.meta new file mode 100644 index 0000000..e2c44de --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 73a9bb2dacafa8141bce8feef34e33a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta new file mode 100644 index 0000000..9279c0c --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8bdb99a29e179d14cb0acc43f175d9ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta new file mode 100644 index 0000000..98a4c11 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f6e5d5acb5879f45a2235ae0f44dc92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta new file mode 100644 index 0000000..a6fc272 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b4e9cc0829b13e54594a80883836bda7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta new file mode 100644 index 0000000..b914a33 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9cc796972dc396a42ba3686bd952e329 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta new file mode 100644 index 0000000..f66b84e --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70f563b7a7210ae43bbcde5cb7721a94 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta new file mode 100644 index 0000000..150d85b --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7c472a3ea1bc4348bd5a0b05bf7cc3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta new file mode 100644 index 0000000..6bf6291 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97501e783fc67a4459b15d10e6c63563 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta new file mode 100644 index 0000000..f1149a9 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43472c60a7c72e54eafe559290dd0fc6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta new file mode 100644 index 0000000..966c503 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b80b95532a9d6e8418aa676a261e4f69 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta new file mode 100644 index 0000000..7cb2a59 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05185b973ba389a4588fc8a99c75a4f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta new file mode 100644 index 0000000..4b7219b --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbabb497385c20346a3c8bda4ae69508 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta new file mode 100644 index 0000000..2c04009 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0688c0fdae5376e4ea74d5c3904eed17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta new file mode 100644 index 0000000..519876d --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6f0311899162c5b49a3c11fa9bd9c133 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta new file mode 100644 index 0000000..a9d32ea --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6838f9df45594d48873518cbb75b329 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta new file mode 100644 index 0000000..306bf7c --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d49649fb32cb96b46b10f013b38a4b50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta new file mode 100644 index 0000000..7e206f1 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a963606335eae0f47abe7ecb5fd028ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta new file mode 100644 index 0000000..82e23fd --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 675f0d0fd4e82b04290c4d30c8d78ede +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta new file mode 100644 index 0000000..5984ce3 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 457ba2df6cb6e1542996c17c715ee81b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta new file mode 100644 index 0000000..86775df --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95bebb8e810e2954485291a26324f7d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta new file mode 100644 index 0000000..5c4294f --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 068feff770f710141afa4a90063a5e6c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta new file mode 100644 index 0000000..1c85828 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b6cfd54b79bb464dbc6ae7f331ed45f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta new file mode 100644 index 0000000..4a22565 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07d1ea5260bc06e4d831c4b61d494bff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta new file mode 100644 index 0000000..67341ea --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76dab753e7255254687cd57985d8d675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta new file mode 100644 index 0000000..eb139af --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfaa626443cc7c94eae138a2e3a04d7c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta new file mode 100644 index 0000000..74c6a0f --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bfc354d4a7f63ca45a653bf5d479afa0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta new file mode 100644 index 0000000..f7fe4f2 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed11184fcffcdc04c9850d82c8014926 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs new file mode 100644 index 0000000..2f11787 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs @@ -0,0 +1 @@ +// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta new file mode 100644 index 0000000..d8857e8 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c67eda1b451338a428df87fda1e3a7c9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta new file mode 100644 index 0000000..8742197 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b307f850ccbbe450295acf24d70e5c28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs new file mode 100644 index 0000000..57f3344 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs @@ -0,0 +1 @@ +// removed 2021-05-13 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta new file mode 100644 index 0000000..509a58f --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 330c9aab13d2d42069c6ebbe582b73ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs b/Assets/Mirror/Runtime/Empty/LogFactory.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/LogFactory.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta new file mode 100644 index 0000000..0715501 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 353c7c9e14e82f349b1679112050b196 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs b/Assets/Mirror/Runtime/Empty/LogFilter.cs new file mode 100644 index 0000000..391c5bd --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/LogFilter.cs @@ -0,0 +1 @@ +// removed 2021-03-08 diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta b/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta new file mode 100644 index 0000000..aab4fa0 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6928b080072948f7b2909b4025fcc79 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging.meta b/Assets/Mirror/Runtime/Empty/Logging.meta new file mode 100644 index 0000000..867da74 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63d647500ca1bfa4a845bc1f4cff9dcc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta new file mode 100644 index 0000000..329c6eb --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2a9618569c20a504aa86feb5913c70e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta new file mode 100644 index 0000000..81b33e9 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a39aa1e48aa54eb4e964f0191c1dcdce +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta new file mode 100644 index 0000000..acf3b63 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d06522432d5a44e1587967a4731cd279 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs new file mode 100644 index 0000000..264a1cd --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs @@ -0,0 +1,2 @@ +// removed 2021-02-16 + diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta new file mode 100644 index 0000000..90c4e4d --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 633889a39717fde4fa28dd6b948dfac7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta new file mode 100644 index 0000000..221a61b --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7627623f2b9fad4484082517cd73e67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta new file mode 100644 index 0000000..2f7ecdf --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac6e8eccf4b6f4dc7b24c276ef47fde8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs new file mode 100644 index 0000000..3797620 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs @@ -0,0 +1 @@ +// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta new file mode 100644 index 0000000..7c7d6cf --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1020a74962faada4b807ac5dc053a4cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs new file mode 100644 index 0000000..712833c --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs @@ -0,0 +1 @@ +// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta new file mode 100644 index 0000000..fee7725 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25fd0c51bbe07c140bc30978b91e9182 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs new file mode 100644 index 0000000..3797620 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs @@ -0,0 +1 @@ +// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta new file mode 100644 index 0000000..c5aa112 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1731d8de2d0c84333b08ebe1e79f4118 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs new file mode 100644 index 0000000..3797620 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs @@ -0,0 +1 @@ +// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta new file mode 100644 index 0000000..b451655 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7fdb599e1359924bad6255660370252 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs new file mode 100644 index 0000000..3797620 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs @@ -0,0 +1 @@ +// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta new file mode 100644 index 0000000..f71b7be --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c08f1a030234d49d391d7223a8592f15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs b/Assets/Mirror/Runtime/Empty/StringHash.cs new file mode 100644 index 0000000..39b95f7 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/StringHash.cs @@ -0,0 +1 @@ +// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta b/Assets/Mirror/Runtime/Empty/StringHash.cs.meta new file mode 100644 index 0000000..6198581 --- /dev/null +++ b/Assets/Mirror/Runtime/Empty/StringHash.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 733f020f9b76d453da841089579fd7a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs b/Assets/Mirror/Runtime/ExponentialMovingAverage.cs new file mode 100644 index 0000000..64b91e1 --- /dev/null +++ b/Assets/Mirror/Runtime/ExponentialMovingAverage.cs @@ -0,0 +1,37 @@ +namespace Mirror +{ + // implementation of N-day EMA + // it calculates an exponential moving average roughly equivalent to the last n observations + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + public class ExponentialMovingAverage + { + readonly float alpha; + bool initialized; + + public double Value; + public double Var; + + public ExponentialMovingAverage(int n) + { + // standard N-day EMA alpha calculation + alpha = 2.0f / (n + 1); + } + + public void Add(double newValue) + { + // simple algorithm for EMA described here: + // https://en.wikipedia.org/wiki/Moving_average#Exponentially_weighted_moving_variance_and_standard_deviation + if (initialized) + { + double delta = newValue - Value; + Value += alpha * delta; + Var = (1 - alpha) * (Var + alpha * delta * delta); + } + else + { + Value = newValue; + initialized = true; + } + } + } +} diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta b/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta new file mode 100644 index 0000000..d0d8210 --- /dev/null +++ b/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05e858cbaa54b4ce4a48c8c7f50c1914 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Extensions.cs b/Assets/Mirror/Runtime/Extensions.cs new file mode 100644 index 0000000..3d285e9 --- /dev/null +++ b/Assets/Mirror/Runtime/Extensions.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class Extensions + { + // string.GetHashCode is not guaranteed to be the same on all machines, but + // we need one that is the same on all machines. simple and stupid: + public static int GetStableHashCode(this string text) + { + unchecked + { + int hash = 23; + foreach (char c in text) + hash = hash * 31 + c; + return hash; + } + } + + // previously in DotnetCompatibility.cs + // leftover from the UNET days. supposedly for windows store? + internal static string GetMethodName(this Delegate func) + { +#if NETFX_CORE + return func.GetMethodInfo().Name; +#else + return func.Method.Name; +#endif + } + + // helper function to copy to List + // C# only provides CopyTo(T[]) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this IEnumerable source, List destination) + { + // foreach allocates. use AddRange. + destination.AddRange(source); + } + +#if !UNITY_2021_OR_NEWER + // Unity 2019 / 2020 don't have Queue.TryDeque which we need for batching. + public static bool TryDequeue(this Queue source, out T element) + { + if (source.Count > 0) + { + element = source.Dequeue(); + return true; + } + + element = default; + return false; + } +#endif + } +} diff --git a/Assets/Mirror/Runtime/Extensions.cs.meta b/Assets/Mirror/Runtime/Extensions.cs.meta new file mode 100644 index 0000000..c2a18b7 --- /dev/null +++ b/Assets/Mirror/Runtime/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: decf32fd053744d18f35712b7a6f5116 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/InterestManagement.cs b/Assets/Mirror/Runtime/InterestManagement.cs new file mode 100644 index 0000000..ab149c3 --- /dev/null +++ b/Assets/Mirror/Runtime/InterestManagement.cs @@ -0,0 +1,99 @@ +// interest management component for custom solutions like +// distance based, spatial hashing, raycast based, etc. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public abstract class InterestManagement : MonoBehaviour + { + // Awake configures InterestManagement in NetworkServer/Client + // Do NOT check for active server or client here. + // Awake must always set the static aoi references. + void Awake() + { + if (NetworkServer.aoi == null) + { + NetworkServer.aoi = this; + } + else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already."); + + if (NetworkClient.aoi == null) + { + NetworkClient.aoi = this; + } + else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already."); + } + + [ServerCallback] + public virtual void Reset() {} + + // Callback used by the visibility system to determine if an observer + // (player) can see the NetworkIdentity. If this function returns true, + // the network connection will be added as an observer. + // conn: Network connection of a player. + // returns True if the player can see this object. + public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver); + + // rebuild observers for the given NetworkIdentity. + // Server will automatically spawn/despawn added/removed ones. + // newObservers: cached hashset to put the result into + // initialize: true if being rebuilt for the first time + // + // IMPORTANT: + // => global rebuild would be more simple, BUT + // => local rebuild is way faster for spawn/despawn because we can + // simply rebuild a select NetworkIdentity only + // => having both .observers and .observing is necessary for local + // rebuilds + // + // in other words, this is the perfect solution even though it's not + // completely simple (due to .observers & .observing). + // + // Mirror maintains .observing automatically in the background. best of + // both worlds without any worrying now! + public abstract void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers); + + // helper function to trigger a full rebuild. + // most implementations should call this in a certain interval. + // some might call this all the time, or only on team changes or + // scene changes and so on. + // + // IMPORTANT: check if NetworkServer.active when using Update()! + [ServerCallback] + protected void RebuildAll() + { + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) + { + NetworkServer.RebuildObservers(identity, false); + } + } + + // Callback used by the visibility system for objects on a host. + // Objects on a host (with a local client) cannot be disabled or + // destroyed when they are not visible to the local client. So this + // function is called to allow custom code to hide these objects. A + // typical implementation will disable renderer components on the + // object. This is only called on local clients on a host. + // => need the function in here and virtual so people can overwrite! + // => not everyone wants to hide renderers! + [ServerCallback] + public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) + { + foreach (Renderer rend in identity.GetComponentsInChildren()) + rend.enabled = visible; + } + + /// Called on the server when a new networked object is spawned. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnSpawned(NetworkIdentity identity) {} + + /// Called on the server when a networked object is destroyed. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnDestroyed(NetworkIdentity identity) {} + } +} diff --git a/Assets/Mirror/Runtime/InterestManagement.cs.meta b/Assets/Mirror/Runtime/InterestManagement.cs.meta new file mode 100644 index 0000000..bfabf6b --- /dev/null +++ b/Assets/Mirror/Runtime/InterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41d809934003479f97e992eebb7ed6af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs b/Assets/Mirror/Runtime/LocalConnectionToClient.cs new file mode 100644 index 0000000..67c9649 --- /dev/null +++ b/Assets/Mirror/Runtime/LocalConnectionToClient.cs @@ -0,0 +1,47 @@ +using System; + +namespace Mirror +{ + // a server's connection TO a LocalClient. + // sending messages on this connection causes the client's handler function to be invoked directly + public class LocalConnectionToClient : NetworkConnectionToClient + { + internal LocalConnectionToServer connectionToServer; + + public LocalConnectionToClient() : base(LocalConnectionId) {} + + public override string address => "localhost"; + + // Send stage two: serialized NetworkMessage as ArraySegment + internal override void Send(ArraySegment segment, int channelId = Channels.Reliable) + { + // get a writer to copy the message into since the segment is only + // valid until returning. + // => pooled writer will be returned to pool when dequeuing. + // => WriteBytes instead of WriteArraySegment because the latter + // includes a 4 bytes header. we just want to write raw. + //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + connectionToServer.queue.Enqueue(writer); + } + + // true because local connections never timeout + internal override bool IsAlive(float timeout) => true; + + internal void DisconnectInternal() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + RemoveFromObservingsObservers(); + } + + /// Disconnects this connection. + public override void Disconnect() + { + DisconnectInternal(); + connectionToServer.DisconnectInternal(); + } + } +} diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta b/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta new file mode 100644 index 0000000..42243ed --- /dev/null +++ b/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e79d1be9a9a54e240ab239f687376c8e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs b/Assets/Mirror/Runtime/LocalConnectionToServer.cs new file mode 100644 index 0000000..378ffdb --- /dev/null +++ b/Assets/Mirror/Runtime/LocalConnectionToServer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // a localClient's connection TO a server. + // send messages on this connection causes the server's handler function to be invoked directly. + public class LocalConnectionToServer : NetworkConnectionToServer + { + internal LocalConnectionToClient connectionToClient; + + // packet queue + internal readonly Queue queue = new Queue(); + + public override string address => "localhost"; + + // see caller for comments on why we need this + bool connectedEventPending; + bool disconnectedEventPending; + internal void QueueConnectedEvent() => connectedEventPending = true; + internal void QueueDisconnectedEvent() => disconnectedEventPending = true; + + // Send stage two: serialized NetworkMessage as ArraySegment + internal override void Send(ArraySegment segment, int channelId = Channels.Reliable) + { + if (segment.Count == 0) + { + Debug.LogError("LocalConnection.SendBytes cannot send zero bytes"); + return; + } + + // OnTransportData assumes batching. + // so let's make a batch with proper timestamp prefix. + Batcher batcher = GetBatchForChannelId(channelId); + batcher.AddMessage(segment, NetworkTime.localTime); + + // flush it to the server's OnTransportData immediately. + // local connection to server always invokes immediately. + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // make a batch with our local time (double precision) + if (batcher.GetBatch(writer)) + { + NetworkServer.OnTransportData(connectionId, writer.ToArraySegment(), channelId); + } + else Debug.LogError("Local connection failed to make batch. This should never happen."); + } + } + + internal override void Update() + { + base.Update(); + + // should we still process a connected event? + if (connectedEventPending) + { + connectedEventPending = false; + NetworkClient.OnConnectedEvent?.Invoke(); + } + + // process internal messages so they are applied at the correct time + while (queue.Count > 0) + { + // call receive on queued writer's content, return to pool + NetworkWriterPooled writer = queue.Dequeue(); + ArraySegment message = writer.ToArraySegment(); + + // OnTransportData assumes a proper batch with timestamp etc. + // let's make a proper batch and pass it to OnTransportData. + Batcher batcher = GetBatchForChannelId(Channels.Reliable); + batcher.AddMessage(message, NetworkTime.localTime); + + using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get()) + { + // make a batch with our local time (double precision) + if (batcher.GetBatch(batchWriter)) + { + NetworkClient.OnTransportData(batchWriter.ToArraySegment(), Channels.Reliable); + } + } + + NetworkWriterPool.Return(writer); + } + + // should we still process a disconnected event? + if (disconnectedEventPending) + { + disconnectedEventPending = false; + NetworkClient.OnDisconnectedEvent?.Invoke(); + } + } + + /// Disconnects this connection. + internal void DisconnectInternal() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + // TODO remove redundant state. have one source of truth for .ready! + isReady = false; + NetworkClient.ready = false; + } + + /// Disconnects this connection. + public override void Disconnect() + { + connectionToClient.DisconnectInternal(); + DisconnectInternal(); + + // simulate what a true remote connection would do: + // first, the server should remove it: + // TODO should probably be in connectionToClient.DisconnectInternal + // because that's the NetworkServer's connection! + NetworkServer.RemoveLocalConnection(); + + // then call OnTransportDisconnected for proper disconnect handling, + // callbacks & cleanups. + // => otherwise OnClientDisconnected() is never called! + // => see NetworkClientTests.DisconnectCallsOnClientDisconnect_HostMode() + NetworkClient.OnTransportDisconnected(); + } + + // true because local connections never timeout + internal override bool IsAlive(float timeout) => true; + } +} diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta b/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta new file mode 100644 index 0000000..856b255 --- /dev/null +++ b/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cdfff390c3504158a269e8b8662e2a40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Mathd.cs b/Assets/Mirror/Runtime/Mathd.cs new file mode 100644 index 0000000..2dfa2f9 --- /dev/null +++ b/Assets/Mirror/Runtime/Mathd.cs @@ -0,0 +1,28 @@ +// 'double' precision variants for some of Unity's Mathf functions. + +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class Mathd + { + /// Linearly interpolates between a and b by t with no limit to t. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LerpUnclamped(double a, double b, double t) => + a + (b - a) * t; + + /// Clamps value between 0 and 1 and returns value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp01(double value) + { + if (value < 0.0) + return 0; + return value > 1 ? 1 : value; + } + + /// Calculates the linear parameter t that produces the interpolant value within the range [a, b]. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double InverseLerp(double a, double b, double value) => + a != b ? Clamp01((value - a) / (b - a)) : 0; + } +} diff --git a/Assets/Mirror/Runtime/Mathd.cs.meta b/Assets/Mirror/Runtime/Mathd.cs.meta new file mode 100644 index 0000000..927c55a --- /dev/null +++ b/Assets/Mirror/Runtime/Mathd.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f74084b91c74df2839b426c4a381373 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/MessagePacking.cs b/Assets/Mirror/Runtime/MessagePacking.cs new file mode 100644 index 0000000..af7fca6 --- /dev/null +++ b/Assets/Mirror/Runtime/MessagePacking.cs @@ -0,0 +1,148 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // message packing all in one place, instead of constructing headers in all + // kinds of different places + // + // MsgType (2 bytes) + // Content (ContentSize bytes) + public static class MessagePacking + { + // message header size + public const int HeaderSize = sizeof(ushort); + + // max message content size (without header) calculation for convenience + // -> Transport.GetMaxPacketSize is the raw maximum + // -> Every message gets serialized into <> + // -> Every serialized message get put into a batch with a header + public static int MaxContentSize + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Transport.activeTransport.GetMaxPacketSize() + - HeaderSize + - Batcher.HeaderSize; + } + + // paul: 16 bits is enough to avoid collisions + // - keeps the message size small + // - in case of collisions, Mirror will display an error + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort GetId() where T : struct, NetworkMessage => + (ushort)(typeof(T).FullName.GetStableHashCode() & 0xFFFF); + + // pack message before sending + // -> NetworkWriter passed as arg so that we can use .ToArraySegment + // and do an allocation free send before recycling it. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Pack(T message, NetworkWriter writer) + where T : struct, NetworkMessage + { + ushort msgType = GetId(); + writer.WriteUShort(msgType); + + // serialize message into writer + writer.Write(message); + } + + // unpack message after receiving + // -> pass NetworkReader so it's less strange if we create it in here + // and pass it upwards. + // -> NetworkReader will point at content afterwards! + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Unpack(NetworkReader messageReader, out ushort msgType) + { + // read message type + try + { + msgType = messageReader.ReadUShort(); + return true; + } + catch (System.IO.EndOfStreamException) + { + msgType = 0; + return false; + } + } + + // version for handlers with channelId + // inline! only exists for 20-30 messages and they call it all the time. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) + where T : struct, NetworkMessage + where C : NetworkConnection + => (conn, reader, channelId) => + { + // protect against DOS attacks if attackers try to send invalid + // data packets to crash the server/client. there are a thousand + // ways to cause an exception in data handling: + // - invalid headers + // - invalid message ids + // - invalid data causing exceptions + // - negative ReadBytesAndSize prefixes + // - invalid utf8 strings + // - etc. + // + // let's catch them all and then disconnect that connection to avoid + // further attacks. + T message = default; + // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching + int startPos = reader.Position; + try + { + if (requireAuthentication && !conn.isAuthenticated) + { + // message requires authentication, but the connection was not authenticated + Debug.LogWarning($"Closing connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet"); + conn.Disconnect(); + return; + } + + //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}"); + + // if it is a value type, just use default(T) + // otherwise allocate a new instance + message = reader.Read(); + } + catch (Exception exception) + { + Debug.LogError($"Closed connection: {conn}. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + return; + } + finally + { + int endPos = reader.Position; + // TODO: Figure out the correct channel + NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos); + } + + // user handler exception should not stop the whole server + try + { + // user implemented handler + handler((C)conn, message, channelId); + } + catch (Exception e) + { + Debug.LogError($"Disconnecting connId={conn.connectionId} to prevent exploits from an Exception in MessageHandler: {e.GetType().Name} {e.Message}\n{e.StackTrace}"); + conn.Disconnect(); + } + }; + + // version for handlers without channelId + // TODO obsolete this some day to always use the channelId version. + // all handlers in this version are wrapped with 1 extra action. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) + where T : struct, NetworkMessage + where C : NetworkConnection + { + // wrap action as channelId version, call original + void Wrapped(C conn, T msg, int _) => handler(conn, msg); + return WrapHandler((Action) Wrapped, requireAuthentication); + } + } +} diff --git a/Assets/Mirror/Runtime/MessagePacking.cs.meta b/Assets/Mirror/Runtime/MessagePacking.cs.meta new file mode 100644 index 0000000..910b75c --- /dev/null +++ b/Assets/Mirror/Runtime/MessagePacking.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2db134099f0df4d96a84ae7a0cd9b4bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Messages.cs b/Assets/Mirror/Runtime/Messages.cs new file mode 100644 index 0000000..d3816f8 --- /dev/null +++ b/Assets/Mirror/Runtime/Messages.cs @@ -0,0 +1,116 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + public struct ReadyMessage : NetworkMessage {} + + public struct NotReadyMessage : NetworkMessage {} + + public struct AddPlayerMessage : NetworkMessage {} + + public struct SceneMessage : NetworkMessage + { + public string sceneName; + // Normal = 0, LoadAdditive = 1, UnloadAdditive = 2 + public SceneOperation sceneOperation; + public bool customHandling; + } + + public enum SceneOperation : byte + { + Normal, + LoadAdditive, + UnloadAdditive + } + + public struct CommandMessage : NetworkMessage + { + public uint netId; + public byte componentIndex; + public int functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + public struct RpcMessage : NetworkMessage + { + public uint netId; + public byte componentIndex; + public int functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + public struct SpawnMessage : NetworkMessage + { + // netId of new or existing object + public uint netId; + public bool isLocalPlayer; + // Sets hasAuthority on the spawned object + public bool isOwner; + public ulong sceneId; + // If sceneId != 0 then it is used instead of assetId + public Guid assetId; + // Local position + public Vector3 position; + // Local rotation + public Quaternion rotation; + // Local scale + public Vector3 scale; + // serialized component data + // ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + public struct ChangeOwnerMessage : NetworkMessage + { + public uint netId; + public bool isOwner; + public bool isLocalPlayer; + } + + public struct ObjectSpawnStartedMessage : NetworkMessage {} + + public struct ObjectSpawnFinishedMessage : NetworkMessage {} + + public struct ObjectDestroyMessage : NetworkMessage + { + public uint netId; + } + + public struct ObjectHideMessage : NetworkMessage + { + public uint netId; + } + + public struct EntityStateMessage : NetworkMessage + { + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + // A client sends this message to the server + // to calculate RTT and synchronize time + public struct NetworkPingMessage : NetworkMessage + { + public double clientTime; + + public NetworkPingMessage(double value) + { + clientTime = value; + } + } + + // The server responds with this message + // The client can use this to calculate RTT and sync time + public struct NetworkPongMessage : NetworkMessage + { + public double clientTime; + public double serverTime; + } +} diff --git a/Assets/Mirror/Runtime/Messages.cs.meta b/Assets/Mirror/Runtime/Messages.cs.meta new file mode 100644 index 0000000..5d119e2 --- /dev/null +++ b/Assets/Mirror/Runtime/Messages.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 938f6f28a6c5b48a0bbd7782342d763b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Mirror.asmdef b/Assets/Mirror/Runtime/Mirror.asmdef new file mode 100644 index 0000000..0f38055 --- /dev/null +++ b/Assets/Mirror/Runtime/Mirror.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Mirror", + "references": [ + "Mirror.CompilerSymbols", + "Telepathy", + "kcp2k" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Mirror.asmdef.meta b/Assets/Mirror/Runtime/Mirror.asmdef.meta new file mode 100644 index 0000000..202009b --- /dev/null +++ b/Assets/Mirror/Runtime/Mirror.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 30817c1a0e6d646d99c048fc403f5979 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs b/Assets/Mirror/Runtime/NetworkAuthenticator.cs new file mode 100644 index 0000000..9f99b50 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkAuthenticator.cs @@ -0,0 +1,84 @@ +using System; +using UnityEngine; +using UnityEngine.Events; + +namespace Mirror +{ + [Serializable] public class UnityEventNetworkConnection : UnityEvent {} + + /// Base class for implementing component-based authentication during the Connect phase + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators")] + public abstract class NetworkAuthenticator : MonoBehaviour + { + /// Notify subscribers on the server when a client is authenticated + [Header("Event Listeners (optional)")] + [Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")] + public UnityEventNetworkConnection OnServerAuthenticated = new UnityEventNetworkConnection(); + + /// Notify subscribers on the client when the client is authenticated + [Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")] + public UnityEvent OnClientAuthenticated = new UnityEvent(); + + /// Called when server starts, used to register message handlers if needed. + public virtual void OnStartServer() {} + + /// Called when server stops, used to unregister message handlers if needed. + public virtual void OnStopServer() {} + + /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {} + + protected void ServerAccept(NetworkConnectionToClient conn) + { + OnServerAuthenticated.Invoke(conn); + } + + protected void ServerReject(NetworkConnectionToClient conn) + { + conn.Disconnect(); + } + + /// Called when client starts, used to register message handlers if needed. + public virtual void OnStartClient() {} + + /// Called when client stops, used to unregister message handlers if needed. + public virtual void OnStopClient() {} + + /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + public virtual void OnClientAuthenticate() {} + + protected void ClientAccept() + { + OnClientAuthenticated.Invoke(); + } + + protected void ClientReject() + { + // Set this on the client for local reference + NetworkClient.connection.isAuthenticated = false; + + // disconnect the client + NetworkClient.connection.Disconnect(); + } + + // Reset() instead of OnValidate(): + // Any NetworkAuthenticator assigns itself to the NetworkManager, this is fine on first adding it, + // but if someone intentionally sets Authenticator to null on the NetworkManager again then the + // Authenticator will reassign itself if a value in the inspector is changed. + // My change switches OnValidate to Reset since Reset is only called when the component is first + // added (or reset is pressed). + void Reset() + { +#if UNITY_EDITOR + // automatically assign authenticator field if we add this to NetworkManager + NetworkManager manager = GetComponent(); + if (manager != null && manager.authenticator == null) + { + // undo has to be called before the change happens + UnityEditor.Undo.RecordObject(manager, "Assigned NetworkManager authenticator"); + manager.authenticator = this; + } +#endif + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta new file mode 100644 index 0000000..d37db68 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 407fc95d4a8257f448799f26cdde0c2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Runtime/NetworkBehaviour.cs new file mode 100644 index 0000000..94cd930 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkBehaviour.cs @@ -0,0 +1,1094 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public enum SyncMode { Observers, Owner } + + /// Base class for networked components. + [AddComponentMenu("")] + [RequireComponent(typeof(NetworkIdentity))] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] + public abstract class NetworkBehaviour : MonoBehaviour + { + /// sync mode for OnSerialize + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. + [Tooltip("By default synced data is sent from the server to all Observers of the object.\nChange this to Owner to only have the server update the client that has ownership authority for this object")] + [HideInInspector] public SyncMode syncMode = SyncMode.Observers; + + /// sync interval for OnSerialize (in seconds) + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. + // [0,2] should be enough. anything >2s is too laggy anyway. + [Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")] + [Range(0, 2)] + [HideInInspector] public float syncInterval = 0.1f; + internal double lastSyncTime; + + /// True if this object is on the server and has been spawned. + // This is different from NetworkServer.active, which is true if the + // server itself is active rather than this object being active. + public bool isServer => netIdentity.isServer; + + /// True if this object is on the client and has been spawned by the server. + public bool isClient => netIdentity.isClient; + + /// True if this object is the the client's own local player. + public bool isLocalPlayer => netIdentity.isLocalPlayer; + + /// True if this object is on the server-only, not host. + public bool isServerOnly => netIdentity.isServerOnly; + + /// True if this object is on the client-only, not host. + public bool isClientOnly => netIdentity.isClientOnly; + + /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. + public bool hasAuthority => netIdentity.hasAuthority; + + /// The unique network Id of this object (unique at runtime). + public uint netId => netIdentity.netId; + + /// Client's network connection to the server. This is only valid for player objects on the client. + // TODO change to NetworkConnectionToServer, but might cause some breaking + public NetworkConnection connectionToServer => netIdentity.connectionToServer; + + /// Server's network connection to the client. This is only valid for player objects on the server. + public NetworkConnectionToClient connectionToClient => netIdentity.connectionToClient; + + // SyncLists, SyncSets, etc. + protected readonly List syncObjects = new List(); + + // NetworkBehaviourInspector needs to know if we have SyncObjects + internal bool HasSyncObjects() => syncObjects.Count > 0; + + // NetworkIdentity based values set from NetworkIdentity.Awake(), + // which is way more simple and way faster than trying to figure out + // component index from in here by searching all NetworkComponents. + + /// Returns the NetworkIdentity of this object + public NetworkIdentity netIdentity { get; internal set; } + + /// Returns the index of the component on this object + public int ComponentIndex { get; internal set; } + + // to avoid fully serializing entities every time, we have two options: + // * run a delta compression algorithm + // -> for fixed size types this is as easy as varint(b-a) for all + // -> for dynamically sized types like strings this is not easy. + // algorithms need to detect inserts/deletions, i.e. Myers Diff. + // those are very cpu intensive and barely fast enough for large + // scale multiplayer games (in Unity) + // * or we use dirty bits as meta data about which fields have changed + // -> spares us from running delta algorithms + // -> still supports dynamically sized types + // + // 64 bit mask, tracking up to 64 SyncVars. + protected ulong syncVarDirtyBits { get; private set; } + // 64 bit mask, tracking up to 64 sync collections (internal for tests). + // internal for tests, field for faster access (instead of property) + // TODO 64 SyncLists are too much. consider smaller mask later. + internal ulong syncObjectDirtyBits; + + // Weaver replaces '[SyncVar] int health' with 'Networkhealth' property. + // setter calls the hook if value changed. + // if we then modify the [SyncVar] from inside the setter, + // the setter would call the hook and we deadlock. + // hook guard prevents that. + ulong syncVarHookGuard; + + // USED BY WEAVER to set syncvars in host mode without deadlocking + protected bool GetSyncVarHookGuard(ulong dirtyBit) => + (syncVarHookGuard & dirtyBit) != 0UL; + + // Deprecated 2021-09-16 (old weavers used it) + [Obsolete("Renamed to GetSyncVarHookGuard (uppercase)")] + protected bool getSyncVarHookGuard(ulong dirtyBit) => GetSyncVarHookGuard(dirtyBit); + + // USED BY WEAVER to set syncvars in host mode without deadlocking + protected void SetSyncVarHookGuard(ulong dirtyBit, bool value) + { + // set the bit + if (value) + syncVarHookGuard |= dirtyBit; + // clear the bit + else + syncVarHookGuard &= ~dirtyBit; + } + + // Deprecated 2021-09-16 (old weavers used it) + [Obsolete("Renamed to SetSyncVarHookGuard (uppercase)")] + protected void setSyncVarHookGuard(ulong dirtyBit, bool value) => SetSyncVarHookGuard(dirtyBit, value); + + /// Set as dirty so that it's synced to clients again. + // these are masks, not bit numbers, ie. 110011b not '2' for 2nd bit. + public void SetSyncVarDirtyBit(ulong dirtyBit) + { + syncVarDirtyBits |= dirtyBit; + } + + // Deprecated 2021-09-19 + [Obsolete("SetDirtyBit was renamed to SetSyncVarDirtyBit because that's what it does")] + public void SetDirtyBit(ulong dirtyBit) => SetSyncVarDirtyBit(dirtyBit); + + // true if syncInterval elapsed and any SyncVar or SyncObject is dirty + public bool IsDirty() + { + if (NetworkTime.localTime - lastSyncTime >= syncInterval) + { + // OR both bitmasks. != 0 if either was dirty. + return (syncVarDirtyBits | syncObjectDirtyBits) != 0UL; + } + return false; + } + + /// Clears all the dirty bits that were set by SetDirtyBits() + // automatically invoked when an update is sent for this object, but can + // be called manually as well. + public void ClearAllDirtyBits() + { + lastSyncTime = NetworkTime.localTime; + syncVarDirtyBits = 0L; + syncObjectDirtyBits = 0L; + + // clear all unsynchronized changes in syncobjects + // (Linq allocates, use for instead) + for (int i = 0; i < syncObjects.Count; ++i) + { + syncObjects[i].ClearChanges(); + } + } + + // this gets called in the constructor by the weaver + // for every SyncObject in the component (e.g. SyncLists). + // We collect all of them and we synchronize them with OnSerialize/OnDeserialize + protected void InitSyncObject(SyncObject syncObject) + { + if (syncObject == null) + { + Debug.LogError("Uninitialized SyncObject. Manually call the constructor on your SyncList, SyncSet, SyncDictionary or SyncField"); + return; + } + + // add it, remember the index in list (if Count=0, index=0 etc.) + int index = syncObjects.Count; + syncObjects.Add(syncObject); + + // OnDirty needs to set nth bit in our dirty mask + ulong nthBit = 1UL << index; + syncObject.OnDirty = () => syncObjectDirtyBits |= nthBit; + + // only record changes while we have observers. + // prevents ever growing .changes lists: + // if a monster has no observers but we keep modifing a SyncObject, + // then the changes would never be flushed and keep growing, + // because OnSerialize isn't called without observers. + syncObject.IsRecording = () => netIdentity.observers?.Count > 0; + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendCommandInternal(string functionFullName, NetworkWriter writer, int channelId, bool requiresAuthority = true) + { + // this was in Weaver before + // NOTE: we could remove this later to allow calling Cmds on Server + // to avoid Wrapper functions. a lot of people requested this. + if (!NetworkClient.active) + { + Debug.LogError($"Command Function {functionFullName} called without an active client."); + return; + } + + // previously we used NetworkClient.readyConnection. + // now we check .ready separately. + if (!NetworkClient.ready) + { + // Unreliable Cmds from NetworkTransform may be generated, + // or client may have been set NotReady intentionally, so + // only warn if on the reliable channel. + if (channelId == Channels.Reliable) + Debug.LogWarning("Send command attempted while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + // local players can always send commands, regardless of authority, other objects must have authority. + if (!(!requiresAuthority || isLocalPlayer || hasAuthority)) + { + Debug.LogWarning($"Trying to send command for object without authority. {functionFullName}"); + return; + } + + // IMPORTANT: can't use .connectionToServer here because calling + // a command on other objects is allowed if requireAuthority is + // false. other objects don't have a .connectionToServer. + // => so we always need to use NetworkClient.connection instead. + // => see also: https://github.com/vis2k/Mirror/issues/2629 + if (NetworkClient.connection == null) + { + Debug.LogError("Send command attempted with no client running."); + return; + } + + // construct the message + CommandMessage message = new CommandMessage + { + netId = netId, + componentIndex = (byte)ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = functionFullName.GetStableHashCode(), + // segment to avoid reader allocations + payload = writer.ToArraySegment() + }; + + // IMPORTANT: can't use .connectionToServer here because calling + // a command on other objects is allowed if requireAuthority is + // false. other objects don't have a .connectionToServer. + // => so we always need to use NetworkClient.connection instead. + // => see also: https://github.com/vis2k/Mirror/issues/2629 + NetworkClient.connection.Send(message, channelId); + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendRPCInternal(string functionFullName, NetworkWriter writer, int channelId, bool includeOwner) + { + // this was in Weaver before + if (!NetworkServer.active) + { + Debug.LogError($"RPC Function {functionFullName} called on Client."); + return; + } + + // This cannot use NetworkServer.active, as that is not specific to this object. + if (!isServer) + { + Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}"); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = (byte)ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = functionFullName.GetStableHashCode(), + // segment to avoid reader allocations + payload = writer.ToArraySegment() + }; + + NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendTargetRPCInternal(NetworkConnection conn, string functionFullName, NetworkWriter writer, int channelId) + { + if (!NetworkServer.active) + { + Debug.LogError($"TargetRPC {functionFullName} called when server not active"); + return; + } + + if (!isServer) + { + Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned"); + return; + } + + // connection parameter is optional. assign if null. + if (conn is null) + { + conn = connectionToClient; + } + + // if still null + if (conn is null) + { + Debug.LogError($"TargetRPC {functionFullName} was given a null connection, make sure the object has an owner or you pass in the target connection"); + return; + } + + if (!(conn is NetworkConnectionToClient)) + { + Debug.LogError($"TargetRPC {functionFullName} requires a NetworkConnectionToClient but was given {conn.GetType().Name}"); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = (byte)ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = functionFullName.GetStableHashCode(), + // segment to avoid reader allocations + payload = writer.ToArraySegment() + }; + + conn.Send(message, channelId); + } + + // move the [SyncVar] generated property's .set into C# to avoid much IL + // + // public int health = 42; + // + // public int Networkhealth + // { + // get + // { + // return health; + // } + // [param: In] + // set + // { + // if (!NetworkBehaviour.SyncVarEqual(value, ref health)) + // { + // int oldValue = health; + // SetSyncVar(value, ref health, 1uL); + // if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) + // { + // SetSyncVarHookGuard(1uL, value: true); + // OnChanged(oldValue, value); + // SetSyncVarHookGuard(1uL, value: false); + // } + // } + // } + // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarSetter(T value, ref T field, ulong dirtyBit, Action OnChanged) + { + if (!SyncVarEqual(value, ref field)) + { + T oldValue = field; + SetSyncVar(value, ref field, dirtyBit); + + // call hook (if any) + if (OnChanged != null) + { + // in host mode, setting a SyncVar calls the hook directly. + // in client-only mode, OnDeserialize would call it. + // we use hook guard to protect against deadlock where hook + // changes syncvar, calling hook again. + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + { + SetSyncVarHookGuard(dirtyBit, true); + OnChanged(oldValue, value); + SetSyncVarHookGuard(dirtyBit, false); + } + } + } + } + + // GameObject needs custom handling for persistence via netId. + // has one extra parameter. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarSetter_GameObject(GameObject value, ref GameObject field, ulong dirtyBit, Action OnChanged, ref uint netIdField) + { + if (!SyncVarGameObjectEqual(value, netIdField)) + { + GameObject oldValue = field; + SetSyncVarGameObject(value, ref field, dirtyBit, ref netIdField); + + // call hook (if any) + if (OnChanged != null) + { + // in host mode, setting a SyncVar calls the hook directly. + // in client-only mode, OnDeserialize would call it. + // we use hook guard to protect against deadlock where hook + // changes syncvar, calling hook again. + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + { + SetSyncVarHookGuard(dirtyBit, true); + OnChanged(oldValue, value); + SetSyncVarHookGuard(dirtyBit, false); + } + } + } + } + + // NetworkIdentity needs custom handling for persistence via netId. + // has one extra parameter. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarSetter_NetworkIdentity(NetworkIdentity value, ref NetworkIdentity field, ulong dirtyBit, Action OnChanged, ref uint netIdField) + { + if (!SyncVarNetworkIdentityEqual(value, netIdField)) + { + NetworkIdentity oldValue = field; + SetSyncVarNetworkIdentity(value, ref field, dirtyBit, ref netIdField); + + // call hook (if any) + if (OnChanged != null) + { + // in host mode, setting a SyncVar calls the hook directly. + // in client-only mode, OnDeserialize would call it. + // we use hook guard to protect against deadlock where hook + // changes syncvar, calling hook again. + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + { + SetSyncVarHookGuard(dirtyBit, true); + OnChanged(oldValue, value); + SetSyncVarHookGuard(dirtyBit, false); + } + } + } + } + + // NetworkBehaviour needs custom handling for persistence via netId. + // has one extra parameter. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarSetter_NetworkBehaviour(T value, ref T field, ulong dirtyBit, Action OnChanged, ref NetworkBehaviourSyncVar netIdField) + where T : NetworkBehaviour + { + if (!SyncVarNetworkBehaviourEqual(value, netIdField)) + { + T oldValue = field; + SetSyncVarNetworkBehaviour(value, ref field, dirtyBit, ref netIdField); + + // call hook (if any) + if (OnChanged != null) + { + // in host mode, setting a SyncVar calls the hook directly. + // in client-only mode, OnDeserialize would call it. + // we use hook guard to protect against deadlock where hook + // changes syncvar, calling hook again. + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + { + SetSyncVarHookGuard(dirtyBit, true); + OnChanged(oldValue, value); + SetSyncVarHookGuard(dirtyBit, false); + } + } + } + } + + // helper function for [SyncVar] GameObjects. + // needs to be public so that tests & NetworkBehaviours from other + // assemblies both find it + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool SyncVarGameObjectEqual(GameObject newGameObject, uint netIdField) + { + uint newNetId = 0; + if (newGameObject != null) + { + NetworkIdentity identity = newGameObject.GetComponent(); + if (identity != null) + { + newNetId = identity.netId; + if (newNetId == 0) + { + Debug.LogWarning($"SetSyncVarGameObject GameObject {newGameObject} has a zero netId. Maybe it is not spawned yet?"); + } + } + } + + return newNetId == netIdField; + } + + // helper function for [SyncVar] GameObjects. + // dirtyBit is a mask like 00010 + protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gameObjectField, ulong dirtyBit, ref uint netIdField) + { + if (GetSyncVarHookGuard(dirtyBit)) + return; + + uint newNetId = 0; + if (newGameObject != null) + { + NetworkIdentity identity = newGameObject.GetComponent(); + if (identity != null) + { + newNetId = identity.netId; + if (newNetId == 0) + { + Debug.LogWarning($"SetSyncVarGameObject GameObject {newGameObject} has a zero netId. Maybe it is not spawned yet?"); + } + } + } + + //Debug.Log($"SetSyncVar GameObject {GetType().Name} bit:{dirtyBit} netfieldId:{netIdField} -> {newNetId}"); + SetSyncVarDirtyBit(dirtyBit); + // assign new one on the server, and in case we ever need it on client too + gameObjectField = newGameObject; + netIdField = newNetId; + } + + // helper function for [SyncVar] GameObjects. + // -> ref GameObject as second argument makes OnDeserialize processing easier + protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField) + { + // server always uses the field + if (isServer) + { + return gameObjectField; + } + + // client always looks up based on netId because objects might get in and out of range + // over and over again, which shouldn't null them forever + if (NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null) + return gameObjectField = identity.gameObject; + return null; + } + + // helper function for [SyncVar] NetworkIdentities. + // needs to be public so that tests & NetworkBehaviours from other + // assemblies both find it + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool SyncVarNetworkIdentityEqual(NetworkIdentity newIdentity, uint netIdField) + { + uint newNetId = 0; + if (newIdentity != null) + { + newNetId = newIdentity.netId; + if (newNetId == 0) + { + Debug.LogWarning($"SetSyncVarNetworkIdentity NetworkIdentity {newIdentity} has a zero netId. Maybe it is not spawned yet?"); + } + } + + // netId changed? + return newNetId == netIdField; + } + + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. + // + // before: + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // int num = health; + // Networkhealth = reader.ReadInt(); + // if (!NetworkBehaviour.SyncVarEqual(num, ref health)) + // { + // OnChanged(num, health); + // } + // return; + // } + // long num2 = (long)reader.ReadULong(); + // if ((num2 & 1L) != 0L) + // { + // int num3 = health; + // Networkhealth = reader.ReadInt(); + // if (!NetworkBehaviour.SyncVarEqual(num3, ref health)) + // { + // OnChanged(num3, health); + // } + // } + // } + // + // after: + // + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // GeneratedSyncVarDeserialize(reader, ref health, null, reader.ReadInt()); + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // GeneratedSyncVarDeserialize(reader, ref health, null, reader.ReadInt()); + // } + // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, T value) + { + T previous = field; + field = value; + + // any hook? then call if changed. + if (OnChanged != null && !SyncVarEqual(previous, ref field)) + { + OnChanged(previous, field); + } + } + + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. + // + // before: + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // uint __targetNetId = ___targetNetId; + // GameObject networktarget = Networktarget; + // ___targetNetId = reader.ReadUInt(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId, ref ___targetNetId)) + // { + // OnChangedNB(networktarget, Networktarget); + // } + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // uint __targetNetId2 = ___targetNetId; + // GameObject networktarget2 = Networktarget; + // ___targetNetId = reader.ReadUInt(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId2, ref ___targetNetId)) + // { + // OnChangedNB(networktarget2, Networktarget); + // } + // } + // } + // + // after: + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // GeneratedSyncVarDeserialize_GameObject(reader, ref target, OnChangedNB, ref ___targetNetId); + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // GeneratedSyncVarDeserialize_GameObject(reader, ref target, OnChangedNB, ref ___targetNetId); + // } + // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action OnChanged, NetworkReader reader, ref uint netIdField) + { + uint previousNetId = netIdField; + GameObject previousGameObject = field; + netIdField = reader.ReadUInt(); + + // get the new GameObject now that netId field is set + field = GetSyncVarGameObject(netIdField, ref field); + + // any hook? then call if changed. + if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField)) + { + OnChanged(previousGameObject, field); + } + } + + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. + // + // before: + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // uint __targetNetId = ___targetNetId; + // NetworkIdentity networktarget = Networktarget; + // ___targetNetId = reader.ReadUInt(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId, ref ___targetNetId)) + // { + // OnChangedNI(networktarget, Networktarget); + // } + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // uint __targetNetId2 = ___targetNetId; + // NetworkIdentity networktarget2 = Networktarget; + // ___targetNetId = reader.ReadUInt(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId2, ref ___targetNetId)) + // { + // OnChangedNI(networktarget2, Networktarget); + // } + // } + // } + // + // after: + // + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // GeneratedSyncVarDeserialize_NetworkIdentity(reader, ref target, OnChangedNI, ref ___targetNetId); + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // GeneratedSyncVarDeserialize_NetworkIdentity(reader, ref target, OnChangedNI, ref ___targetNetId); + // } + // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity field, Action OnChanged, NetworkReader reader, ref uint netIdField) + { + uint previousNetId = netIdField; + NetworkIdentity previousIdentity = field; + netIdField = reader.ReadUInt(); + + // get the new NetworkIdentity now that netId field is set + field = GetSyncVarNetworkIdentity(netIdField, ref field); + + // any hook? then call if changed. + if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField)) + { + OnChanged(previousIdentity, field); + } + } + + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. + // + // before: + // + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // NetworkBehaviourSyncVar __targetNetId = ___targetNetId; + // Tank networktarget = Networktarget; + // ___targetNetId = reader.ReadNetworkBehaviourSyncVar(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId, ref ___targetNetId)) + // { + // OnChangedNB(networktarget, Networktarget); + // } + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // NetworkBehaviourSyncVar __targetNetId2 = ___targetNetId; + // Tank networktarget2 = Networktarget; + // ___targetNetId = reader.ReadNetworkBehaviourSyncVar(); + // if (!NetworkBehaviour.SyncVarEqual(__targetNetId2, ref ___targetNetId)) + // { + // OnChangedNB(networktarget2, Networktarget); + // } + // } + // } + // + // after: + // + // public override void DeserializeSyncVars(NetworkReader reader, bool initialState) + // { + // base.DeserializeSyncVars(reader, initialState); + // if (initialState) + // { + // GeneratedSyncVarDeserialize_NetworkBehaviour(reader, ref target, OnChangedNB, ref ___targetNetId); + // return; + // } + // long num = (long)reader.ReadULong(); + // if ((num & 1L) != 0L) + // { + // GeneratedSyncVarDeserialize_NetworkBehaviour(reader, ref target, OnChangedNB, ref ___targetNetId); + // } + // } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GeneratedSyncVarDeserialize_NetworkBehaviour(ref T field, Action OnChanged, NetworkReader reader, ref NetworkBehaviourSyncVar netIdField) + where T : NetworkBehaviour + { + NetworkBehaviourSyncVar previousNetId = netIdField; + T previousBehaviour = field; + netIdField = reader.ReadNetworkBehaviourSyncVar(); + + // get the new NetworkBehaviour now that netId field is set + field = GetSyncVarNetworkBehaviour(netIdField, ref field); + + // any hook? then call if changed. + if (OnChanged != null && !SyncVarEqual(previousNetId, ref netIdField)) + { + OnChanged(previousBehaviour, field); + } + } + + // helper function for [SyncVar] NetworkIdentities. + // dirtyBit is a mask like 00010 + protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref NetworkIdentity identityField, ulong dirtyBit, ref uint netIdField) + { + if (GetSyncVarHookGuard(dirtyBit)) + return; + + uint newNetId = 0; + if (newIdentity != null) + { + newNetId = newIdentity.netId; + if (newNetId == 0) + { + Debug.LogWarning($"SetSyncVarNetworkIdentity NetworkIdentity {newIdentity} has a zero netId. Maybe it is not spawned yet?"); + } + } + + //Debug.Log($"SetSyncVarNetworkIdentity NetworkIdentity {GetType().Name} bit:{dirtyBit} netIdField:{netIdField} -> {newNetId}"); + SetSyncVarDirtyBit(dirtyBit); + netIdField = newNetId; + // assign new one on the server, and in case we ever need it on client too + identityField = newIdentity; + } + + // helper function for [SyncVar] NetworkIdentities. + // -> ref GameObject as second argument makes OnDeserialize processing easier + protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField) + { + // server always uses the field + if (isServer) + { + return identityField; + } + + // client always looks up based on netId because objects might get in and out of range + // over and over again, which shouldn't null them forever + NetworkClient.spawned.TryGetValue(netId, out identityField); + return identityField; + } + + protected static bool SyncVarNetworkBehaviourEqual(T newBehaviour, NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour + { + uint newNetId = 0; + int newComponentIndex = 0; + if (newBehaviour != null) + { + newNetId = newBehaviour.netId; + newComponentIndex = newBehaviour.ComponentIndex; + if (newNetId == 0) + { + Debug.LogWarning($"SetSyncVarNetworkIdentity NetworkIdentity {newBehaviour} has a zero netId. Maybe it is not spawned yet?"); + } + } + + // netId changed? + return syncField.Equals(newNetId, newComponentIndex); + } + + // helper function for [SyncVar] NetworkIdentities. + // dirtyBit is a mask like 00010 + protected void SetSyncVarNetworkBehaviour(T newBehaviour, ref T behaviourField, ulong dirtyBit, ref NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour + { + if (GetSyncVarHookGuard(dirtyBit)) + return; + + uint newNetId = 0; + int componentIndex = 0; + if (newBehaviour != null) + { + newNetId = newBehaviour.netId; + componentIndex = newBehaviour.ComponentIndex; + if (newNetId == 0) + { + Debug.LogWarning($"{nameof(SetSyncVarNetworkBehaviour)} NetworkIdentity {newBehaviour} has a zero netId. Maybe it is not spawned yet?"); + } + } + + syncField = new NetworkBehaviourSyncVar(newNetId, componentIndex); + + SetSyncVarDirtyBit(dirtyBit); + + // assign new one on the server, and in case we ever need it on client too + behaviourField = newBehaviour; + + // Debug.Log($"SetSyncVarNetworkBehaviour NetworkIdentity {GetType().Name} bit [{dirtyBit}] netIdField:{oldField}->{syncField}"); + } + + // helper function for [SyncVar] NetworkIdentities. + // -> ref GameObject as second argument makes OnDeserialize processing easier + protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour + { + // server always uses the field + if (isServer) + { + return behaviourField; + } + + // client always looks up based on netId because objects might get in and out of range + // over and over again, which shouldn't null them forever + if (!NetworkClient.spawned.TryGetValue(syncNetBehaviour.netId, out NetworkIdentity identity)) + { + return null; + } + + behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T; + return behaviourField; + } + + // backing field for sync NetworkBehaviour + public struct NetworkBehaviourSyncVar : IEquatable + { + public uint netId; + // limited to 255 behaviours per identity + public byte componentIndex; + + public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this() + { + this.netId = netId; + this.componentIndex = (byte)componentIndex; + } + + public bool Equals(NetworkBehaviourSyncVar other) + { + return other.netId == netId && other.componentIndex == componentIndex; + } + + public bool Equals(uint netId, int componentIndex) + { + return this.netId == netId && this.componentIndex == componentIndex; + } + + public override string ToString() + { + return $"[netId:{netId} compIndex:{componentIndex}]"; + } + } + + protected static bool SyncVarEqual(T value, ref T fieldValue) + { + // newly initialized or changed value? + // value.Equals(fieldValue) allocates without 'where T : IEquatable' + // seems like we use EqualityComparer to avoid allocations, + // because not all SyncVars are IEquatable + return EqualityComparer.Default.Equals(value, fieldValue); + } + + // dirtyBit is a mask like 00010 + protected void SetSyncVar(T value, ref T fieldValue, ulong dirtyBit) + { + //Debug.Log($"SetSyncVar {GetType().Name} bit:{dirtyBit} fieldValue:{value}"); + SetSyncVarDirtyBit(dirtyBit); + fieldValue = value; + } + + /// Override to do custom serialization (instead of SyncVars/SyncLists). Use OnDeserialize too. + // if a class has syncvars, then OnSerialize/OnDeserialize are added + // automatically. + // + // initialState is true for full spawns, false for delta syncs. + // note: SyncVar hooks are only called when inital=false + public virtual bool OnSerialize(NetworkWriter writer, bool initialState) + { + // if initialState: write all SyncVars. + // otherwise write dirtyBits+dirty SyncVars + bool objectWritten = initialState ? SerializeObjectsAll(writer) : SerializeObjectsDelta(writer); + bool syncVarWritten = SerializeSyncVars(writer, initialState); + return objectWritten || syncVarWritten; + } + + /// Override to do custom deserialization (instead of SyncVars/SyncLists). Use OnSerialize too. + public virtual void OnDeserialize(NetworkReader reader, bool initialState) + { + if (initialState) + { + DeSerializeObjectsAll(reader); + } + else + { + DeSerializeObjectsDelta(reader); + } + + DeserializeSyncVars(reader, initialState); + } + + // USED BY WEAVER + protected virtual bool SerializeSyncVars(NetworkWriter writer, bool initialState) + { + return false; + + // SyncVar are written here in subclass + + // if initialState + // write all SyncVars + // else + // write syncVarDirtyBits + // write dirty SyncVars + } + + // USED BY WEAVER + protected virtual void DeserializeSyncVars(NetworkReader reader, bool initialState) + { + // SyncVars are read here in subclass + + // if initialState + // read all SyncVars + // else + // read syncVarDirtyBits + // read dirty SyncVars + } + + public bool SerializeObjectsAll(NetworkWriter writer) + { + bool dirty = false; + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + syncObject.OnSerializeAll(writer); + dirty = true; + } + return dirty; + } + + public bool SerializeObjectsDelta(NetworkWriter writer) + { + bool dirty = false; + // write the mask + writer.WriteULong(syncObjectDirtyBits); + // serializable objects, such as synclists + for (int i = 0; i < syncObjects.Count; i++) + { + // check dirty mask at nth bit + SyncObject syncObject = syncObjects[i]; + if ((syncObjectDirtyBits & (1UL << i)) != 0) + { + syncObject.OnSerializeDelta(writer); + dirty = true; + } + } + return dirty; + } + + internal void DeSerializeObjectsAll(NetworkReader reader) + { + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + syncObject.OnDeserializeAll(reader); + } + } + + internal void DeSerializeObjectsDelta(NetworkReader reader) + { + ulong dirty = reader.ReadULong(); + for (int i = 0; i < syncObjects.Count; i++) + { + // check dirty mask at nth bit + SyncObject syncObject = syncObjects[i]; + if ((dirty & (1UL << i)) != 0) + { + syncObject.OnDeserializeDelta(reader); + } + } + } + + internal void ResetSyncObjects() + { + foreach (SyncObject syncObject in syncObjects) + { + syncObject.Reset(); + } + } + + /// Like Start(), but only called on server and host. + public virtual void OnStartServer() {} + + /// Stop event, only called on server and host. + public virtual void OnStopServer() {} + + /// Like Start(), but only called on client and host. + public virtual void OnStartClient() {} + + /// Stop event, only called on client and host. + public virtual void OnStopClient() {} + + /// Like Start(), but only called on client and host for the local player object. + public virtual void OnStartLocalPlayer() {} + + /// Stop event, but only called on client and host for the local player object. + public virtual void OnStopLocalPlayer() {} + + /// Like Start(), but only called for objects the client has authority over. + public virtual void OnStartAuthority() {} + + /// Stop event, only called for objects the client has authority over. + public virtual void OnStopAuthority() {} + } +} diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta b/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta new file mode 100644 index 0000000..f0bc195 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 655ee8cba98594f70880da5cc4dc442d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkClient.cs b/Assets/Mirror/Runtime/NetworkClient.cs new file mode 100644 index 0000000..e5dabe3 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkClient.cs @@ -0,0 +1,1544 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mirror.RemoteCalls; +using UnityEngine; + +namespace Mirror +{ + public enum ConnectState + { + None, + // connecting between Connect() and OnTransportConnected() + Connecting, + Connected, + // disconnecting between Disconnect() and OnTransportDisconnected() + Disconnecting, + Disconnected + } + + /// NetworkClient with connection to server. + public static class NetworkClient + { + // message handlers by messageId + internal static readonly Dictionary handlers = + new Dictionary(); + + /// All spawned NetworkIdentities by netId. + // client sees OBSERVED spawned ones. + public static readonly Dictionary spawned = + new Dictionary(); + + /// Client's NetworkConnection to server. + public static NetworkConnection connection { get; internal set; } + + /// True if client is ready (= joined world). + // TODO redundant state. point it to .connection.isReady instead (& test) + // TODO OR remove NetworkConnection.isReady? unless it's used on server + // + // TODO maybe ClientState.Connected/Ready/AddedPlayer/etc.? + // way better for security if we can check states in callbacks + public static bool ready; + + /// NetworkIdentity of the localPlayer + public static NetworkIdentity localPlayer { get; internal set; } + + // NetworkClient state + internal static ConnectState connectState = ConnectState.None; + + /// IP address of the connection to server. + // empty if the client has not connected yet. + public static string serverIp => connection.address; + + /// active is true while a client is connecting/connected + // (= while the network is active) + public static bool active => connectState == ConnectState.Connecting || + connectState == ConnectState.Connected; + + /// Check if client is connecting (before connected). + public static bool isConnecting => connectState == ConnectState.Connecting; + + /// Check if client is connected (after connecting). + public static bool isConnected => connectState == ConnectState.Connected; + + /// True if client is running in host mode. + public static bool isHostClient => connection is LocalConnectionToServer; + + // OnConnected / OnDisconnected used to be NetworkMessages that were + // invoked. this introduced a bug where external clients could send + // Connected/Disconnected messages over the network causing undefined + // behaviour. + // => public so that custom NetworkManagers can hook into it + public static Action OnConnectedEvent; + public static Action OnDisconnectedEvent; + public static Action OnErrorEvent; + + /// Registered spawnable prefabs by assetId. + public static readonly Dictionary prefabs = + new Dictionary(); + + // custom spawn / unspawn handlers. + // useful to support prefab pooling etc.: + // https://mirror-networking.gitbook.io/docs/guides/gameobjects/custom-spawnfunctions + internal static readonly Dictionary spawnHandlers = + new Dictionary(); + internal static readonly Dictionary unspawnHandlers = + new Dictionary(); + + // spawning + // internal for tests + internal static bool isSpawnFinished; + + // Disabled scene objects that can be spawned again, by sceneId. + internal static readonly Dictionary spawnableObjects = + new Dictionary(); + + static Unbatcher unbatcher = new Unbatcher(); + + // interest management component (optional) + // only needed for SetHostVisibility + public static InterestManagement aoi; + + // scene loading + public static bool isLoadingScene; + + // initialization ////////////////////////////////////////////////////// + static void AddTransportHandlers() + { + // += so that other systems can also hook into it (i.e. statistics) + Transport.activeTransport.OnClientConnected += OnTransportConnected; + Transport.activeTransport.OnClientDataReceived += OnTransportData; + Transport.activeTransport.OnClientDisconnected += OnTransportDisconnected; + Transport.activeTransport.OnClientError += OnError; + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) + Transport.activeTransport.OnClientConnected -= OnTransportConnected; + Transport.activeTransport.OnClientDataReceived -= OnTransportData; + Transport.activeTransport.OnClientDisconnected -= OnTransportDisconnected; + Transport.activeTransport.OnClientError -= OnError; + } + + internal static void RegisterSystemHandlers(bool hostMode) + { + // host mode client / remote client react to some messages differently. + // but we still need to add handlers for all of them to avoid + // 'message id not found' errors. + if (hostMode) + { + RegisterHandler(OnHostClientObjectDestroy); + RegisterHandler(OnHostClientObjectHide); + RegisterHandler(_ => {}, false); + RegisterHandler(OnHostClientSpawn); + // host mode doesn't need spawning + RegisterHandler(_ => {}); + // host mode doesn't need spawning + RegisterHandler(_ => {}); + // host mode doesn't need state updates + RegisterHandler(_ => {}); + } + else + { + RegisterHandler(OnObjectDestroy); + RegisterHandler(OnObjectHide); + RegisterHandler(NetworkTime.OnClientPong, false); + RegisterHandler(OnSpawn); + RegisterHandler(OnObjectSpawnStarted); + RegisterHandler(OnObjectSpawnFinished); + RegisterHandler(OnEntityStateMessage); + } + + // These handlers are the same for host and remote clients + RegisterHandler(OnChangeOwner); + RegisterHandler(OnRPCMessage); + } + + // connect ///////////////////////////////////////////////////////////// + /// Connect client to a NetworkServer by address. + public static void Connect(string address) + { + // Debug.Log($"Client Connect: {address}"); + Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + + RegisterSystemHandlers(false); + Transport.activeTransport.enabled = true; + AddTransportHandlers(); + + connectState = ConnectState.Connecting; + Transport.activeTransport.ClientConnect(address); + + connection = new NetworkConnectionToServer(); + } + + /// Connect client to a NetworkServer by Uri. + public static void Connect(Uri uri) + { + // Debug.Log($"Client Connect: {uri}"); + Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + + RegisterSystemHandlers(false); + Transport.activeTransport.enabled = true; + AddTransportHandlers(); + + connectState = ConnectState.Connecting; + Transport.activeTransport.ClientConnect(uri); + + connection = new NetworkConnectionToServer(); + } + + // TODO why are there two connect host methods? + // called from NetworkManager.FinishStartHost() + public static void ConnectHost() + { + //Debug.Log("Client Connect Host to Server"); + + RegisterSystemHandlers(true); + + connectState = ConnectState.Connected; + + // create local connection objects and connect them + LocalConnectionToServer connectionToServer = new LocalConnectionToServer(); + LocalConnectionToClient connectionToClient = new LocalConnectionToClient(); + connectionToServer.connectionToClient = connectionToClient; + connectionToClient.connectionToServer = connectionToServer; + + connection = connectionToServer; + + // create server connection to local client + NetworkServer.SetLocalConnection(connectionToClient); + } + + /// Connect host mode + // called from NetworkManager.StartHostClient + // TODO why are there two connect host methods? + public static void ConnectLocalServer() + { + // call server OnConnected with server's connection to client + NetworkServer.OnConnected(NetworkServer.localConnection); + + // call client OnConnected with client's connection to server + // => previously we used to send a ConnectMessage to + // NetworkServer.localConnection. this would queue the message + // until NetworkClient.Update processes it. + // => invoking the client's OnConnected event directly here makes + // tests fail. so let's do it exactly the same order as before by + // queueing the event for next Update! + //OnConnectedEvent?.Invoke(connection); + ((LocalConnectionToServer)connection).QueueConnectedEvent(); + } + + // disconnect ////////////////////////////////////////////////////////// + /// Disconnect from server. + public static void Disconnect() + { + // only if connected or connecting. + // don't disconnect() again if already in the process of + // disconnecting or fully disconnected. + if (connectState != ConnectState.Connecting && + connectState != ConnectState.Connected) + return; + + // we are disconnecting until OnTransportDisconnected is called. + // setting state to Disconnected would stop OnTransportDisconnected + // from calling cleanup code because it would think we are already + // disconnected fully. + // TODO move to 'cleanup' code below if safe + connectState = ConnectState.Disconnecting; + ready = false; + + // call Disconnect on the NetworkConnection + connection?.Disconnect(); + + // IMPORTANT: do NOT clear connection here yet. + // we still need it in OnTransportDisconnected for callbacks. + // connection = null; + } + + // transport events //////////////////////////////////////////////////// + // called by Transport + static void OnTransportConnected() + { + if (connection != null) + { + // reset network time stats + NetworkTime.ResetStatics(); + + // reset unbatcher in case any batches from last session remain. + unbatcher = new Unbatcher(); + + // the handler may want to send messages to the client + // thus we should set the connected state before calling the handler + connectState = ConnectState.Connected; + NetworkTime.UpdateClient(); + OnConnectedEvent?.Invoke(); + } + else Debug.LogError("Skipped Connect message handling because connection is null."); + } + + // helper function + static bool UnpackAndInvoke(NetworkReader reader, int channelId) + { + if (MessagePacking.Unpack(reader, out ushort msgType)) + { + // try to invoke the handler for that message + if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) + { + handler.Invoke(connection, reader, channelId); + + // message handler may disconnect client, making connection = null + // therefore must check for null to avoid NRE. + if (connection != null) + connection.lastMessageTime = Time.time; + + return true; + } + else + { + // message in a batch are NOT length prefixed to save bandwidth. + // every message needs to be handled and read until the end. + // otherwise it would overlap into the next message. + // => need to warn and disconnect to avoid undefined behaviour. + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"Unknown message id: {msgType}. This can happen if no handler was registered for this message."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + else + { + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning("Invalid message header."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + + // called by Transport + internal static void OnTransportData(ArraySegment data, int channelId) + { + if (connection != null) + { + // server might batch multiple messages into one packet. + // feed it to the Unbatcher. + // NOTE: we don't need to associate a channelId because we + // always process all messages in the batch. + if (!unbatcher.AddBatch(data)) + { + Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting."); + connection.Disconnect(); + return; + } + + // process all messages in the batch. + // only while NOT loading a scene. + // if we get a scene change message, then we need to stop + // processing. otherwise we might apply them to the old scene. + // => fixes https://github.com/vis2k/Mirror/issues/2651 + // + // NOTE: is scene starts loading, then the rest of the batch + // would only be processed when OnTransportData is called + // the next time. + // => consider moving processing to NetworkEarlyUpdate. + while (!isLoadingScene && + unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) + { + // enough to read at least header size? + if (reader.Remaining >= MessagePacking.HeaderSize) + { + // make remoteTimeStamp available to the user + connection.remoteTimeStamp = remoteTimestamp; + + // handle message + if (!UnpackAndInvoke(reader, channelId)) + { + // warn, disconnect and return if failed + // -> warning because attackers might send random data + // -> messages in a batch aren't length prefixed. + // failing to read one would cause undefined + // behaviour for every message afterwards. + // so we need to disconnect. + // -> return to avoid the below unbatches.count error. + // we already disconnected and handled it. + Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting."); + connection.Disconnect(); + return; + } + } + // otherwise disconnect + else + { + // WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)"); + connection.Disconnect(); + return; + } + } + + // if we weren't interrupted by a scene change, + // then all batched messages should have been processed now. + // if not, we need to log an error to avoid debugging hell. + // otherwise batches would silently grow. + // we need to log an error to avoid debugging hell. + // + // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882 + // -> UnpackAndInvoke silently returned because no handler for id + // -> Reader would never be read past the end + // -> Batch would never be retired because end is never reached + // + // NOTE: prefixing every message in a batch with a length would + // avoid ever not reading to the end. for extra bandwidth. + // + // IMPORTANT: always keep this check to detect memory leaks. + // this took half a day to debug last time. + if (!isLoadingScene && unbatcher.BatchesCount > 0) + { + Debug.LogError($"Still had {unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end."); + } + } + else Debug.LogError("Skipped Data message handling because connection is null."); + } + + // called by Transport + // IMPORTANT: often times when disconnecting, we call this from Mirror + // too because we want to remove the connection and handle + // the disconnect immediately. + // => which is fine as long as we guarantee it only runs once + // => which we do by setting the state to Disconnected! + internal static void OnTransportDisconnected() + { + // StopClient called from user code triggers Disconnected event + // from transport which calls StopClient again, so check here + // and short circuit running the Shutdown process twice. + if (connectState == ConnectState.Disconnected) return; + + // Raise the event before changing ConnectState + // because 'active' depends on this during shutdown + if (connection != null) OnDisconnectedEvent?.Invoke(); + + connectState = ConnectState.Disconnected; + ready = false; + + // now that everything was handled, clear the connection. + // previously this was done in Disconnect() already, but we still + // need it for the above OnDisconnectedEvent. + connection = null; + + // transport handlers are only added when connecting. + // so only remove when actually disconnecting. + RemoveTransportHandlers(); + } + + static void OnError(Exception exception) + { + Debug.LogException(exception); + OnErrorEvent?.Invoke(exception); + } + + // send //////////////////////////////////////////////////////////////// + /// Send a NetworkMessage to the server over the given channel. + public static void Send(T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + if (connection != null) + { + if (connectState == ConnectState.Connected) + { + connection.Send(message, channelId); + } + else Debug.LogError("NetworkClient Send when not connected to a server"); + } + else Debug.LogError("NetworkClient Send with no connection"); + } + + // message handlers //////////////////////////////////////////////////// + /// Register a handler for a message type T. Most should require authentication. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + void HandlerWrapped(NetworkConnection _, T value) => handler(value); + handlers[msgType] = MessagePacking.WrapHandler((Action) HandlerWrapped, requireAuthentication); + } + + /// Replace a handler for a particular message type. Should require authentication by default. + // RegisterHandler throws a warning (as it should) if a handler is assigned twice + // Use of ReplaceHandler makes it clear the user intended to replace the handler + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + } + + /// Replace a handler for a particular message type. Should require authentication by default. + // RegisterHandler throws a warning (as it should) if a handler is assigned twice + // Use of ReplaceHandler makes it clear the user intended to replace the handler + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication); + } + + /// Unregister a message handler of type T. + public static bool UnregisterHandler() + where T : struct, NetworkMessage + { + // use int to minimize collisions + ushort msgType = MessagePacking.GetId(); + return handlers.Remove(msgType); + } + + // spawnable prefabs /////////////////////////////////////////////////// + /// Find the registered prefab for this asset id. + // Useful for debuggers + public static bool GetPrefab(Guid assetId, out GameObject prefab) + { + prefab = null; + return assetId != Guid.Empty && + prefabs.TryGetValue(assetId, out prefab) && prefab != null; + } + + /// Validates Prefab then adds it to prefabs dictionary. + static void RegisterPrefabIdentity(NetworkIdentity prefab) + { + if (prefab.assetId == Guid.Empty) + { + Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); + return; + } + + if (prefab.sceneId != 0) + { + Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); + return; + } + + NetworkIdentity[] identities = prefab.GetComponentsInChildren(); + if (identities.Length > 1) + { + Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); + } + + if (prefabs.ContainsKey(prefab.assetId)) + { + GameObject existingPrefab = prefabs[prefab.assetId]; + Debug.LogWarning($"Replacing existing prefab with assetId '{prefab.assetId}'. Old prefab '{existingPrefab.name}', New prefab '{prefab.name}'"); + } + + if (spawnHandlers.ContainsKey(prefab.assetId) || unspawnHandlers.ContainsKey(prefab.assetId)) + { + Debug.LogWarning($"Adding prefab '{prefab.name}' with assetId '{prefab.assetId}' when spawnHandlers with same assetId already exists. If you want to use custom spawn handling, then remove the prefab from NetworkManager's registered prefabs first."); + } + + // Debug.Log($"Registering prefab '{prefab.name}' as asset:{prefab.assetId}"); + + prefabs[prefab.assetId] = prefab.gameObject; + } + + /// Register spawnable prefab with custom assetId. + // Note: newAssetId can not be set on GameObjects that already have an assetId + // Note: registering with assetId is useful for assetbundles etc. a lot + // of people use this. + public static void RegisterPrefab(GameObject prefab, Guid newAssetId) + { + if (prefab == null) + { + Debug.LogError("Could not register prefab because it was null"); + return; + } + + if (newAssetId == Guid.Empty) + { + Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + { + Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); + return; + } + + identity.assetId = newAssetId; + + RegisterPrefabIdentity(identity); + } + + /// Register spawnable prefab. + public static void RegisterPrefab(GameObject prefab) + { + if (prefab == null) + { + Debug.LogError("Could not register prefab because it was null"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + RegisterPrefabIdentity(identity); + } + + /// Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers. + // Note: newAssetId can not be set on GameObjects that already have an assetId + // Note: registering with assetId is useful for assetbundles etc. a lot + // of people use this. + // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? + public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + // We need this check here because we don't want a null handler in the lambda expression below + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {newAssetId}"); + return; + } + + RegisterPrefab(prefab, newAssetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); + } + + /// Register a spawnable prefab with custom spawn/unspawn handlers. + // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? + public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (prefab == null) + { + Debug.LogError("Could not register handler for prefab because the prefab was null"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.sceneId != 0) + { + Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); + return; + } + + Guid assetId = identity.assetId; + + if (assetId == Guid.Empty) + { + Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); + return; + } + + // We need this check here because we don't want a null handler in the lambda expression below + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + return; + } + + RegisterPrefab(prefab, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); + } + + /// Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers. + // Note: newAssetId can not be set on GameObjects that already have an assetId + // Note: registering with assetId is useful for assetbundles etc. a lot + // of people use this. + // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? + public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (newAssetId == Guid.Empty) + { + Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty"); + return; + } + + if (prefab == null) + { + Debug.LogError("Could not register handler for prefab because the prefab was null"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + { + Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); + return; + } + + if (identity.sceneId != 0) + { + Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); + return; + } + + identity.assetId = newAssetId; + Guid assetId = identity.assetId; + + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + return; + } + + if (unspawnHandler == null) + { + Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); + return; + } + + if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) + { + Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'"); + } + + if (prefabs.ContainsKey(assetId)) + { + // this is error because SpawnPrefab checks prefabs before handler + Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler"); + } + + NetworkIdentity[] identities = prefab.GetComponentsInChildren(); + if (identities.Length > 1) + { + Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); + } + + //Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}"); + + spawnHandlers[assetId] = spawnHandler; + unspawnHandlers[assetId] = unspawnHandler; + } + + /// Register a spawnable prefab with custom spawn/unspawn handlers. + // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? + public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (prefab == null) + { + Debug.LogError("Could not register handler for prefab because the prefab was null"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.sceneId != 0) + { + Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); + return; + } + + Guid assetId = identity.assetId; + + if (assetId == Guid.Empty) + { + Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); + return; + } + + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + return; + } + + if (unspawnHandler == null) + { + Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); + return; + } + + if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) + { + Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'"); + } + + if (prefabs.ContainsKey(assetId)) + { + // this is error because SpawnPrefab checks prefabs before handler + Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler"); + } + + NetworkIdentity[] identities = prefab.GetComponentsInChildren(); + if (identities.Length > 1) + { + Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); + } + + //Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}"); + + spawnHandlers[assetId] = spawnHandler; + unspawnHandlers[assetId] = unspawnHandler; + } + + /// Removes a registered spawn prefab that was setup with NetworkClient.RegisterPrefab. + public static void UnregisterPrefab(GameObject prefab) + { + if (prefab == null) + { + Debug.LogError("Could not unregister prefab because it was null"); + return; + } + + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + Guid assetId = identity.assetId; + + prefabs.Remove(assetId); + spawnHandlers.Remove(assetId); + unspawnHandlers.Remove(assetId); + } + + // spawn handlers ////////////////////////////////////////////////////// + /// This is an advanced spawning function that registers a custom assetId with the spawning system. + // This can be used to register custom spawning methods for an assetId - + // instead of the usual method of registering spawning methods for a + // prefab. This should be used when no prefab exists for the spawned + // objects - such as when they are constructed dynamically at runtime + // from configuration data. + public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + // We need this check here because we don't want a null handler in the lambda expression below + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + return; + } + + RegisterSpawnHandler(assetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); + } + + /// This is an advanced spawning function that registers a custom assetId with the spawning system. + // This can be used to register custom spawning methods for an assetId - + // instead of the usual method of registering spawning methods for a + // prefab. This should be used when no prefab exists for the spawned + // objects - such as when they are constructed dynamically at runtime + // from configuration data. + public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (spawnHandler == null) + { + Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + return; + } + + if (unspawnHandler == null) + { + Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); + return; + } + + if (assetId == Guid.Empty) + { + Debug.LogError("Can not Register SpawnHandler for empty Guid"); + return; + } + + if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) + { + Debug.LogWarning($"Replacing existing spawnHandlers for {assetId}"); + } + + if (prefabs.ContainsKey(assetId)) + { + // this is error because SpawnPrefab checks prefabs before handler + Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}'"); + } + + // Debug.Log("RegisterSpawnHandler asset {assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}"); + + spawnHandlers[assetId] = spawnHandler; + unspawnHandlers[assetId] = unspawnHandler; + } + + /// Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler(). + public static void UnregisterSpawnHandler(Guid assetId) + { + spawnHandlers.Remove(assetId); + unspawnHandlers.Remove(assetId); + } + + /// This clears the registered spawn prefabs and spawn handler functions for this client. + public static void ClearSpawners() + { + prefabs.Clear(); + spawnHandlers.Clear(); + unspawnHandlers.Clear(); + } + + internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj) + { + if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null) + { + handler(obj); + return true; + } + return false; + } + + // ready /////////////////////////////////////////////////////////////// + /// Sends Ready message to server, indicating that we loaded the scene, ready to enter the game. + // This could be for example when a client enters an ongoing game and + // has finished loading the current scene. The server should respond to + // the SYSTEM_READY event with an appropriate handler which instantiates + // the players object for example. + public static bool Ready() + { + // Debug.Log($"NetworkClient.Ready() called with connection {conn}"); + if (ready) + { + Debug.LogError("NetworkClient is already ready. It shouldn't be called twice."); + return false; + } + + // need a valid connection to become ready + if (connection == null) + { + Debug.LogError("Ready() called with invalid connection object: conn=null"); + return false; + } + + // Set these before sending the ReadyMessage, otherwise host client + // will fail in InternalAddPlayer with null readyConnection. + // TODO this is redundant. have one source of truth for .ready + ready = true; + connection.isReady = true; + + // Tell server we're ready to have a player object spawned + connection.Send(new ReadyMessage()); + return true; + } + + // add player ////////////////////////////////////////////////////////// + // called from message handler for Owner message + internal static void InternalAddPlayer(NetworkIdentity identity) + { + //Debug.Log("NetworkClient.InternalAddPlayer"); + + // NOTE: It can be "normal" when changing scenes for the player to be destroyed and recreated. + // But, the player structures are not cleaned up, we'll just replace the old player + localPlayer = identity; + + // NOTE: we DONT need to set isClient=true here, because OnStartClient + // is called before OnStartLocalPlayer, hence it's already set. + // localPlayer.isClient = true; + + // TODO this check might not be necessary + //if (readyConnection != null) + if (ready && connection != null) + { + connection.identity = identity; + } + else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer"); + } + + /// Sends AddPlayer message to the server, indicating that we want to join the world. + public static bool AddPlayer() + { + // ensure valid ready connection + if (connection == null) + { + Debug.LogError("AddPlayer requires a valid NetworkClient.connection."); + return false; + } + + // UNET checked 'if readyConnection != null'. + // in other words, we need a connection and we need to be ready. + if (!ready) + { + Debug.LogError("AddPlayer requires a ready NetworkClient."); + return false; + } + + if (connection.identity != null) + { + Debug.LogError("NetworkClient.AddPlayer: a PlayerController was already added. Did you call AddPlayer twice?"); + return false; + } + + // Debug.Log($"NetworkClient.AddPlayer() called with connection {readyConnection}"); + connection.Send(new AddPlayerMessage()); + return true; + } + + // spawning //////////////////////////////////////////////////////////// + internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message) + { + if (message.assetId != Guid.Empty) + identity.assetId = message.assetId; + + if (!identity.gameObject.activeSelf) + { + identity.gameObject.SetActive(true); + } + + // apply local values for VR support + identity.transform.localPosition = message.position; + identity.transform.localRotation = message.rotation; + identity.transform.localScale = message.scale; + identity.hasAuthority = message.isOwner; + identity.netId = message.netId; + + if (message.isLocalPlayer) + InternalAddPlayer(identity); + + // deserialize components if any payload + // (Count is 0 if there were no components) + if (message.payload.Count > 0) + { + using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload)) + { + identity.OnDeserializeAllSafely(payloadReader, true); + } + } + + spawned[message.netId] = identity; + + // the initial spawn with OnObjectSpawnStarted/Finished calls all + // object's OnStartClient/OnStartLocalPlayer after they were all + // spawned. + // this only happens once though. + // for all future spawns, we need to call OnStartClient/LocalPlayer + // here immediately since there won't be another OnObjectSpawnFinished. + if (isSpawnFinished) + { + identity.NotifyAuthority(); + identity.OnStartClient(); + CheckForLocalPlayer(identity); + } + } + + // Finds Existing Object with NetId or spawns a new one using AssetId or sceneId + internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity) + { + // was the object already spawned? + identity = GetExistingObject(message.netId); + + // if found, return early + if (identity != null) + { + return true; + } + + if (message.assetId == Guid.Empty && message.sceneId == 0) + { + Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId"); + return false; + } + + identity = message.sceneId == 0 ? SpawnPrefab(message) : SpawnSceneObject(message.sceneId); + + if (identity == null) + { + Debug.LogError($"Could not spawn assetId={message.assetId} scene={message.sceneId:X} netId={message.netId}"); + return false; + } + + return true; + } + + static NetworkIdentity GetExistingObject(uint netid) + { + spawned.TryGetValue(netid, out NetworkIdentity localObject); + return localObject; + } + + static NetworkIdentity SpawnPrefab(SpawnMessage message) + { + // custom spawn handler for this prefab? (for prefab pools etc.) + // + // IMPORTANT: look for spawn handlers BEFORE looking for registered + // prefabs. Unspawning also looks for unspawn handlers + // before falling back to regular Destroy. this needs to + // be consistent. + // https://github.com/vis2k/Mirror/issues/2705 + if (spawnHandlers.TryGetValue(message.assetId, out SpawnHandlerDelegate handler)) + { + GameObject obj = handler(message); + if (obj == null) + { + Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'"); + return null; + } + NetworkIdentity identity = obj.GetComponent(); + if (identity == null) + { + Debug.LogError($"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{message.assetId}'"); + return null; + } + return identity; + } + + // otherwise look in NetworkManager registered prefabs + if (GetPrefab(message.assetId, out GameObject prefab)) + { + GameObject obj = GameObject.Instantiate(prefab, message.position, message.rotation); + //Debug.Log($"Client spawn handler instantiating [netId{message.netId} asset ID:{message.assetId} pos:{message.position} rotation:{message.rotation}]"); + return obj.GetComponent(); + } + + Debug.LogError($"Failed to spawn server object, did you forget to add it to the NetworkManager? assetId={message.assetId} netId={message.netId}"); + return null; + } + + static NetworkIdentity SpawnSceneObject(ulong sceneId) + { + NetworkIdentity identity = GetAndRemoveSceneObject(sceneId); + if (identity == null) + { + Debug.LogError($"Spawn scene object not found for {sceneId:X}. Make sure that client and server use exactly the same project. This only happens if the hierarchy gets out of sync."); + + // dump the whole spawnable objects dict for easier debugging + //foreach (KeyValuePair kvp in spawnableObjects) + // Debug.Log($"Spawnable: SceneId={kvp.Key:X} name={kvp.Value.name}"); + } + //else Debug.Log($"Client spawn for [netId:{msg.netId}] [sceneId:{msg.sceneId:X}] obj:{identity}"); + return identity; + } + + static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId) + { + if (spawnableObjects.TryGetValue(sceneId, out NetworkIdentity identity)) + { + spawnableObjects.Remove(sceneId); + return identity; + } + return null; + } + + // Checks if identity is not spawned yet, not hidden and has sceneId + static bool ConsiderForSpawning(NetworkIdentity identity) + { + // not spawned yet, not hidden, etc.? + return !identity.gameObject.activeSelf && + identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.sceneId != 0; + } + + /// Call this after loading/unloading a scene in the client after connection to register the spawnable objects + public static void PrepareToSpawnSceneObjects() + { + // remove existing items, they will be re-added below + spawnableObjects.Clear(); + + // finds all NetworkIdentity currently loaded by unity (includes disabled objects) + NetworkIdentity[] allIdentities = Resources.FindObjectsOfTypeAll(); + foreach (NetworkIdentity identity in allIdentities) + { + // add all unspawned NetworkIdentities to spawnable objects + if (ConsiderForSpawning(identity)) + { + spawnableObjects.Add(identity.sceneId, identity); + } + } + } + + internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _) + { + // Debug.Log("SpawnStarted"); + PrepareToSpawnSceneObjects(); + isSpawnFinished = false; + } + + internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _) + { + //Debug.Log("SpawnFinished"); + ClearNullFromSpawned(); + + // paul: Initialize the objects in the same order as they were + // initialized in the server. This is important if spawned objects + // use data from scene objects + foreach (NetworkIdentity identity in spawned.Values.OrderBy(uv => uv.netId)) + { + identity.NotifyAuthority(); + identity.OnStartClient(); + CheckForLocalPlayer(identity); + } + isSpawnFinished = true; + } + + static readonly List removeFromSpawned = new List(); + static void ClearNullFromSpawned() + { + // spawned has null objects after changing scenes on client using + // NetworkManager.ServerChangeScene remove them here so that 2nd + // loop below does not get NullReferenceException + // see https://github.com/vis2k/Mirror/pull/2240 + // TODO fix scene logic so that client scene doesn't have null objects + foreach (KeyValuePair kvp in spawned) + { + if (kvp.Value == null) + { + removeFromSpawned.Add(kvp.Key); + } + } + + // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove + foreach (uint id in removeFromSpawned) + { + spawned.Remove(id); + } + removeFromSpawned.Clear(); + } + + // host mode callbacks ///////////////////////////////////////////////// + static void OnHostClientObjectDestroy(ObjectDestroyMessage message) + { + //Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}"); + spawned.Remove(message.netId); + } + + static void OnHostClientObjectHide(ObjectHideMessage message) + { + //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && + localObject != null) + { + if (aoi != null) + aoi.SetHostVisibility(localObject, false); + } + } + + internal static void OnHostClientSpawn(SpawnMessage message) + { + // on host mode, the object already exist in NetworkServer.spawned. + // simply add it to NetworkClient.spawned too. + if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + { + spawned[message.netId] = localObject; + + // now do the actual 'spawning' on host mode + if (message.isLocalPlayer) + InternalAddPlayer(localObject); + + localObject.hasAuthority = message.isOwner; + localObject.NotifyAuthority(); + localObject.OnStartClient(); + + if (aoi != null) + aoi.SetHostVisibility(localObject, true); + + CheckForLocalPlayer(localObject); + } + } + + // client-only mode callbacks ////////////////////////////////////////// + static void OnEntityStateMessage(EntityStateMessage message) + { + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + { + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) + localObject.OnDeserializeAllSafely(networkReader, false); + } + else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + static void OnRPCMessage(RpcMessage message) + { + // Debug.Log($"NetworkClient.OnRPCMessage hash:{msg.functionHash} netId:{msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity)) + { + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) + identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, networkReader); + } + } + + static void OnObjectHide(ObjectHideMessage message) => DestroyObject(message.netId); + + internal static void OnObjectDestroy(ObjectDestroyMessage message) => DestroyObject(message.netId); + + internal static void OnSpawn(SpawnMessage message) + { + // Debug.Log($"Client spawn handler instantiating netId={msg.netId} assetID={msg.assetId} sceneId={msg.sceneId:X} pos={msg.position}"); + if (FindOrSpawnObject(message, out NetworkIdentity identity)) + { + ApplySpawnPayload(identity, message); + } + } + + internal static void OnChangeOwner(ChangeOwnerMessage message) + { + NetworkIdentity identity = GetExistingObject(message.netId); + + if (identity != null) + ChangeOwner(identity, message); + else + Debug.LogError($"OnChangeOwner: Could not find object with netId {message.netId}"); + } + + // ChangeOwnerMessage contains new 'owned' and new 'localPlayer' + // that we need to apply to the identity. + internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage message) + { + // local player before, but not anymore? + // call OnStopLocalPlayer before setting new values. + if (identity.isLocalPlayer && !message.isLocalPlayer) + { + identity.OnStopLocalPlayer(); + } + + // set ownership flag (aka authority) + identity.hasAuthority = message.isOwner; + identity.NotifyAuthority(); + + // set localPlayer flag + identity.isLocalPlayer = message.isLocalPlayer; + + // identity is now local player. set our static helper field to it. + if (identity.isLocalPlayer) + { + localPlayer = identity; + } + // identity's isLocalPlayer was set to false. + // clear our static localPlayer IF (and only IF) it was that one before. + else if (localPlayer == identity) + { + localPlayer = null; + } + + // call OnStartLocalPlayer if it's the local player now. + CheckForLocalPlayer(identity); + } + + internal static void CheckForLocalPlayer(NetworkIdentity identity) + { + if (identity == localPlayer) + { + // Set isLocalPlayer to true on this NetworkIdentity and trigger + // OnStartLocalPlayer in all scripts on the same GO + identity.connectionToServer = connection; + identity.OnStartLocalPlayer(); + // Debug.Log($"NetworkClient.OnOwnerMessage player:{identity.name}"); + } + } + + // destroy ///////////////////////////////////////////////////////////// + static void DestroyObject(uint netId) + { + // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); + if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) + { + if (localObject.isLocalPlayer) + localObject.OnStopLocalPlayer(); + + localObject.OnStopClient(); + + // custom unspawn handler for this prefab? (for prefab pools etc.) + if (InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject)) + { + // reset object after user's handler + localObject.Reset(); + } + // otherwise fall back to default Destroy + else if (localObject.sceneId == 0) + { + // don't call reset before destroy so that values are still set in OnDestroy + GameObject.Destroy(localObject.gameObject); + } + // scene object.. disable it in scene instead of destroying + else + { + localObject.gameObject.SetActive(false); + spawnableObjects[localObject.sceneId] = localObject; + // reset for scene objects + localObject.Reset(); + } + + // remove from dictionary no matter how it is unspawned + spawned.Remove(netId); + } + //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); + } + + // update ////////////////////////////////////////////////////////////// + // NetworkEarlyUpdate called before any Update/FixedUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkEarlyUpdate() + { + // process all incoming messages first before updating the world + if (Transport.activeTransport != null) + Transport.activeTransport.ClientEarlyUpdate(); + } + + // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkLateUpdate() + { + // local connection? + if (connection is LocalConnectionToServer localConnection) + { + localConnection.Update(); + } + // remote connection? + else if (connection is NetworkConnectionToServer remoteConnection) + { + // only update things while connected + if (active && connectState == ConnectState.Connected) + { + // update NetworkTime + NetworkTime.UpdateClient(); + + // update connection to flush out batched messages + remoteConnection.Update(); + } + } + + // process all outgoing messages after updating the world + if (Transport.activeTransport != null) + Transport.activeTransport.ClientLateUpdate(); + } + + // shutdown //////////////////////////////////////////////////////////// + /// Destroys all networked objects on the client. + // Note: NetworkServer.CleanupNetworkIdentities does the same on server. + public static void DestroyAllClientObjects() + { + // user can modify spawned lists which causes InvalidOperationException + // list can modified either in UnSpawnHandler or in OnDisable/OnDestroy + // we need the Try/Catch so that the rest of the shutdown does not get stopped + try + { + foreach (NetworkIdentity identity in spawned.Values) + { + if (identity != null && identity.gameObject != null) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + + // NetworkClient.Shutdown calls DestroyAllClientObjects. + // which destroys all objects in NetworkClient.spawned. + // => NC.spawned contains owned & observed objects + // => in host mode, we CAN NOT destroy observed objects. + // => that would destroy them other connection's objects + // on the host server, making them disconnect. + // https://github.com/vis2k/Mirror/issues/2954 + bool hostOwned = identity.connectionToServer is LocalConnectionToServer; + bool shouldDestroy = !identity.isServer || hostOwned; + if (shouldDestroy) + { + bool wasUnspawned = InvokeUnSpawnHandler(identity.assetId, identity.gameObject); + + // unspawned objects should be reset for reuse later. + if (wasUnspawned) + { + identity.Reset(); + } + // without unspawn handler, we need to disable/destroy. + else + { + // scene objects are reset and disabled. + // they always stay in the scene, we don't destroy them. + if (identity.sceneId != 0) + { + identity.Reset(); + identity.gameObject.SetActive(false); + } + // spawned objects are destroyed + else + { + GameObject.Destroy(identity.gameObject); + } + } + } + } + } + spawned.Clear(); + } + catch (InvalidOperationException e) + { + Debug.LogException(e); + Debug.LogError("Could not DestroyAllClientObjects because spawned list was modified during loop, make sure you are not modifying NetworkIdentity.spawned by calling NetworkServer.Destroy or NetworkServer.Spawn in OnDestroy or OnDisable."); + } + } + + /// Shutdown the client. + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Shutdown() + { + //Debug.Log("Shutting down client."); + + // calls prefabs.Clear(); + // calls spawnHandlers.Clear(); + // calls unspawnHandlers.Clear(); + ClearSpawners(); + + // calls spawned.Clear() if no exception occurs + DestroyAllClientObjects(); + + spawned.Clear(); + handlers.Clear(); + spawnableObjects.Clear(); + + // IMPORTANT: do NOT call NetworkIdentity.ResetStatics() here! + // calling StopClient() in host mode would reset nextNetId to 1, + // causing next connection to have a duplicate netId accidentally. + // => see also: https://github.com/vis2k/Mirror/issues/2954 + //NetworkIdentity.ResetStatics(); + // => instead, reset only the client sided statics. + NetworkIdentity.ResetClientStatics(); + + // disconnect the client connection. + // we do NOT call Transport.Shutdown, because someone only called + // NetworkClient.Shutdown. we can't assume that the server is + // supposed to be shut down too! + if (Transport.activeTransport != null) + Transport.activeTransport.ClientDisconnect(); + + // reset statics + connectState = ConnectState.None; + connection = null; + localPlayer = null; + ready = false; + isSpawnFinished = false; + isLoadingScene = false; + + unbatcher = new Unbatcher(); + + // clear events. someone might have hooked into them before, but + // we don't want to use those hooks after Shutdown anymore. + OnConnectedEvent = null; + OnDisconnectedEvent = null; + OnErrorEvent = null; + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkClient.cs.meta b/Assets/Mirror/Runtime/NetworkClient.cs.meta new file mode 100644 index 0000000..20cb211 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abe6be14204d94224a3e7cd99dd2ea73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs b/Assets/Mirror/Runtime/NetworkConnection.cs new file mode 100644 index 0000000..14729c6 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnection.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + /// Base NetworkConnection class for server-to-client and client-to-server connection. + public abstract class NetworkConnection + { + public const int LocalConnectionId = 0; + + /// NetworkIdentities that this connection can see + // DEPRECATED 2022-02-05 + [Obsolete("Cast to NetworkConnectionToClient to access .observing")] + public HashSet observing => ((NetworkConnectionToClient)this).observing; + + /// Unique identifier for this connection that is assigned by the transport layer. + // assigned by transport, this id is unique for every connection on server. + // clients don't know their own id and they don't know other client's ids. + public readonly int connectionId; + + /// Flag that indicates the client has been authenticated. + public bool isAuthenticated; + + /// General purpose object to hold authentication data, character selection, tokens, etc. + public object authenticationData; + + /// A server connection is ready after joining the game world. + // TODO move this to ConnectionToClient so the flag only lives on server + // connections? clients could use NetworkClient.ready to avoid redundant + // state. + public bool isReady; + + /// IP address of the connection. Can be useful for game master IP bans etc. + public abstract string address { get; } + + /// Last time a message was received for this connection. Includes system and user messages. + public float lastMessageTime; + + /// This connection's main object (usually the player object). + public NetworkIdentity identity { get; internal set; } + + /// All NetworkIdentities owned by this connection. Can be main player, pets, etc. + // IMPORTANT: this needs to be , not . + // fixes a bug where DestroyOwnedObjects wouldn't find the + // netId anymore: https://github.com/vis2k/Mirror/issues/1380 + // Works fine with NetworkIdentity pointers though. + // DEPRECATED 2022-02-05 + [Obsolete("Cast to NetworkConnectionToClient to access .clientOwnedObjects")] + public HashSet clientOwnedObjects => ((NetworkConnectionToClient)this).clientOwnedObjects; + + // batching from server to client & client to server. + // fewer transport calls give us significantly better performance/scale. + // + // for a 64KB max message transport and 64 bytes/message on average, we + // reduce transport calls by a factor of 1000. + // + // depending on the transport, this can give 10x performance. + // + // Dictionary because we have multiple channels. + protected Dictionary batches = new Dictionary(); + + /// last batch's remote timestamp. not interpolated. useful for NetworkTransform etc. + // for any given NetworkMessage/Rpc/Cmd/OnSerialize, this was the time + // on the REMOTE END when it was sent. + // + // NOTE: this is NOT in NetworkTime, it needs to be per-connection + // because the server receives different batch timestamps from + // different connections. + public double remoteTimeStamp { get; internal set; } + + internal NetworkConnection() + { + // set lastTime to current time when creating connection to make + // sure it isn't instantly kicked for inactivity + lastMessageTime = Time.time; + } + + internal NetworkConnection(int networkConnectionId) : this() + { + connectionId = networkConnectionId; + } + + // TODO if we only have Reliable/Unreliable, then we could initialize + // two batches and avoid this code + protected Batcher GetBatchForChannelId(int channelId) + { + // get existing or create new writer for the channelId + Batcher batch; + if (!batches.TryGetValue(channelId, out batch)) + { + // get max batch size for this channel + int threshold = Transport.activeTransport.GetBatchThreshold(channelId); + + // create batcher + batch = new Batcher(threshold); + batches[channelId] = batch; + } + return batch; + } + + // validate packet size before sending. show errors if too big/small. + // => it's best to check this here, we can't assume that all transports + // would check max size and show errors internally. best to do it + // in one place in Mirror. + // => it's important to log errors, so the user knows what went wrong. + protected static bool ValidatePacketSize(ArraySegment segment, int channelId) + { + int max = Transport.activeTransport.GetMaxPacketSize(channelId); + if (segment.Count > max) + { + Debug.LogError($"NetworkConnection.ValidatePacketSize: cannot send packet larger than {max} bytes, was {segment.Count} bytes"); + return false; + } + + if (segment.Count == 0) + { + // zero length packets getting into the packet queues are bad. + Debug.LogError("NetworkConnection.ValidatePacketSize: cannot send zero bytes"); + return false; + } + + // good size + return true; + } + + // Send stage one: NetworkMessage + /// Send a NetworkMessage to this connection over the given channel. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Send(T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message and send allocation free + MessagePacking.Pack(message, writer); + NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1); + Send(writer.ToArraySegment(), channelId); + } + } + + // Send stage two: serialized NetworkMessage as ArraySegment + // internal because no one except Mirror should send bytes directly to + // the client. they would be detected as a message. send messages instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void Send(ArraySegment segment, int channelId = Channels.Reliable) + { + //Debug.Log($"ConnectionSend {this} bytes:{BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); + + // add to batch no matter what. + // batching will try to fit as many as possible into MTU. + // but we still allow > MTU, e.g. kcp max packet size 144kb. + // those are simply sent as single batches. + // + // IMPORTANT: do NOT send > batch sized messages directly: + // - data race: large messages would be sent directly. small + // messages would be sent in the batch at the end of frame + // - timestamps: if batching assumes a timestamp, then large + // messages need that too. + // + // NOTE: we ALWAYS batch. it's not optional, because the + // receiver needs timestamps for NT etc. + // + // NOTE: we do NOT ValidatePacketSize here yet. the final packet + // will be the full batch, including timestamp. + GetBatchForChannelId(channelId).AddMessage(segment, NetworkTime.localTime); + } + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected abstract void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable); + + // flush batched messages at the end of every Update. + internal virtual void Update() + { + // go through batches for all channels + foreach (KeyValuePair kvp in batches) + { + // make and send as many batches as necessary from the stored + // messages. + Batcher batcher = kvp.Value; + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // make a batch with our local time (double precision) + while (batcher.GetBatch(writer)) + { + // validate packet before handing the batch to the + // transport. this guarantees that we always stay + // within transport's max message size limit. + // => just in case transport forgets to check it + // => just in case mirror miscalulated it etc. + ArraySegment segment = writer.ToArraySegment(); + if (ValidatePacketSize(segment, kvp.Key)) + { + // send to transport + SendToTransport(segment, kvp.Key); + //UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}"); + + // reset writer for each new batch + writer.Position = 0; + } + } + } + } + } + + /// Check if we received a message within the last 'timeout' seconds. + internal virtual bool IsAlive(float timeout) => Time.time - lastMessageTime < timeout; + + /// Disconnects this connection. + // for future reference, here is how Disconnects work in Mirror. + // + // first, there are two types of disconnects: + // * voluntary: the other end simply disconnected + // * involuntary: server disconnects a client by itself + // + // UNET had special (complex) code to handle both cases differently. + // + // Mirror handles both cases the same way: + // * Disconnect is called from TOP to BOTTOM + // NetworkServer/Client -> NetworkConnection -> Transport.Disconnect() + // * Disconnect is handled from BOTTOM to TOP + // Transport.OnDisconnected -> ... + // + // in other words, calling Disconnect() does no cleanup whatsoever. + // it simply asks the transport to disconnect. + // then later the transport events will do the clean up. + public abstract void Disconnect(); + + public override string ToString() => $"connection({connectionId})"; + } +} diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs.meta b/Assets/Mirror/Runtime/NetworkConnection.cs.meta new file mode 100644 index 0000000..32c4ba2 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11ea41db366624109af1f0834bcdde2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs new file mode 100644 index 0000000..4cb56f5 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public class NetworkConnectionToClient : NetworkConnection + { + public override string address => + Transport.activeTransport.ServerGetClientAddress(connectionId); + + /// NetworkIdentities that this connection can see + // TODO move to server's NetworkConnectionToClient? + public new readonly HashSet observing = new HashSet(); + + /// All NetworkIdentities owned by this connection. Can be main player, pets, etc. + // IMPORTANT: this needs to be , not . + // fixes a bug where DestroyOwnedObjects wouldn't find the + // netId anymore: https://github.com/vis2k/Mirror/issues/1380 + // Works fine with NetworkIdentity pointers though. + public new readonly HashSet clientOwnedObjects = new HashSet(); + + // unbatcher + public Unbatcher unbatcher = new Unbatcher(); + + public NetworkConnectionToClient(int networkConnectionId) + : base(networkConnectionId) {} + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.activeTransport.ServerSend(connectionId, segment, channelId); + + /// Disconnects this connection. + public override void Disconnect() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + Transport.activeTransport.ServerDisconnect(connectionId); + + // IMPORTANT: NetworkConnection.Disconnect() is NOT called for + // voluntary disconnects from the other end. + // -> so all 'on disconnect' cleanup code needs to be in + // OnTransportDisconnect, where it's called for both voluntary + // and involuntary disconnects! + } + + internal void AddToObserving(NetworkIdentity netIdentity) + { + observing.Add(netIdentity); + + // spawn identity for this conn + NetworkServer.ShowForConnection(netIdentity, this); + } + + internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) + { + observing.Remove(netIdentity); + + if (!isDestroyed) + { + // hide identity for this conn + NetworkServer.HideForConnection(netIdentity, this); + } + } + + internal void RemoveFromObservingsObservers() + { + foreach (NetworkIdentity netIdentity in observing) + { + netIdentity.RemoveObserver(this); + } + observing.Clear(); + } + + internal void AddOwnedObject(NetworkIdentity obj) + { + clientOwnedObjects.Add(obj); + } + + internal void RemoveOwnedObject(NetworkIdentity obj) + { + clientOwnedObjects.Remove(obj); + } + + internal void DestroyOwnedObjects() + { + // create a copy because the list might be modified when destroying + HashSet tmp = new HashSet(clientOwnedObjects); + foreach (NetworkIdentity netIdentity in tmp) + { + if (netIdentity != null) + { + NetworkServer.Destroy(netIdentity.gameObject); + } + } + + // clear the hashset because we destroyed them all + clientOwnedObjects.Clear(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta new file mode 100644 index 0000000..6001a71 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb2195f8b29d24f0680a57fde2e9fd09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs b/Assets/Mirror/Runtime/NetworkConnectionToServer.cs new file mode 100644 index 0000000..a1ebc5f --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnectionToServer.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public class NetworkConnectionToServer : NetworkConnection + { + public override string address => ""; + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.activeTransport.ClientSend(segment, channelId); + + /// Disconnects this connection. + public override void Disconnect() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + // TODO remove redundant state. have one source of truth for .ready! + isReady = false; + NetworkClient.ready = false; + Transport.activeTransport.ClientDisconnect(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta b/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta new file mode 100644 index 0000000..3424b58 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 761977cbf38a34ded9dd89de45445675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs b/Assets/Mirror/Runtime/NetworkDiagnostics.cs new file mode 100644 index 0000000..1cdc96f --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkDiagnostics.cs @@ -0,0 +1,63 @@ +using System; + +namespace Mirror +{ + /// Profiling statistics for tool to subscribe to (profiler etc.) + public static class NetworkDiagnostics + { + /// Describes an outgoing message + public readonly struct MessageInfo + { + /// The message being sent + public readonly NetworkMessage message; + /// channel through which the message was sent + public readonly int channel; + /// how big was the message (does not include transport headers) + public readonly int bytes; + /// How many connections was the message sent to. + public readonly int count; + + internal MessageInfo(NetworkMessage message, int channel, int bytes, int count) + { + this.message = message; + this.channel = channel; + this.bytes = bytes; + this.count = count; + } + } + + /// Event for when Mirror sends a message. Can be subscribed to. + public static event Action OutMessageEvent; + + /// Event for when Mirror receives a message. Can be subscribed to. + public static event Action InMessageEvent; + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [UnityEngine.RuntimeInitializeOnLoadMethod] + static void ResetStatics() + { + InMessageEvent = null; + OutMessageEvent = null; + } + + internal static void OnSend(T message, int channel, int bytes, int count) + where T : struct, NetworkMessage + { + if (count > 0 && OutMessageEvent != null) + { + MessageInfo outMessage = new MessageInfo(message, channel, bytes, count); + OutMessageEvent?.Invoke(outMessage); + } + } + + internal static void OnReceive(T message, int channel, int bytes) + where T : struct, NetworkMessage + { + if (InMessageEvent != null) + { + MessageInfo inMessage = new MessageInfo(message, channel, bytes, 1); + InMessageEvent?.Invoke(inMessage); + } + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta b/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta new file mode 100644 index 0000000..fe37316 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3754b39e5f8740fd93f3337b2c4274e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Runtime/NetworkIdentity.cs new file mode 100644 index 0000000..6c3c122 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkIdentity.cs @@ -0,0 +1,1317 @@ +using System; +using System.Collections.Generic; +using Mirror.RemoteCalls; +using UnityEngine; +using UnityEngine.Serialization; + +#if UNITY_EDITOR + using UnityEditor; + + #if UNITY_2021_2_OR_NEWER + using UnityEditor.SceneManagement; + #elif UNITY_2018_3_OR_NEWER + using UnityEditor.Experimental.SceneManagement; + #endif +#endif + +namespace Mirror +{ + // Default = use interest management + // ForceHidden = useful to hide monsters while they respawn etc. + // ForceShown = useful to have score NetworkIdentities that always broadcast + // to everyone etc. + public enum Visibility { Default, ForceHidden, ForceShown } + + public struct NetworkIdentitySerialization + { + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + public int tick; + public NetworkWriter ownerWriter; + public NetworkWriter observersWriter; + } + + /// NetworkIdentity identifies objects across the network. + [DisallowMultipleComponent] + // NetworkIdentity.Awake initializes all NetworkComponents. + // let's make sure it's always called before their Awake's. + [DefaultExecutionOrder(-1)] + [AddComponentMenu("Network/Network Identity")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-identity")] + public sealed class NetworkIdentity : MonoBehaviour + { + /// Returns true if running as a client and this object was spawned by a server. + // + // IMPORTANT: + // OnStartClient sets it to true. we NEVER set it to false after. + // otherwise components like Skillbars couldn't use OnDestroy() + // for saving, etc. since isClient may have been reset before + // OnDestroy was called. + // + // we also DO NOT make it dependent on NetworkClient.active or similar. + // we set it, then never change it. that's the user's expectation too. + // + // => fixes https://github.com/vis2k/Mirror/issues/1475 + public bool isClient { get; internal set; } + + /// Returns true if NetworkServer.active and server is not stopped. + // + // IMPORTANT: + // OnStartServer sets it to true. we NEVER set it to false after. + // otherwise components like Skillbars couldn't use OnDestroy() + // for saving, etc. since isServer may have been reset before + // OnDestroy was called. + // + // we also DO NOT make it dependent on NetworkServer.active or similar. + // we set it, then never change it. that's the user's expectation too. + // + // => fixes https://github.com/vis2k/Mirror/issues/1484 + // => fixes https://github.com/vis2k/Mirror/issues/2533 + public bool isServer { get; internal set; } + + /// Return true if this object represents the player on the local machine. + // + // IMPORTANT: + // OnStartLocalPlayer sets it to true. we NEVER set it to false after. + // otherwise components like Skillbars couldn't use OnDestroy() + // for saving, etc. since isLocalPlayer may have been reset before + // OnDestroy was called. + // + // we also DO NOT make it dependent on NetworkClient.localPlayer or similar. + // we set it, then never change it. that's the user's expectation too. + // + // => fixes https://github.com/vis2k/Mirror/issues/2615 + public bool isLocalPlayer { get; internal set; } + + /// True if this object only exists on the server + public bool isServerOnly => isServer && !isClient; + + /// True if this object exists on a client that is not also acting as a server. + public bool isClientOnly => isClient && !isServer; + + /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. + public bool hasAuthority { get; internal set; } + + /// The set of network connections (players) that can see this object. + // note: null until OnStartServer was called. this is necessary for + // SendTo* to work properly in server-only mode. + public Dictionary observers; + + /// The unique network Id of this object (unique at runtime). + public uint netId { get; internal set; } + + /// Unique identifier for NetworkIdentity objects within a scene, used for spawning scene objects. + // persistent scene id (see AssignSceneID comments) + [FormerlySerializedAs("m_SceneId"), HideInInspector] + public ulong sceneId; + + /// Make this object only exist when the game is running as a server (or host). + [FormerlySerializedAs("m_ServerOnly")] + [Tooltip("Prevents this object from being spawned / enabled on clients")] + public bool serverOnly; + + // Set before Destroy is called so that OnDestroy doesn't try to destroy + // the object again + internal bool destroyCalled; + + /// Client's network connection to the server. This is only valid for player objects on the client. + // TODO change to NetworkConnectionToServer, but might cause some breaking + public NetworkConnection connectionToServer { get; internal set; } + + /// Server's network connection to the client. This is only valid for client-owned objects (including the Player object) on the server. + public NetworkConnectionToClient connectionToClient + { + get => _connectionToClient; + internal set + { + _connectionToClient?.RemoveOwnedObject(this); + _connectionToClient = value; + _connectionToClient?.AddOwnedObject(this); + } + } + NetworkConnectionToClient _connectionToClient; + + /// All spawned NetworkIdentities by netId. Available on server and client. + // server sees ALL spawned ones. + // client sees OBSERVED spawned ones. + // => split into NetworkServer.spawned and NetworkClient.spawned to + // reduce shared state between server & client. + // => prepares for NetworkServer/Client as component & better host mode. + [Obsolete("NetworkIdentity.spawned is now NetworkServer.spawned on server, NetworkClient.spawned on client.\nPrepares for NetworkServer/Client as component, better host mode, better testing.")] + public static Dictionary spawned + { + get + { + // server / host mode: use the one from server. + // host mode has access to all spawned. + if (NetworkServer.active) return NetworkServer.spawned; + + // client + if (NetworkClient.active) return NetworkClient.spawned; + + // neither: then we are testing. + // we could default to NetworkServer.spawned. + // but from the outside, that's not obvious. + // better to throw an exception to make it obvious. + throw new Exception("NetworkIdentity.spawned was accessed before NetworkServer/NetworkClient were active."); + } + } + + // get all NetworkBehaviour components + public NetworkBehaviour[] NetworkBehaviours { get; private set; } + + // current visibility + // + // Default = use interest management + // ForceHidden = useful to hide monsters while they respawn etc. + // ForceShown = useful to have score NetworkIdentities that always broadcast + // to everyone etc. + // + // TODO rename to 'visibility' after removing .visibility some day! + [Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")] + public Visibility visible = Visibility.Default; + + // broadcasting serializes all entities around a player for each player. + // we don't want to serialize one entity twice in the same tick. + // so we cache the last serialization and remember the timestamp so we + // know which Update it was serialized. + // (timestamp is the same while inside Update) + // => this way we don't need to pool thousands of writers either. + // => way easier to store them per object + NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization + { + ownerWriter = new NetworkWriter(), + observersWriter = new NetworkWriter() + }; + + /// Prefab GUID used to spawn prefabs across the network. + // + // The AssetId trick: + // Ideally we would have a serialized 'Guid m_AssetId' but Unity can't + // serialize it because Guid's internal bytes are private + // + // UNET used 'NetworkHash128' originally, with byte0, ..., byte16 + // which works, but it just unnecessary extra code + // + // Using just the Guid string would work, but it's 32 chars long and + // would then be sent over the network as 64 instead of 16 bytes + // + // => The solution is to serialize the string internally here and then + // use the real 'Guid' type for everything else via .assetId + public Guid assetId + { + get + { +#if UNITY_EDITOR + // This is important because sometimes OnValidate does not run (like when adding view to prefab with no child links) + if (string.IsNullOrWhiteSpace(m_AssetId)) + SetupIDs(); +#endif + // convert string to Guid and use .Empty to avoid exception if + // we would use 'new Guid("")' + return string.IsNullOrWhiteSpace(m_AssetId) ? Guid.Empty : new Guid(m_AssetId); + } + internal set + { + string newAssetIdString = value == Guid.Empty ? string.Empty : value.ToString("N"); + string oldAssetIdString = m_AssetId; + + // they are the same, do nothing + if (oldAssetIdString == newAssetIdString) + { + return; + } + + // new is empty + if (string.IsNullOrWhiteSpace(newAssetIdString)) + { + Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{oldAssetIdString}'"); + return; + } + + // old not empty + if (!string.IsNullOrWhiteSpace(oldAssetIdString)) + { + Debug.LogError($"Can not Set AssetId on NetworkIdentity '{name}' because it already had an assetId, current assetId '{oldAssetIdString}', attempted new assetId '{newAssetIdString}'"); + return; + } + + // old is empty + m_AssetId = newAssetIdString; + // Debug.Log($"Settings AssetId on NetworkIdentity '{name}', new assetId '{newAssetIdString}'"); + } + } + [SerializeField, HideInInspector] string m_AssetId; + + // Keep track of all sceneIds to detect scene duplicates + static readonly Dictionary sceneIds = + new Dictionary(); + + // reset only client sided statics. + // don't touch server statics when calling StopClient in host mode. + // https://github.com/vis2k/Mirror/issues/2954 + internal static void ResetClientStatics() + { + previousLocalPlayer = null; + clientAuthorityCallback = null; + } + + internal static void ResetServerStatics() + { + nextNetworkId = 1; + } + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + // internal so it can be called from NetworkServer & NetworkClient + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + internal static void ResetStatics() + { + // reset ALL statics + ResetClientStatics(); + ResetServerStatics(); + } + + /// Gets the NetworkIdentity from the sceneIds dictionary with the corresponding id + public static NetworkIdentity GetSceneIdentity(ulong id) => sceneIds[id]; + + // used when adding players + internal void SetClientOwner(NetworkConnectionToClient conn) + { + // do nothing if it already has an owner + if (connectionToClient != null && conn != connectionToClient) + { + Debug.LogError($"Object {this} netId={netId} already has an owner. Use RemoveClientAuthority() first", this); + return; + } + + // otherwise set the owner connection + connectionToClient = conn; + } + + static uint nextNetworkId = 1; + internal static uint GetNextNetworkId() => nextNetworkId++; + + /// Resets nextNetworkId = 1 + public static void ResetNextNetworkId() => nextNetworkId = 1; + + /// The delegate type for the clientAuthorityCallback. + public delegate void ClientAuthorityCallback(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState); + + /// A callback that can be populated to be notified when the client-authority state of objects changes. + public static event ClientAuthorityCallback clientAuthorityCallback; + + // hasSpawned should always be false before runtime + [SerializeField, HideInInspector] bool hasSpawned; + public bool SpawnedFromInstantiate { get; private set; } + + // NetworkBehaviour components are initialized in Awake once. + // Changing them at runtime would get client & server out of sync. + // BUT internal so tests can add them after creating the NetworkIdentity + internal void InitializeNetworkBehaviours() + { + // Get all NetworkBehaviours + // (never null. GetComponents returns [] if none found) + NetworkBehaviours = GetComponents(); + if (NetworkBehaviours.Length > byte.MaxValue) + Debug.LogError($"Only {byte.MaxValue} NetworkBehaviour components are allowed for NetworkIdentity: {name} because we send the index as byte.", this); + + // initialize each one + for (int i = 0; i < NetworkBehaviours.Length; ++i) + { + NetworkBehaviour component = NetworkBehaviours[i]; + component.netIdentity = this; + component.ComponentIndex = i; + } + } + + // Awake is only called in Play mode. + // internal so we can call it during unit tests too. + internal void Awake() + { + // initialize NetworkBehaviour components. + // Awake() is called immediately after initialization. + // no one can overwrite it because NetworkIdentity is sealed. + // => doing it here is the fastest and easiest solution. + InitializeNetworkBehaviours(); + + if (hasSpawned) + { + Debug.LogError($"{name} has already spawned. Don't call Instantiate for NetworkIdentities that were in the scene since the beginning (aka scene objects). Otherwise the client won't know which object to use for a SpawnSceneObject message."); + SpawnedFromInstantiate = true; + Destroy(gameObject); + } + hasSpawned = true; + } + + void OnValidate() + { + // OnValidate is not called when using Instantiate, so we can use + // it to make sure that hasSpawned is false + hasSpawned = false; + +#if UNITY_EDITOR + SetupIDs(); +#endif + } + +#if UNITY_EDITOR + void AssignAssetID(string path) + { + // only set if not empty. fixes https://github.com/vis2k/Mirror/issues/2765 + if (!string.IsNullOrWhiteSpace(path)) + m_AssetId = AssetDatabase.AssetPathToGUID(path); + } + + void AssignAssetID(GameObject prefab) => AssignAssetID(AssetDatabase.GetAssetPath(prefab)); + + // persistent sceneId assignment + // (because scene objects have no persistent unique ID in Unity) + // + // original UNET used OnPostProcessScene to assign an index based on + // FindObjectOfType order. + // -> this didn't work because FindObjectOfType order isn't deterministic. + // -> one workaround is to sort them by sibling paths, but it can still + // get out of sync when we open scene2 in editor and we have + // DontDestroyOnLoad objects that messed with the sibling index. + // + // we absolutely need a persistent id. challenges: + // * it needs to be 0 for prefabs + // => we set it to 0 in SetupIDs() if prefab! + // * it needs to be only assigned in edit time, not at runtime because + // only the objects that were in the scene since beginning should have + // a scene id. + // => Application.isPlaying check solves that + // * it needs to detect duplicated sceneIds after duplicating scene + // objects + // => sceneIds dict takes care of that + // * duplicating the whole scene file shouldn't result in duplicate + // scene objects + // => buildIndex is shifted into sceneId for that. + // => if we have no scenes in build index then it doesn't matter + // because by definition a build can't switch to other scenes + // => if we do have scenes in build index then it will be != -1 + // note: the duplicated scene still needs to be opened once for it to + // be set properly + // * scene objects need the correct scene index byte even if the scene's + // build index was changed or a duplicated scene wasn't opened yet. + // => OnPostProcessScene is the only function that gets called for + // each scene before runtime, so this is where we set the scene + // byte. + // * disabled scenes in build settings should result in same scene index + // in editor and in build + // => .gameObject.scene.buildIndex filters out disabled scenes by + // default + // * generated sceneIds absolutely need to set scene dirty and force the + // user to resave. + // => Undo.RecordObject does that perfectly. + // * sceneIds should never be generated temporarily for unopened scenes + // when building, otherwise editor and build get out of sync + // => BuildPipeline.isBuildingPlayer check solves that + void AssignSceneID() + { + // we only ever assign sceneIds at edit time, never at runtime. + // by definition, only the original scene objects should get one. + // -> if we assign at runtime then server and client would generate + // different random numbers! + if (Application.isPlaying) + return; + + // no valid sceneId yet, or duplicate? + bool duplicate = sceneIds.TryGetValue(sceneId, out NetworkIdentity existing) && existing != null && existing != this; + if (sceneId == 0 || duplicate) + { + // clear in any case, because it might have been a duplicate + sceneId = 0; + + // if a scene was never opened and we are building it, then a + // sceneId would be assigned to build but not saved in editor, + // resulting in them getting out of sync. + // => don't ever assign temporary ids. they always need to be + // permanent + // => throw an exception to cancel the build and let the user + // know how to fix it! + if (BuildPipeline.isBuildingPlayer) + throw new InvalidOperationException($"Scene {gameObject.scene.path} needs to be opened and resaved before building, because the scene object {name} has no valid sceneId yet."); + + // if we generate the sceneId then we MUST be sure to set dirty + // in order to save the scene object properly. otherwise it + // would be regenerated every time we reopen the scene, and + // upgrading would be very difficult. + // -> Undo.RecordObject is the new EditorUtility.SetDirty! + // -> we need to call it before changing. + Undo.RecordObject(this, "Generated SceneId"); + + // generate random sceneId part (0x00000000FFFFFFFF) + uint randomId = Utils.GetTrueRandomUInt(); + + // only assign if not a duplicate of an existing scene id + // (small chance, but possible) + duplicate = sceneIds.TryGetValue(randomId, out existing) && existing != null && existing != this; + if (!duplicate) + { + sceneId = randomId; + //Debug.Log($"{name} in scene {gameObject.scene.name} sceneId assigned to:{sceneId:X}"); + } + } + + // add to sceneIds dict no matter what + // -> even if we didn't generate anything new, because we still need + // existing sceneIds in there to check duplicates + sceneIds[sceneId] = this; + } + + // copy scene path hash into sceneId for scene objects. + // this is the only way for scene file duplication to not contain + // duplicate sceneIds as it seems. + // -> sceneId before: 0x00000000AABBCCDD + // -> then we clear the left 4 bytes, so that our 'OR' uses 0x00000000 + // -> then we OR the hash into the 0x00000000 part + // -> buildIndex is not enough, because Editor and Build have different + // build indices if there are disabled scenes in build settings, and + // if no scene is in build settings then Editor and Build have + // different indices too (Editor=0, Build=-1) + // => ONLY USE THIS FROM POSTPROCESSSCENE! + public void SetSceneIdSceneHashPartInternal() + { + // Use `ToLower` to that because BuildPipeline.BuildPlayer is case insensitive but hash is case sensitive + // If the scene in the project is `forest.unity` but `Forest.unity` is given to BuildPipeline then the + // BuildPipeline will use `Forest.unity` for the build and create a different hash than the editor will. + // Using ToLower will mean the hash will be the same for these 2 paths + // Assets/Scenes/Forest.unity + // Assets/Scenes/forest.unity + string scenePath = gameObject.scene.path.ToLower(); + + // get deterministic scene hash + uint pathHash = (uint)scenePath.GetStableHashCode(); + + // shift hash from 0x000000FFFFFFFF to 0xFFFFFFFF00000000 + ulong shiftedHash = (ulong)pathHash << 32; + + // OR into scene id + sceneId = (sceneId & 0xFFFFFFFF) | shiftedHash; + + // log it. this is incredibly useful to debug sceneId issues. + //Debug.Log($"{name} in scene {gameObject.scene.name} scene index hash {pathHash:X} copied into sceneId {sceneId:X}"); + } + + void SetupIDs() + { + // is this a prefab? + if (Utils.IsPrefab(gameObject)) + { + // force 0 for prefabs + sceneId = 0; + AssignAssetID(gameObject); + } + // are we currently in prefab editing mode? aka prefab stage + // => check prefabstage BEFORE SceneObjectWithPrefabParent + // (fixes https://github.com/vis2k/Mirror/issues/976) + // => if we don't check GetCurrentPrefabStage and only check + // GetPrefabStage(gameObject), then the 'else' case where we + // assign a sceneId and clear the assetId would still be + // triggered for prefabs. in other words: if we are in prefab + // stage, do not bother with anything else ever! + else if (PrefabStageUtility.GetCurrentPrefabStage() != null) + { + // when modifying a prefab in prefab stage, Unity calls + // OnValidate for that prefab and for all scene objects based on + // that prefab. + // + // is this GameObject the prefab that we modify, and not just a + // scene object based on the prefab? + // * GetCurrentPrefabStage = 'are we editing ANY prefab?' + // * GetPrefabStage(go) = 'are we editing THIS prefab?' + if (PrefabStageUtility.GetPrefabStage(gameObject) != null) + { + // force 0 for prefabs + sceneId = 0; + //Debug.Log($"{name} scene:{gameObject.scene.name} sceneid reset to 0 because CurrentPrefabStage={PrefabStageUtility.GetCurrentPrefabStage()} PrefabStage={PrefabStageUtility.GetPrefabStage(gameObject)}"); + + // get path from PrefabStage for this prefab +#if UNITY_2020_1_OR_NEWER + string path = PrefabStageUtility.GetPrefabStage(gameObject).assetPath; +#else + string path = PrefabStageUtility.GetPrefabStage(gameObject).prefabAssetPath; +#endif + + AssignAssetID(path); + } + } + // is this a scene object with prefab parent? + else if (Utils.IsSceneObjectWithPrefabParent(gameObject, out GameObject prefab)) + { + AssignSceneID(); + AssignAssetID(prefab); + } + else + { + AssignSceneID(); + + // IMPORTANT: DO NOT clear assetId at runtime! + // => fixes a bug where clicking any of the NetworkIdentity + // properties (like ServerOnly/ForceHidden) at runtime would + // call OnValidate + // => OnValidate gets into this else case here because prefab + // connection isn't known at runtime + // => then we would clear the previously assigned assetId + // => and NetworkIdentity couldn't be spawned on other clients + // anymore because assetId was cleared + if (!EditorApplication.isPlaying) + { + m_AssetId = ""; + } + // don't log. would show a lot when pressing play in uMMORPG/uSurvival/etc. + //else Debug.Log($"Avoided clearing assetId at runtime for {name} after (probably) clicking any of the NetworkIdentity properties."); + } + } +#endif + + // OnDestroy is called for all SPAWNED NetworkIdentities + // => scene objects aren't destroyed. it's not called for them. + // + // Note: Unity will Destroy all networked objects on Scene Change, so we + // have to handle that here silently. That means we cannot have any + // warning or logging in this method. + void OnDestroy() + { + // Objects spawned from Instantiate are not allowed so are destroyed right away + // we don't want to call NetworkServer.Destroy if this is the case + if (SpawnedFromInstantiate) + return; + + // If false the object has already been unspawned + // if it is still true, then we need to unspawn it + // if destroy is already called don't call it again + if (isServer && !destroyCalled) + { + // Do not add logging to this (see above) + NetworkServer.Destroy(gameObject); + } + + if (isLocalPlayer) + { + // previously there was a bug where isLocalPlayer was + // false in OnDestroy because it was dynamically defined as: + // isLocalPlayer => NetworkClient.localPlayer == this + // we fixed it by setting isLocalPlayer manually and never + // resetting it. + // + // BUT now we need to be aware of a possible data race like in + // our rooms example: + // => GamePlayer is in world + // => player returns to room + // => GamePlayer is destroyed + // => NetworkClient.localPlayer is set to RoomPlayer + // => GamePlayer.OnDestroy is called 1 frame later + // => GamePlayer.OnDestroy 'isLocalPlayer' is true, so here we + // are trying to clear NetworkClient.localPlayer + // => which would overwrite the new RoomPlayer local player + // + // FIXED by simply only clearing if NetworkClient.localPlayer + // still points to US! + // => see also: https://github.com/vis2k/Mirror/issues/2635 + if (NetworkClient.localPlayer == this) + NetworkClient.localPlayer = null; + } + } + + internal void OnStartServer() + { + // do nothing if already spawned + if (isServer) + return; + + // set isServer flag + isServer = true; + + // set isLocalPlayer earlier, in case OnStartLocalplayer is called + // AFTER OnStartClient, in which case it would still be falsse here. + // many projects will check isLocalPlayer in OnStartClient though. + // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? + if (NetworkClient.localPlayer == this) + { + isLocalPlayer = true; + } + + // If the instance/net ID is invalid here then this is an object instantiated from a prefab and the server should assign a valid ID + // NOTE: this might not be necessary because the above m_IsServer + // check already checks netId. BUT this case here checks only + // netId, so it would still check cases where isServer=false + // but netId!=0. + if (netId != 0) + { + // This object has already been spawned, this method might be called again + // if we try to respawn all objects. This can happen when we add a scene + // in that case there is nothing else to do. + return; + } + + netId = GetNextNetworkId(); + observers = new Dictionary(); + + //Debug.Log($"OnStartServer {this} NetId:{netId} SceneId:{sceneId:X}"); + + // add to spawned (note: the original EnableIsServer isn't needed + // because we already set m_isServer=true above) + NetworkServer.spawned[netId] = this; + + // in host mode we set isClient true before calling OnStartServer, + // otherwise isClient is false in OnStartServer. + if (NetworkClient.active) + { + isClient = true; + } + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStartServer should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStartServer(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + internal void OnStopServer() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStartServer should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopServer(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + bool clientStarted; + internal void OnStartClient() + { + if (clientStarted) + return; + clientStarted = true; + + isClient = true; + + // set isLocalPlayer earlier, in case OnStartLocalplayer is called + // AFTER OnStartClient, in which case it would still be falsse here. + // many projects will check isLocalPlayer in OnStartClient though. + // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? + if (NetworkClient.localPlayer == this) + { + isLocalPlayer = true; + } + + // Debug.Log($"OnStartClient {gameObject} netId:{netId}"); + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStartClient should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + // user implemented startup + comp.OnStartClient(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + internal void OnStopClient() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStopClient should be caught, so that + // one component's exception doesn't stop all other components + // from being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopClient(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + // TODO any way to make this not static? + // introduced in https://github.com/vis2k/Mirror/commit/c7530894788bb843b0f424e8f25029efce72d8ca#diff-dc8b7a5a67840f75ccc884c91b9eb76ab7311c9ca4360885a7e41d980865bdc2 + // for PR https://github.com/vis2k/Mirror/pull/1263 + // + // explanation: + // we send the spawn message multiple times. Whenever an object changes + // authority, we send the spawn message again for the object. This is + // necessary because we need to reinitialize all variables when + // ownership change due to sync to owner feature. + // Without this static, the second time we get the spawn message we + // would call OnStartLocalPlayer again on the same object + internal static NetworkIdentity previousLocalPlayer = null; + internal void OnStartLocalPlayer() + { + if (previousLocalPlayer == this) + return; + previousLocalPlayer = this; + + isLocalPlayer = true; + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStartLocalPlayer should be caught, so that + // one component's exception doesn't stop all other components + // from being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStartLocalPlayer(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + internal void OnStopLocalPlayer() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStopLocalPlayer should be caught, so that + // one component's exception doesn't stop all other components + // from being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopLocalPlayer(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + bool hadAuthority; + internal void NotifyAuthority() + { + if (!hadAuthority && hasAuthority) + OnStartAuthority(); + if (hadAuthority && !hasAuthority) + OnStopAuthority(); + hadAuthority = hasAuthority; + } + + internal void OnStartAuthority() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStartAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStartAuthority(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + internal void OnStopAuthority() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // an exception in OnStopAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopAuthority(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } + } + } + + // vis2k: readstring bug prevention: https://github.com/vis2k/Mirror/issues/2617 + // -> OnSerialize writes length,componentData,length,componentData,... + // -> OnDeserialize carefully extracts each data, then deserializes each component with separate readers + // -> it will be impossible to read too many or too few bytes in OnDeserialize + // -> we can properly track down errors + bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initialState) + { + // write placeholder length bytes + // (jumping back later is WAY faster than allocating a temporary + // writer for the payload, then writing payload.size, payload) + int headerPosition = writer.Position; + // no varint because we don't know the final size yet + writer.WriteInt(0); + int contentPosition = writer.Position; + + // write payload + bool result = false; + try + { + result = comp.OnSerialize(writer, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnSerialize failed for: object={name} component={comp.GetType()} sceneId={sceneId:X}\n\n{e}"); + } + int endPosition = writer.Position; + + // fill in length now + writer.Position = headerPosition; + writer.WriteInt(endPosition - contentPosition); + writer.Position = endPosition; + + //Debug.Log($"OnSerializeSafely written for object {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); + + return result; + } + + // serialize all components using dirtyComponentsMask + // check ownerWritten/observersWritten to know if anything was written + // We pass dirtyComponentsMask into this function so that we can check + // if any Components are dirty before creating writers + internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + { + // check if components are in byte.MaxRange just to be 100% sure + // that we avoid overflows + NetworkBehaviour[] components = NetworkBehaviours; + if (components.Length > byte.MaxValue) + throw new IndexOutOfRangeException($"{name} has more than {byte.MaxValue} components. This is not supported."); + + // serialize all components + for (int i = 0; i < components.Length; ++i) + { + // is this component dirty? + // -> always serialize if initialState so all components are included in spawn packet + // -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet + NetworkBehaviour comp = components[i]; + if (initialState || comp.IsDirty()) + { + //Debug.Log($"OnSerializeAllSafely: {name} -> {comp.GetType()} initial:{ initialState}"); + + // remember start position in case we need to copy it into + // observers writer too + int startPosition = ownerWriter.Position; + + // write index as byte [0..255] + ownerWriter.WriteByte((byte)i); + + // serialize into ownerWriter first + // (owner always gets everything!) + OnSerializeSafely(comp, ownerWriter, initialState); + + // copy into observersWriter too if SyncMode.Observers + // -> we copy instead of calling OnSerialize again because + // we don't know what magic the user does in OnSerialize. + // -> it's not guaranteed that calling it twice gets the + // same result + // -> it's not guaranteed that calling it twice doesn't mess + // with the user's OnSerialize timing code etc. + // => so we just copy the result without touching + // OnSerialize again + if (comp.syncMode == SyncMode.Observers) + { + ArraySegment segment = ownerWriter.ToArraySegment(); + int length = ownerWriter.Position - startPosition; + observersWriter.WriteBytes(segment.Array, startPosition, length); + } + } + } + } + + // get cached serialization for this tick (or serialize if none yet) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + internal NetworkIdentitySerialization GetSerializationAtTick(int tick) + { + // only rebuild serialization once per tick. reuse otherwise. + // except for tests, where Time.frameCount never increases. + // so during tests, we always rebuild. + // (otherwise [SyncVar] changes would never be serialized in tests) + // + // NOTE: != instead of < because int.max+1 overflows at some point. + if (lastSerialization.tick != tick || !Application.isPlaying) + { + // reset + lastSerialization.ownerWriter.Position = 0; + lastSerialization.observersWriter.Position = 0; + + // serialize + OnSerializeAllSafely(false, + lastSerialization.ownerWriter, + lastSerialization.observersWriter); + + // clear dirty bits for the components that we serialized. + // previously we did this in NetworkServer.BroadcastToConnection + // for every connection, for every entity. + // but we only serialize each entity once, right here in this + // 'lastSerialization.tick != tick' scope. + // so only do it once. + // + // NOTE: not in OnSerializeAllSafely as that should only do one + // thing: serialize data. + // + // + // NOTE: DO NOT clear ALL component's dirty bits, because + // components can have different syncIntervals and we + // don't want to reset dirty bits for the ones that were + // not synced yet. + // + // NOTE: this used to be very important to avoid ever growing + // SyncList changes if they had no observers, but we've + // added SyncObject.isRecording since. + ClearDirtyComponentsDirtyBits(); + + // set tick + lastSerialization.tick = tick; + //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); + } + + // return it + return lastSerialization; + } + + void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) + { + // read header as 4 bytes and calculate this chunk's start+end + int contentSize = reader.ReadInt(); + int chunkStart = reader.Position; + int chunkEnd = reader.Position + contentSize; + + // call OnDeserialize and wrap it in a try-catch block so there's no + // way to mess up another component's deserialization + try + { + //Debug.Log($"OnDeserializeSafely: {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} length:{contentSize}"); + comp.OnDeserialize(reader, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnDeserialize failed Exception={e.GetType()} (see below) object={name} component={comp.GetType()} sceneId={sceneId:X} length={contentSize}. Possible Reasons:\n" + + $" * Do {comp.GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data({contentSize} bytes)? \n" + + $" * Was there an exception in {comp.GetType()}'s OnSerialize/OnDeserialize code?\n" + + $" * Are the server and client the exact same project?\n" + + $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + + $"Exception {e}"); + } + + // now the reader should be EXACTLY at 'before + size'. + // otherwise the component read too much / too less data. + if (reader.Position != chunkEnd) + { + // warn the user + int bytesRead = reader.Position - chunkStart; + Debug.LogWarning($"OnDeserialize was expected to read {contentSize} instead of {bytesRead} bytes for object:{name} component={comp.GetType()} sceneId={sceneId:X}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + + // fix the position, so the following components don't all fail + reader.Position = chunkEnd; + } + } + + internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState) + { + if (NetworkBehaviours == null) + { + Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" + + $"Typically this can happen when a networked object is a child of a " + + $"non-networked parent that's disabled, preventing Awake on the networked object " + + $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject); + return; + } + + // deserialize all components that were received + NetworkBehaviour[] components = NetworkBehaviours; + while (reader.Remaining > 0) + { + // read & check index [0..255] + byte index = reader.ReadByte(); + if (index < components.Length) + { + // deserialize this component + OnDeserializeSafely(components[index], reader, initialState); + } + } + } + + // Helper function to handle Command/Rpc + internal void HandleRemoteCall(byte componentIndex, int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null) + { + // check if unity object has been destroyed + if (this == null) + { + Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]"); + return; + } + + // find the right component to invoke the function on + if (componentIndex >= NetworkBehaviours.Length) + { + Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]"); + return; + } + + NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex]; + if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection)) + { + Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}]."); + } + } + + internal void AddObserver(NetworkConnectionToClient conn) + { + if (observers == null) + { + Debug.LogError($"AddObserver for {gameObject} observer list is null"); + return; + } + + if (observers.ContainsKey(conn.connectionId)) + { + // if we try to add a connectionId that was already added, then + // we may have generated one that was already in use. + return; + } + + // Debug.Log($"Added observer: {conn.address} added for {gameObject}"); + + // if we previously had no observers, then clear all dirty bits once. + // a monster's health may have changed while it had no observers. + // but that change (= the dirty bits) don't matter as soon as the + // first observer comes. + // -> first observer gets full spawn packet + // -> afterwards it gets delta packet + // => if we don't clear previous dirty bits, observer would get + // the health change because the bit was still set. + // => ultimately this happens because spawn doesn't reset dirty + // bits + // => which happens because spawn happens separately, instead of + // in Broadcast() (which will be changed in the future) + // + // NOTE that NetworkServer.Broadcast previously cleared dirty bits + // for ALL SPAWNED that don't have observers. that was super + // expensive. doing it when adding the first observer has the + // same result, without the O(N) iteration in Broadcast(). + // + // TODO remove this after moving spawning into Broadcast()! + if (observers.Count == 0) + { + ClearAllComponentsDirtyBits(); + } + + observers[conn.connectionId] = conn; + conn.AddToObserving(this); + } + + // this is used when a connection is destroyed, since the "observers" property is read-only + internal void RemoveObserver(NetworkConnection conn) + { + observers?.Remove(conn.connectionId); + } + + // Called when NetworkIdentity is destroyed + internal void ClearObservers() + { + if (observers != null) + { + foreach (NetworkConnectionToClient conn in observers.Values) + { + conn.RemoveFromObserving(this, true); + } + observers.Clear(); + } + } + + /// Assign control of an object to a client via the client's NetworkConnection. + // This causes hasAuthority to be set on the client that owns the object, + // and NetworkBehaviour.OnStartAuthority will be called on that client. + // This object then will be in the NetworkConnection.clientOwnedObjects + // list for the connection. + // + // Authority can be removed with RemoveClientAuthority. Only one client + // can own an object at any time. This does not need to be called for + // player objects, as their authority is setup automatically. + public bool AssignClientAuthority(NetworkConnectionToClient conn) + { + if (!isServer) + { + Debug.LogError("AssignClientAuthority can only be called on the server for spawned objects."); + return false; + } + + if (conn == null) + { + Debug.LogError($"AssignClientAuthority for {gameObject} owner cannot be null. Use RemoveClientAuthority() instead."); + return false; + } + + if (connectionToClient != null && conn != connectionToClient) + { + Debug.LogError($"AssignClientAuthority for {gameObject} already has an owner. Use RemoveClientAuthority() first."); + return false; + } + + SetClientOwner(conn); + + // The client will match to the existing object + NetworkServer.SendChangeOwnerMessage(this, conn); + + clientAuthorityCallback?.Invoke(conn, this, true); + + return true; + } + + /// Removes ownership for an object. + // Applies to objects that had authority set by AssignClientAuthority, + // or NetworkServer.Spawn with a NetworkConnection parameter included. + // Authority cannot be removed for player objects. + public void RemoveClientAuthority() + { + if (!isServer) + { + Debug.LogError("RemoveClientAuthority can only be called on the server for spawned objects."); + return; + } + + if (connectionToClient?.identity == this) + { + Debug.LogError("RemoveClientAuthority cannot remove authority for a player object"); + return; + } + + if (connectionToClient != null) + { + clientAuthorityCallback?.Invoke(connectionToClient, this, false); + NetworkConnectionToClient previousOwner = connectionToClient; + connectionToClient = null; + NetworkServer.SendChangeOwnerMessage(this, previousOwner); + } + } + + // Reset is called when the user hits the Reset button in the + // Inspector's context menu or when adding the component the first time. + // This function is only called in editor mode. + // + // Reset() seems to be called only for Scene objects. + // we can't destroy them (they are always in the scene). + // instead we disable them and call Reset(). + // + // OLD COMMENT: + // Marks the identity for future reset, this is because we cant reset + // the identity during destroy as people might want to be able to read + // the members inside OnDestroy(), and we have no way of invoking reset + // after OnDestroy is called. + internal void Reset() + { + // make sure to call this before networkBehavioursCache is cleared below + ResetSyncObjects(); + + hasSpawned = false; + clientStarted = false; + isClient = false; + isServer = false; + //isLocalPlayer = false; <- cleared AFTER ClearLocalPlayer below! + + // remove authority flag. This object may be unspawned, not destroyed, on client. + hasAuthority = false; + NotifyAuthority(); + + netId = 0; + connectionToServer = null; + connectionToClient = null; + + ClearObservers(); + + // clear local player if it was the local player, + // THEN reset isLocalPlayer AFTERWARDS + if (isLocalPlayer) + { + // only clear NetworkClient.localPlayer IF IT POINTS TO US! + // see OnDestroy() comments. it does the same. + // (https://github.com/vis2k/Mirror/issues/2635) + if (NetworkClient.localPlayer == this) + NetworkClient.localPlayer = null; + } + + previousLocalPlayer = null; + isLocalPlayer = false; + } + + // clear all component's dirty bits no matter what + internal void ClearAllComponentsDirtyBits() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ClearAllDirtyBits(); + } + } + + // Clear only dirty component's dirty bits. ignores components which + // may be dirty but not ready to be synced yet (because of syncInterval) + // + // NOTE: this used to be very important to avoid ever + // growing SyncList changes if they had no observers, + // but we've added SyncObject.isRecording since. + internal void ClearDirtyComponentsDirtyBits() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + if (comp.IsDirty()) + { + comp.ClearAllDirtyBits(); + } + } + } + + void ResetSyncObjects() + { + // ResetSyncObjects is called by Reset, which is called by Unity. + // AddComponent() calls Reset(). + // AddComponent() is called before Awake(). + // so NetworkBehaviours may not be initialized yet. + if (NetworkBehaviours == null) + return; + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ResetSyncObjects(); + } + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta b/Assets/Mirror/Runtime/NetworkIdentity.cs.meta new file mode 100644 index 0000000..7b96521 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkIdentity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b91ecbcc199f4492b9a91e820070131 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs b/Assets/Mirror/Runtime/NetworkLoop.cs new file mode 100644 index 0000000..50d9e95 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkLoop.cs @@ -0,0 +1,208 @@ +// our ideal update looks like this: +// transport.process_incoming() +// update_world() +// transport.process_outgoing() +// +// this way we avoid unnecessary latency for low-ish server tick rates. +// for example, if we were to use this tick: +// transport.process_incoming/outgoing() +// update_world() +// +// then anything sent in update_world wouldn't be actually sent out by the +// transport until the next frame. if server runs at 60Hz, then this can add +// 16ms latency for every single packet. +// +// => instead we process incoming, update world, process_outgoing in the same +// frame. it's more clear (no race conditions) and lower latency. +// => we need to add custom Update functions to the Unity engine: +// NetworkEarlyUpdate before Update()/FixedUpdate() +// NetworkLateUpdate after LateUpdate() +// this way the user can update the world in Update/FixedUpdate/LateUpdate +// and networking still runs before/after those functions no matter what! +// => see also: https://docs.unity3d.com/Manual/ExecutionOrder.html +// => update order: +// * we add to the end of EarlyUpdate so it runs after any Unity initializations +// * we add to the end of PreLateUpdate so it runs after LateUpdate(). adding +// to the beginning of PostLateUpdate doesn't actually work. +using System; +using UnityEngine; + +// PlayerLoop and LowLevel were in the Experimental namespace until 2019.3 +// https://docs.unity3d.com/2019.2/Documentation/ScriptReference/Experimental.LowLevel.PlayerLoop.html +// https://docs.unity3d.com/2019.3/Documentation/ScriptReference/LowLevel.PlayerLoop.html +#if UNITY_2019_3_OR_NEWER +using UnityEngine.LowLevel; +using UnityEngine.PlayerLoop; +#else +using UnityEngine.Experimental.LowLevel; +using UnityEngine.Experimental.PlayerLoop; +#endif + +namespace Mirror +{ + public static class NetworkLoop + { + // helper enum to add loop to begin/end of subSystemList + internal enum AddMode { Beginning, End } + + // callbacks in case someone needs to use early/lateupdate too. + public static Action OnEarlyUpdate; + public static Action OnLateUpdate; + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void ResetStatics() + { + OnEarlyUpdate = null; + OnLateUpdate = null; + } + + // helper function to find an update function's index in a player loop + // type. this is used for testing to guarantee our functions are added + // at the beginning/end properly. + internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction function, PlayerLoopSystem playerLoop, Type playerLoopSystemType) + { + // did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc. + if (playerLoop.type == playerLoopSystemType) + return Array.FindIndex(playerLoop.subSystemList, (elem => elem.updateDelegate == function)); + + // recursively keep looking + if (playerLoop.subSystemList != null) + { + for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + { + int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType); + if (index != -1) return index; + } + } + return -1; + } + + // MODIFIED AddSystemToPlayerLoopList from Unity.Entities.ScriptBehaviourUpdateOrder (ECS) + // + // => adds an update function to the Unity internal update type. + // => Unity has different update loops: + // https://medium.com/@thebeardphantom/unity-2018-and-playerloop-5c46a12a677 + // EarlyUpdate + // FixedUpdate + // PreUpdate + // Update + // PreLateUpdate + // PostLateUpdate + // + // function: the custom update function to add + // IMPORTANT: according to a comment in Unity.Entities.ScriptBehaviourUpdateOrder, + // the UpdateFunction can not be virtual because + // Mono 4.6 has problems invoking virtual methods + // as delegates from native! + // ownerType: the .type to fill in so it's obvious who the new function + // belongs to. seems to be mostly for debugging. pass any. + // addMode: prepend or append to update list + internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, Type ownerType, ref PlayerLoopSystem playerLoop, Type playerLoopSystemType, AddMode addMode) + { + // did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc. + if (playerLoop.type == playerLoopSystemType) + { + // debugging + //Debug.Log($"Found playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:"); + //foreach (PlayerLoopSystem sys in playerLoop.subSystemList) + // Debug.Log($" ->{sys.type}"); + + // resize & expand subSystemList to fit one more entry + int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0; + Array.Resize(ref playerLoop.subSystemList, oldListLength + 1); + + // IMPORTANT: always insert a FRESH PlayerLoopSystem! + // We CAN NOT resize and then OVERWRITE an entry's type/loop. + // => PlayerLoopSystem has native IntPtr loop members + // => forgetting to clear those would cause undefined behaviour! + // see also: https://github.com/vis2k/Mirror/pull/2652 + PlayerLoopSystem system = new PlayerLoopSystem { + type = ownerType, + updateDelegate = function + }; + + // prepend our custom loop to the beginning + if (addMode == AddMode.Beginning) + { + // shift to the right, write into first array element + Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1); + playerLoop.subSystemList[0] = system; + + } + // append our custom loop to the end + else if (addMode == AddMode.End) + { + // simply write into last array element + playerLoop.subSystemList[oldListLength] = system; + } + + // debugging + //Debug.Log($"New playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:"); + //foreach (PlayerLoopSystem sys in playerLoop.subSystemList) + // Debug.Log($" ->{sys.type}"); + + return true; + } + + // recursively keep looking + if (playerLoop.subSystemList != null) + { + for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + { + if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode)) + return true; + } + } + return false; + } + + // hook into Unity runtime to actually add our custom functions + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void RuntimeInitializeOnLoad() + { + //Debug.Log("Mirror: adding Network[Early/Late]Update to Unity..."); + + // get loop + // 2019 has GetCURRENTPlayerLoop which is safe to use without + // breaking other custom system's custom loops. + // see also: https://github.com/vis2k/Mirror/pull/2627/files + PlayerLoopSystem playerLoop = +#if UNITY_2019_3_OR_NEWER + PlayerLoop.GetCurrentPlayerLoop(); +#else + PlayerLoop.GetDefaultPlayerLoop(); +#endif + + // add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after + // any Unity initializations but before the first Update/FixedUpdate + AddToPlayerLoop(NetworkEarlyUpdate, typeof(NetworkLoop), ref playerLoop, typeof(EarlyUpdate), AddMode.End); + + // add NetworkLateUpdate to the end of PreLateUpdate so it runs after + // LateUpdate(). adding to the beginning of PostLateUpdate doesn't + // actually work. + AddToPlayerLoop(NetworkLateUpdate, typeof(NetworkLoop), ref playerLoop, typeof(PreLateUpdate), AddMode.End); + + // set the new loop + PlayerLoop.SetPlayerLoop(playerLoop); + } + + static void NetworkEarlyUpdate() + { + //Debug.Log($"NetworkEarlyUpdate {Time.time}"); + NetworkServer.NetworkEarlyUpdate(); + NetworkClient.NetworkEarlyUpdate(); + // invoke event after mirror has done it's early updating. + OnEarlyUpdate?.Invoke(); + } + + static void NetworkLateUpdate() + { + //Debug.Log($"NetworkLateUpdate {Time.time}"); + // invoke event before mirror does its final late updating. + OnLateUpdate?.Invoke(); + NetworkServer.NetworkLateUpdate(); + NetworkClient.NetworkLateUpdate(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs.meta b/Assets/Mirror/Runtime/NetworkLoop.cs.meta new file mode 100644 index 0000000..52b6e6a --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkLoop.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c6cec4e279774b919386e05545317b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Runtime/NetworkManager.cs new file mode 100644 index 0000000..37be9ae --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkManager.cs @@ -0,0 +1,1374 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kcp2k; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Serialization; + +namespace Mirror +{ + public enum PlayerSpawnMethod { Random, RoundRobin } + public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } + + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Manager")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager")] + public class NetworkManager : MonoBehaviour + { + /// Enable to keep NetworkManager alive when changing scenes. + // This should be set if your game has a single NetworkManager that exists for the lifetime of the process. If there is a NetworkManager in each scene, then this should not be set. + [Header("Configuration")] + [FormerlySerializedAs("m_DontDestroyOnLoad")] + [Tooltip("Should the Network Manager object be persisted through scene changes?")] + public bool dontDestroyOnLoad = true; + + /// Multiplayer games should always run in the background so the network doesn't time out. + [FormerlySerializedAs("m_RunInBackground")] + [Tooltip("Multiplayer games should always run in the background so the network doesn't time out.")] + public bool runInBackground = true; + + /// Should the server auto-start when 'Server Build' is checked in build settings + [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] + [FormerlySerializedAs("startOnHeadless")] + public bool autoStartServerBuild = true; + + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + [Tooltip("Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + public int serverTickRate = 30; + + /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). + [Header("Scene Management")] + [Scene] + [FormerlySerializedAs("m_OfflineScene")] + [Tooltip("Scene that Mirror will switch to when the client or server is stopped")] + public string offlineScene = ""; + + /// Automatically switch to this scene upon going online (after connect/startserver). + [Scene] + [FormerlySerializedAs("m_OnlineScene")] + [Tooltip("Scene that Mirror will switch to when the server is started. Clients will recieve a Scene Message to load the server's current scene when they connect.")] + public string onlineScene = ""; + + // transport layer + [Header("Network Info")] + [Tooltip("Transport component attached to this object that server and client will use to connect")] + [SerializeField] + protected Transport transport; + + /// Server's address for clients to connect to. + [FormerlySerializedAs("m_NetworkAddress")] + [Tooltip("Network Address where the client should connect to the server. Server does not use this for anything.")] + public string networkAddress = "localhost"; + + /// The maximum number of concurrent network connections to support. + [FormerlySerializedAs("m_MaxConnections")] + [Tooltip("Maximum number of concurrent connections.")] + public int maxConnections = 100; + + [Header("Authentication")] + [Tooltip("Authentication component attached to this object")] + public NetworkAuthenticator authenticator; + + /// The default prefab to be used to create player objects on the server. + // Player objects are created in the default handler for AddPlayer() on + // the server. Implementing OnServerAddPlayer overrides this behaviour. + [Header("Player Object")] + [FormerlySerializedAs("m_PlayerPrefab")] + [Tooltip("Prefab of the player object. Prefab must have a Network Identity component. May be an empty game object or a full avatar.")] + public GameObject playerPrefab; + + /// Enable to automatically create player objects on connect and on scene change. + [FormerlySerializedAs("m_AutoCreatePlayer")] + [Tooltip("Should Mirror automatically spawn the player after scene change?")] + public bool autoCreatePlayer = true; + + /// Where to spawn players. + [FormerlySerializedAs("m_PlayerSpawnMethod")] + [Tooltip("Round Robin or Random order of Start Position selection")] + public PlayerSpawnMethod playerSpawnMethod; + + /// Prefabs that can be spawned over the network need to be registered here. + [FormerlySerializedAs("m_SpawnPrefabs"), HideInInspector] + public List spawnPrefabs = new List(); + + /// List of transforms populated by NetworkStartPositions + public static List startPositions = new List(); + public static int startPositionIndex; + + /// The one and only NetworkManager + public static NetworkManager singleton { get; internal set; } + + /// Number of active player objects across all connections on the server. + public int numPlayers => NetworkServer.connections.Count(kv => kv.Value.identity != null); + + /// True if the server is running or client is connected/connecting. + public bool isNetworkActive => NetworkServer.active || NetworkClient.active; + + // TODO remove this + // internal for tests + internal static NetworkConnection clientReadyConnection; + + /// True if the client loaded a new scene when connecting to the server. + // This is set before OnClientConnect is called, so it can be checked + // there to perform different logic if a scene load occurred. + protected bool clientLoadedScene; + + // helper enum to know if we started the networkmanager as server/client/host. + // -> this is necessary because when StartHost changes server scene to + // online scene, FinishLoadScene is called and the host client isn't + // connected yet (no need to connect it before server was fully set up). + // in other words, we need this to know which mode we are running in + // during FinishLoadScene. + public NetworkManagerMode mode { get; private set; } + + // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too + public virtual void OnValidate() + { + // always >= 0 + maxConnections = Mathf.Max(maxConnections, 0); + + if (playerPrefab != null && playerPrefab.GetComponent() == null) + { + Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); + playerPrefab = null; + } + + // This avoids the mysterious "Replacing existing prefab with assetId ... Old prefab 'Player', New prefab 'Player'" warning. + if (playerPrefab != null && spawnPrefabs.Contains(playerPrefab)) + { + Debug.LogWarning("NetworkManager - Player Prefab should not be added to Registered Spawnable Prefabs list...removed it."); + spawnPrefabs.Remove(playerPrefab); + } + } + + // virtual so that inheriting classes' Reset() can call base.Reset() too + // Reset only gets called when the component is added or the user resets the component + // Thats why we validate these things that only need to be validated on adding the NetworkManager here + // If we would do it in OnValidate() then it would run this everytime a value changes + public virtual void Reset() + { + // make sure someone doesn't accidentally add another NetworkManager + // need transform.root because when adding to a child, the parent's + // Reset isn't called. + foreach (NetworkManager manager in transform.root.GetComponentsInChildren()) + { + if (manager != this) + { + Debug.LogError($"{name} detected another component of type {typeof(NetworkManager)} in its hierarchy on {manager.name}. There can only be one, please remove one of them."); + // return early so that transport component isn't auto-added + // to the duplicate NetworkManager. + return; + } + } + + // add transport if there is none yet. makes upgrading easier. + if (transport == null) + { +#if UNITY_EDITOR + // RecordObject needs to be called before we make the change + UnityEditor.Undo.RecordObject(gameObject, "Added default Transport"); +#endif + + transport = GetComponent(); + + // was a transport added yet? if not, add one + if (transport == null) + { + transport = gameObject.AddComponent(); + Debug.Log("NetworkManager: added default Transport because there was none yet."); + } + } + } + + // virtual so that inheriting classes' Awake() can call base.Awake() too + public virtual void Awake() + { + // Don't allow collision-destroyed second instance to continue. + if (!InitializeSingleton()) return; + + Debug.Log("Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + + // Set the networkSceneName to prevent a scene reload + // if client connection to server fails. + networkSceneName = offlineScene; + + // setup OnSceneLoaded callback + SceneManager.sceneLoaded += OnSceneLoaded; + } + + // virtual so that inheriting classes' Start() can call base.Start() too + public virtual void Start() + { + // headless mode? then start the server + // can't do this in Awake because Awake is for initialization. + // some transports might not be ready until Start. + // + // (tick rate is applied in StartServer!) +#if UNITY_SERVER + if (autoStartServerBuild) + { + StartServer(); + } +#endif + } + + // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + public virtual void LateUpdate() + { + UpdateScene(); + } + + // keep the online scene change check in a separate function + bool IsServerOnlineSceneChangeNeeded() + { + // Only change scene if the requested online scene is not blank, and is not already loaded + return !string.IsNullOrWhiteSpace(onlineScene) && !IsSceneActive(onlineScene) && onlineScene != offlineScene; + } + + public static bool IsSceneActive(string scene) + { + Scene activeScene = SceneManager.GetActiveScene(); + return activeScene.path == scene || activeScene.name == scene; + } + + // full server setup code, without spawning objects yet + void SetupServer() + { + // Debug.Log("NetworkManager SetupServer"); + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + if (authenticator != null) + { + authenticator.OnStartServer(); + authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated); + } + + ConfigureHeadlessFrameRate(); + + // start listening to network connections + NetworkServer.Listen(maxConnections); + + // call OnStartServer AFTER Listen, so that NetworkServer.active is + // true and we can call NetworkServer.Spawn in OnStartServer + // overrides. + // (useful for loading & spawning stuff from database etc.) + // + // note: there is no risk of someone connecting after Listen() and + // before OnStartServer() because this all runs in one thread + // and we don't start processing connects until Update. + OnStartServer(); + + // this must be after Listen(), since that registers the default message handlers + RegisterServerMessages(); + } + + /// Starts the server, listening for incoming connections. + public void StartServer() + { + if (NetworkServer.active) + { + Debug.LogWarning("Server already started."); + return; + } + + mode = NetworkManagerMode.ServerOnly; + + // StartServer is inherently ASYNCHRONOUS (=doesn't finish immediately) + // + // Here is what it does: + // Listen + // if onlineScene: + // LoadSceneAsync + // ... + // FinishLoadSceneServerOnly + // SpawnObjects + // else: + // SpawnObjects + // + // there is NO WAY to make it synchronous because both LoadSceneAsync + // and LoadScene do not finish loading immediately. as long as we + // have the onlineScene feature, it will be asynchronous! + + SetupServer(); + + // scene change needed? then change scene and spawn afterwards. + if (IsServerOnlineSceneChangeNeeded()) + { + ServerChangeScene(onlineScene); + } + // otherwise spawn directly + else + { + NetworkServer.SpawnObjects(); + } + } + + /// Starts the client, connects it to the server with networkAddress. + public void StartClient() + { + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } + + mode = NetworkManagerMode.ClientOnly; + + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + if (authenticator != null) + { + authenticator.OnStartClient(); + authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); + } + + // In case this is a headless client... + ConfigureHeadlessFrameRate(); + + RegisterClientMessages(); + + if (string.IsNullOrWhiteSpace(networkAddress)) + { + Debug.LogError("Must set the Network Address field in the manager"); + return; + } + // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); + + NetworkClient.Connect(networkAddress); + + OnStartClient(); + } + + /// Starts the client, connects it to the server via Uri + public void StartClient(Uri uri) + { + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } + + mode = NetworkManagerMode.ClientOnly; + + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + if (authenticator != null) + { + authenticator.OnStartClient(); + authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); + } + + RegisterClientMessages(); + + // Debug.Log($"NetworkManager StartClient address:{uri}"); + networkAddress = uri.Host; + + NetworkClient.Connect(uri); + + OnStartClient(); + } + + /// Starts a network "host" - a server and client in the same application. + public void StartHost() + { + if (NetworkServer.active || NetworkClient.active) + { + Debug.LogWarning("Server or Client already started."); + return; + } + + mode = NetworkManagerMode.Host; + + // StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately) + // + // Here is what it does: + // Listen + // ConnectHost + // if onlineScene: + // LoadSceneAsync + // ... + // FinishLoadSceneHost + // FinishStartHost + // SpawnObjects + // StartHostClient <= not guaranteed to happen after SpawnObjects if onlineScene is set! + // ClientAuth + // success: server sends changescene msg to client + // else: + // FinishStartHost + // + // there is NO WAY to make it synchronous because both LoadSceneAsync + // and LoadScene do not finish loading immediately. as long as we + // have the onlineScene feature, it will be asynchronous! + + // setup server first + SetupServer(); + + // call OnStartHost AFTER SetupServer. this way we can use + // NetworkServer.Spawn etc. in there too. just like OnStartServer + // is called after the server is actually properly started. + OnStartHost(); + + // scene change needed? then change scene and spawn afterwards. + // => BEFORE host client connects. if client auth succeeds then the + // server tells it to load 'onlineScene'. we can't do that if + // server is still in 'offlineScene'. so load on server first. + if (IsServerOnlineSceneChangeNeeded()) + { + // call FinishStartHost after changing scene. + finishStartHostPending = true; + ServerChangeScene(onlineScene); + } + // otherwise call FinishStartHost directly + else + { + FinishStartHost(); + } + } + + // This may be set true in StartHost and is evaluated in FinishStartHost + bool finishStartHostPending; + + // FinishStartHost is guaranteed to be called after the host server was + // fully started and all the asynchronous StartHost magic is finished + // (= scene loading), or immediately if there was no asynchronous magic. + // + // note: we don't really need FinishStartClient/FinishStartServer. the + // host version is enough. + void FinishStartHost() + { + // ConnectHost needs to be called BEFORE SpawnObjects: + // https://github.com/vis2k/Mirror/pull/1249/ + // -> this sets NetworkServer.localConnection. + // -> localConnection needs to be set before SpawnObjects because: + // -> SpawnObjects calls OnStartServer in all NetworkBehaviours + // -> OnStartServer might spawn an object and set [SyncVar(hook="OnColorChanged")] object.color = green; + // -> this calls SyncVar.set (generated by Weaver), which has + // a custom case for host mode (because host mode doesn't + // get OnDeserialize calls, where SyncVar hooks are usually + // called): + // + // if (!SyncVarEqual(value, ref color)) + // { + // if (NetworkServer.localClientActive && !getSyncVarHookGuard(1uL)) + // { + // setSyncVarHookGuard(1uL, value: true); + // OnColorChangedHook(value); + // setSyncVarHookGuard(1uL, value: false); + // } + // SetSyncVar(value, ref color, 1uL); + // } + // + // -> localClientActive needs to be true, otherwise the hook + // isn't called in host mode! + // + // TODO call this after spawnobjects and worry about the syncvar hook fix later? + NetworkClient.ConnectHost(); + + // server scene was loaded. now spawn all the objects + NetworkServer.SpawnObjects(); + + // connect client and call OnStartClient AFTER server scene was + // loaded and all objects were spawned. + // DO NOT do this earlier. it would cause race conditions where a + // client will do things before the server is even fully started. + //Debug.Log("StartHostClient called"); + StartHostClient(); + } + + void StartHostClient() + { + //Debug.Log("NetworkManager ConnectLocalClient"); + + if (authenticator != null) + { + authenticator.OnStartClient(); + authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); + } + + networkAddress = "localhost"; + NetworkServer.ActivateHostScene(); + RegisterClientMessages(); + + // ConnectLocalServer needs to be called AFTER RegisterClientMessages + // (https://github.com/vis2k/Mirror/pull/1249/) + NetworkClient.ConnectLocalServer(); + + OnStartClient(); + } + + /// This stops both the client and the server that the manager is using. + public void StopHost() + { + OnStopHost(); + + // calling OnTransportDisconnected was needed to fix + // https://github.com/vis2k/Mirror/issues/1515 + // so that the host client receives a DisconnectMessage + // TODO reevaluate if this is still needed after all the disconnect + // fixes, and try to put this into LocalConnection.Disconnect! + NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId); + + StopClient(); + StopServer(); + } + + /// Stops the server from listening and simulating the game. + public void StopServer() + { + // return if already stopped to avoid recursion deadlock + if (!NetworkServer.active) + return; + + if (authenticator != null) + { + authenticator.OnServerAuthenticated.RemoveListener(OnServerAuthenticated); + authenticator.OnStopServer(); + } + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + OnStopServer(); + + //Debug.Log("NetworkManager StopServer"); + NetworkServer.Shutdown(); + + // set offline mode BEFORE changing scene so that FinishStartScene + // doesn't think we need initialize anything. + mode = NetworkManagerMode.Offline; + + if (!string.IsNullOrWhiteSpace(offlineScene)) + { + ServerChangeScene(offlineScene); + } + + startPositionIndex = 0; + + networkSceneName = ""; + } + + /// Stops and disconnects the client. + public void StopClient() + { + if (mode == NetworkManagerMode.Offline) + return; + + if (authenticator != null) + { + authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); + authenticator.OnStopClient(); + } + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + OnStopClient(); + + //Debug.Log("NetworkManager StopClient"); + + // set offline mode BEFORE changing scene so that FinishStartScene + // doesn't think we need initialize anything. + // set offline mode BEFORE NetworkClient.Disconnect so StopClient + // only runs once. + mode = NetworkManagerMode.Offline; + + // shutdown client + NetworkClient.Disconnect(); + NetworkClient.Shutdown(); + + // If this is the host player, StopServer will already be changing scenes. + // Check loadingSceneAsync to ensure we don't double-invoke the scene change. + // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. + if (!string.IsNullOrWhiteSpace(offlineScene) && !IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) + { + ClientChangeScene(offlineScene, SceneOperation.Normal); + } + + networkSceneName = ""; + } + + // called when quitting the application by closing the window / pressing + // stop in the editor. virtual so that inheriting classes' + // OnApplicationQuit() can call base.OnApplicationQuit() too + public virtual void OnApplicationQuit() + { + // stop client first + // (we want to send the quit packet to the server instead of waiting + // for a timeout) + if (NetworkClient.isConnected) + { + StopClient(); + //Debug.Log("OnApplicationQuit: stopped client"); + } + + // stop server after stopping client (for proper host mode stopping) + if (NetworkServer.active) + { + StopServer(); + //Debug.Log("OnApplicationQuit: stopped server"); + } + + // Call ResetStatics to reset statics and singleton + ResetStatics(); + } + + /// Set the frame rate for a headless builds. Override to disable or modify. + // useful for dedicated servers. + // useful for headless benchmark clients. + public virtual void ConfigureHeadlessFrameRate() + { +#if UNITY_SERVER + Application.targetFrameRate = serverTickRate; + // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); +#endif + } + + bool InitializeSingleton() + { + if (singleton != null && singleton == this) + return true; + + if (dontDestroyOnLoad) + { + if (singleton != null) + { + Debug.LogWarning("Multiple NetworkManagers detected in the scene. Only one NetworkManager can exist at a time. The duplicate NetworkManager will be destroyed."); + Destroy(gameObject); + + // Return false to not allow collision-destroyed second instance to continue. + return false; + } + //Debug.Log("NetworkManager created singleton (DontDestroyOnLoad)"); + singleton = this; + if (Application.isPlaying) + { + // Force the object to scene root, in case user made it a child of something + // in the scene since DDOL is only allowed for scene root objects + transform.SetParent(null); + DontDestroyOnLoad(gameObject); + } + } + else + { + //Debug.Log("NetworkManager created singleton (ForScene)"); + singleton = this; + } + + // set active transport AFTER setting singleton. + // so only if we didn't destroy ourselves. + Transport.activeTransport = transport; + return true; + } + + void RegisterServerMessages() + { + NetworkServer.OnConnectedEvent = OnServerConnectInternal; + NetworkServer.OnDisconnectedEvent = OnServerDisconnect; + NetworkServer.OnErrorEvent = OnServerError; + NetworkServer.RegisterHandler(OnServerAddPlayerInternal); + + // Network Server initially registers its own handler for this, so we replace it here. + NetworkServer.ReplaceHandler(OnServerReadyMessageInternal); + } + + void RegisterClientMessages() + { + NetworkClient.OnConnectedEvent = OnClientConnectInternal; + NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; + NetworkClient.OnErrorEvent = OnClientError; + NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); + NetworkClient.RegisterHandler(OnClientSceneInternal, false); + + if (playerPrefab != null) + NetworkClient.RegisterPrefab(playerPrefab); + + foreach (GameObject prefab in spawnPrefabs.Where(t => t != null)) + NetworkClient.RegisterPrefab(prefab); + } + + // This is the only way to clear the singleton, so another instance can be created. + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void ResetStatics() + { + // call StopHost if we have a singleton + if (singleton) + singleton.StopHost(); + + // reset all statics + startPositions.Clear(); + startPositionIndex = 0; + clientReadyConnection = null; + loadingSceneAsync = null; + networkSceneName = string.Empty; + + // and finally (in case it isn't null already)... + singleton = null; + } + + // virtual so that inheriting classes' OnDestroy() can call base.OnDestroy() too + public virtual void OnDestroy() + { + //Debug.Log("NetworkManager destroyed"); + } + + /// The name of the current network scene. + // set by NetworkManager when changing the scene. + // new clients will automatically load this scene. + // Loading a scene manually won't set it. + public static string networkSceneName { get; protected set; } = ""; + + public static AsyncOperation loadingSceneAsync; + + /// Change the server scene and all client's scenes across the network. + // Called automatically if onlineScene or offlineScene are set, but it + // can be called from user code to switch scenes again while the game is + // in progress. This automatically sets clients to be not-ready during + // the change and ready again to participate in the new scene. + public virtual void ServerChangeScene(string newSceneName) + { + if (string.IsNullOrWhiteSpace(newSceneName)) + { + Debug.LogError("ServerChangeScene empty scene name"); + return; + } + + if (NetworkServer.isLoadingScene && newSceneName == networkSceneName) + { + Debug.LogError($"Scene change is already in progress for {newSceneName}"); + return; + } + + // Debug.Log($"ServerChangeScene {newSceneName}"); + NetworkServer.SetAllClientsNotReady(); + networkSceneName = newSceneName; + + // Let server prepare for scene change + OnServerChangeScene(newSceneName); + + // set server flag to stop processing messages while changing scenes + // it will be re-enabled in FinishLoadScene. + NetworkServer.isLoadingScene = true; + + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); + + // ServerChangeScene can be called when stopping the server + // when this happens the server is not active so does not need to tell clients about the change + if (NetworkServer.active) + { + // notify all clients about the new scene + NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName }); + } + + startPositionIndex = 0; + startPositions.Clear(); + } + + // This is only set in ClientChangeScene below...never on server. + // We need to check this in OnClientSceneChanged called from FinishLoadSceneClientOnly + // to prevent AddPlayer message after loading/unloading additive scenes + SceneOperation clientSceneOperation = SceneOperation.Normal; + + internal void ClientChangeScene(string newSceneName, SceneOperation sceneOperation = SceneOperation.Normal, bool customHandling = false) + { + if (string.IsNullOrWhiteSpace(newSceneName)) + { + Debug.LogError("ClientChangeScene empty scene name"); + return; + } + + //Debug.Log($"ClientChangeScene newSceneName: {newSceneName} networkSceneName{networkSceneName}"); + + // Let client prepare for scene change + OnClientChangeScene(newSceneName, sceneOperation, customHandling); + + // After calling OnClientChangeScene, exit if server since server is already doing + // the actual scene change, and we don't need to do it for the host client + if (NetworkServer.active) + return; + + // set client flag to stop processing messages while loading scenes. + // otherwise we would process messages and then lose all the state + // as soon as the load is finishing, causing all kinds of bugs + // because of missing state. + // (client may be null after StopClient etc.) + // Debug.Log("ClientChangeScene: pausing handlers while scene is loading to avoid data loss after scene was loaded."); + NetworkClient.isLoadingScene = true; + + // Cache sceneOperation so we know what was requested by the + // Scene message in OnClientChangeScene and OnClientSceneChanged + clientSceneOperation = sceneOperation; + + // scene handling will happen in overrides of OnClientChangeScene and/or OnClientSceneChanged + // Do not call FinishLoadScene here. Custom handler will assign loadingSceneAsync and we need + // to wait for that to finish. UpdateScene already checks for that to be not null and isDone. + if (customHandling) + return; + + switch (sceneOperation) + { + case SceneOperation.Normal: + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); + break; + case SceneOperation.LoadAdditive: + // Ensure additive scene is not already loaded on client by name or path + // since we don't know which was passed in the Scene message + if (!SceneManager.GetSceneByName(newSceneName).IsValid() && !SceneManager.GetSceneByPath(newSceneName).IsValid()) + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, LoadSceneMode.Additive); + else + { + Debug.LogWarning($"Scene {newSceneName} is already loaded"); + + // Reset the flag that we disabled before entering this switch + NetworkClient.isLoadingScene = false; + } + break; + case SceneOperation.UnloadAdditive: + // Ensure additive scene is actually loaded on client by name or path + // since we don't know which was passed in the Scene message + if (SceneManager.GetSceneByName(newSceneName).IsValid() || SceneManager.GetSceneByPath(newSceneName).IsValid()) + loadingSceneAsync = SceneManager.UnloadSceneAsync(newSceneName, UnloadSceneOptions.UnloadAllEmbeddedSceneObjects); + else + { + Debug.LogWarning($"Cannot unload {newSceneName} with UnloadAdditive operation"); + + // Reset the flag that we disabled before entering this switch + NetworkClient.isLoadingScene = false; + } + break; + } + + // don't change the client's current networkSceneName when loading additive scene content + if (sceneOperation == SceneOperation.Normal) + networkSceneName = newSceneName; + } + + // support additive scene loads: + // NetworkScenePostProcess disables all scene objects on load, and + // * NetworkServer.SpawnObjects enables them again on the server when + // calling OnStartServer + // * NetworkClient.PrepareToSpawnSceneObjects enables them again on the + // client after the server sends ObjectSpawnStartedMessage to client + // in SpawnObserversForConnection. this is only called when the + // client joins, so we need to rebuild scene objects manually again + // TODO merge this with FinishLoadScene()? + void OnSceneLoaded(Scene scene, LoadSceneMode mode) + { + if (mode == LoadSceneMode.Additive) + { + if (NetworkServer.active) + { + // TODO only respawn the server objects from that scene later! + NetworkServer.SpawnObjects(); + // Debug.Log($"Respawned Server objects after additive scene load: {scene.name}"); + } + if (NetworkClient.active) + { + NetworkClient.PrepareToSpawnSceneObjects(); + // Debug.Log($"Rebuild Client spawnableObjects after additive scene load: {scene.name}"); + } + } + } + + void UpdateScene() + { + if (loadingSceneAsync != null && loadingSceneAsync.isDone) + { + //Debug.Log($"ClientChangeScene done readyConn {clientReadyConnection}"); + + // try-finally to guarantee loadingSceneAsync being cleared. + // fixes https://github.com/vis2k/Mirror/issues/2517 where if + // FinishLoadScene throws an exception, loadingSceneAsync would + // never be cleared and this code would run every Update. + try + { + FinishLoadScene(); + } + finally + { + loadingSceneAsync.allowSceneActivation = true; + loadingSceneAsync = null; + } + } + } + + protected void FinishLoadScene() + { + // NOTE: this cannot use NetworkClient.allClients[0] - that client may be for a completely different purpose. + + // process queued messages that we received while loading the scene + //Debug.Log("FinishLoadScene: resuming handlers after scene was loading."); + NetworkServer.isLoadingScene = false; + NetworkClient.isLoadingScene = false; + + // host mode? + if (mode == NetworkManagerMode.Host) + { + FinishLoadSceneHost(); + } + // server-only mode? + else if (mode == NetworkManagerMode.ServerOnly) + { + FinishLoadSceneServerOnly(); + } + // client-only mode? + else if (mode == NetworkManagerMode.ClientOnly) + { + FinishLoadSceneClientOnly(); + } + // otherwise we called it after stopping when loading offline scene. + // do nothing then. + } + + // finish load scene part for host mode. makes code easier and is + // necessary for FinishStartHost later. + // (the 3 things have to happen in that exact order) + void FinishLoadSceneHost() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in host mode."); + + if (clientReadyConnection != null) + { +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientConnect(clientReadyConnection); +#pragma warning restore 618 + clientLoadedScene = true; + clientReadyConnection = null; + } + + // do we need to finish a StartHost() call? + // then call FinishStartHost and let it take care of spawning etc. + if (finishStartHostPending) + { + finishStartHostPending = false; + FinishStartHost(); + + // call OnServerSceneChanged + OnServerSceneChanged(networkSceneName); + + // DO NOT call OnClientSceneChanged here. + // the scene change happened because StartHost loaded the + // server's online scene. it has nothing to do with the client. + // this was not meant as a client scene load, so don't call it. + // + // otherwise AddPlayer would be called twice: + // -> once for client OnConnected + // -> once in OnClientSceneChanged + } + // otherwise we just changed a scene in host mode + else + { + // spawn server objects + NetworkServer.SpawnObjects(); + + // call OnServerSceneChanged + OnServerSceneChanged(networkSceneName); + + if (NetworkClient.isConnected) + { + // let client know that we changed scene +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientSceneChanged(NetworkClient.connection); +#pragma warning restore 618 + } + } + } + + // finish load scene part for server-only. . makes code easier and is + // necessary for FinishStartServer later. + void FinishLoadSceneServerOnly() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in server-only mode."); + + NetworkServer.SpawnObjects(); + OnServerSceneChanged(networkSceneName); + } + + // finish load scene part for client-only. makes code easier and is + // necessary for FinishStartClient later. + void FinishLoadSceneClientOnly() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in client-only mode."); + + if (clientReadyConnection != null) + { +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientConnect(clientReadyConnection); +#pragma warning restore 618 + clientLoadedScene = true; + clientReadyConnection = null; + } + + if (NetworkClient.isConnected) + { +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientSceneChanged(NetworkClient.connection); +#pragma warning restore 618 + } + } + + /// + /// Registers the transform of a game object as a player spawn location. + /// This is done automatically by NetworkStartPosition components, but can be done manually from user script code. + /// + /// Transform to register. + // Static because it's called from NetworkStartPosition::Awake + // and singleton may not exist yet + public static void RegisterStartPosition(Transform start) + { + // Debug.Log($"RegisterStartPosition: {start.gameObject.name} {start.position}"); + startPositions.Add(start); + + // reorder the list so that round-robin spawning uses the start positions + // in hierarchy order. This assumes all objects with NetworkStartPosition + // component are siblings, either in the scene root or together as children + // under a single parent in the scene. + startPositions = startPositions.OrderBy(transform => transform.GetSiblingIndex()).ToList(); + } + + /// Unregister a Transform from start positions. + // Static because it's called from NetworkStartPosition::OnDestroy + // and singleton may not exist yet + public static void UnRegisterStartPosition(Transform start) + { + //Debug.Log($"UnRegisterStartPosition: {start.name} {start.position}"); + startPositions.Remove(start); + } + + /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. + public Transform GetStartPosition() + { + // first remove any dead transforms + startPositions.RemoveAll(t => t == null); + + if (startPositions.Count == 0) + return null; + + if (playerSpawnMethod == PlayerSpawnMethod.Random) + { + return startPositions[UnityEngine.Random.Range(0, startPositions.Count)]; + } + else + { + Transform startPosition = startPositions[startPositionIndex]; + startPositionIndex = (startPositionIndex + 1) % startPositions.Count; + return startPosition; + } + } + + void OnServerConnectInternal(NetworkConnectionToClient conn) + { + //Debug.Log("NetworkManager.OnServerConnectInternal"); + + if (authenticator != null) + { + // we have an authenticator - let it handle authentication + authenticator.OnServerAuthenticate(conn); + } + else + { + // authenticate immediately + OnServerAuthenticated(conn); + } + } + + // called after successful authentication + // TODO do the NetworkServer.OnAuthenticated thing from x branch + void OnServerAuthenticated(NetworkConnectionToClient conn) + { + //Debug.Log("NetworkManager.OnServerAuthenticated"); + + // set connection to authenticated + conn.isAuthenticated = true; + + // proceed with the login handshake by calling OnServerConnect + if (networkSceneName != "" && networkSceneName != offlineScene) + { + SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; + conn.Send(msg); + } + + OnServerConnect(conn); + } + + void OnServerReadyMessageInternal(NetworkConnectionToClient conn, ReadyMessage msg) + { + //Debug.Log("NetworkManager.OnServerReadyMessageInternal"); + OnServerReady(conn); + } + + void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage msg) + { + //Debug.Log("NetworkManager.OnServerAddPlayer"); + + if (autoCreatePlayer && playerPrefab == null) + { + Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); + return; + } + + if (autoCreatePlayer && playerPrefab.GetComponent() == null) + { + Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); + return; + } + + if (conn.identity != null) + { + Debug.LogError("There is already a player for this connection."); + return; + } + + OnServerAddPlayer(conn); + } + + void OnClientConnectInternal() + { + //Debug.Log("NetworkManager.OnClientConnectInternal"); + + if (authenticator != null) + { + // we have an authenticator - let it handle authentication + authenticator.OnClientAuthenticate(); + } + else + { + // authenticate immediately + OnClientAuthenticated(); + } + } + + // called after successful authentication + void OnClientAuthenticated() + { + //Debug.Log("NetworkManager.OnClientAuthenticated"); + + // set connection to authenticated + NetworkClient.connection.isAuthenticated = true; + + // proceed with the login handshake by calling OnClientConnect + if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || IsSceneActive(onlineScene)) + { + clientLoadedScene = false; +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientConnect(NetworkClient.connection); +#pragma warning restore 618 + } + else + { + // will wait for scene id to come from the server. + clientLoadedScene = true; + clientReadyConnection = NetworkClient.connection; + } + } + + void OnClientDisconnectInternal() + { + //Debug.Log("NetworkManager.OnClientDisconnectInternal"); +#pragma warning disable 618 + // obsolete method calls new method because it's not empty + OnClientDisconnect(NetworkClient.connection); +#pragma warning restore 618 + } + + void OnClientNotReadyMessageInternal(NotReadyMessage msg) + { + //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); + NetworkClient.ready = false; +#pragma warning disable 618 + OnClientNotReady(NetworkClient.connection); +#pragma warning restore 618 + OnClientNotReady(); + + // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. + } + + void OnClientSceneInternal(SceneMessage msg) + { + //Debug.Log("NetworkManager.OnClientSceneInternal"); + + // This needs to run for host client too. NetworkServer.active is checked there + if (NetworkClient.isConnected) + { + ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling); + } + } + + /// Called on the server when a new client connects. + public virtual void OnServerConnect(NetworkConnectionToClient conn) {} + + /// Called on the server when a client disconnects. + // Called by NetworkServer.OnTransportDisconnect! + public virtual void OnServerDisconnect(NetworkConnectionToClient conn) + { + // by default, this function destroys the connection's player. + // can be overwritten for cases like delayed logouts in MMOs to + // avoid players escaping from PvP situations by logging out. + NetworkServer.DestroyPlayerForConnection(conn); + //Debug.Log("OnServerDisconnect: Client disconnected."); + } + + /// Called on the server when a client is ready (= loaded the scene) + public virtual void OnServerReady(NetworkConnectionToClient conn) + { + if (conn.identity == null) + { + // this is now allowed (was not for a while) + //Debug.Log("Ready with no player object"); + } + NetworkServer.SetClientReady(conn); + } + + /// Called on server when a client requests to add the player. Adds playerPrefab by default. Can be overwritten. + // The default implementation for this function creates a new player object from the playerPrefab. + public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) + { + Transform startPos = GetStartPosition(); + GameObject player = startPos != null + ? Instantiate(playerPrefab, startPos.position, startPos.rotation) + : Instantiate(playerPrefab); + + // instantiating a "Player" prefab gives it the name "Player(clone)" + // => appending the connectionId is WAY more useful for debugging! + player.name = $"{playerPrefab.name} [connId={conn.connectionId}]"; + NetworkServer.AddPlayerForConnection(conn, player); + } + + /// Called on server when transport raises an exception. NetworkConnection may be null. + public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {} + + /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed + public virtual void OnServerChangeScene(string newSceneName) {} + + /// Called on server after a scene load with ServerChangeScene() is completed. + public virtual void OnServerSceneChanged(string sceneName) {} + + /// Called on the client when connected to a server. By default it sets client as ready and adds a player. + public virtual void OnClientConnect() + { + // OnClientConnect by default calls AddPlayer but it should not do + // that when we have online/offline scenes. so we need the + // clientLoadedScene flag to prevent it. + if (!clientLoadedScene) + { + // Ready/AddPlayer is usually triggered by a scene load completing. + // if no scene was loaded, then Ready/AddPlayer it here instead. + if (!NetworkClient.ready) + NetworkClient.Ready(); + + if (autoCreatePlayer) + NetworkClient.AddPlayer(); + } + } + + // Deprecated 2021-12-11 + [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] + public virtual void OnClientConnect(NetworkConnection conn) => OnClientConnect(); + + /// Called on clients when disconnected from a server. + public virtual void OnClientDisconnect() + { + if (mode == NetworkManagerMode.Offline) + return; + + StopClient(); + } + + // Deprecated 2021-12-11 + [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] + public virtual void OnClientDisconnect(NetworkConnection conn) => OnClientDisconnect(); + + /// Called on client when transport raises an exception. + public virtual void OnClientError(Exception exception) {} + + /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. + public virtual void OnClientNotReady() {} + + // Deprecated 2021-12-11 + [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] + public virtual void OnClientNotReady(NetworkConnection conn) {} + + /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed + // customHandling: indicates if scene loading will be handled through overrides + public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {} + + /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. + // Scene changes can cause player objects to be destroyed. The default + // implementation of OnClientSceneChanged in the NetworkManager is to + // add a player object for the connection if no player object exists. + public virtual void OnClientSceneChanged() + { + // always become ready. + if (!NetworkClient.ready) NetworkClient.Ready(); + + // Only call AddPlayer for normal scene changes, not additive load/unload + if (clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) + { + // add player if existing one is null + NetworkClient.AddPlayer(); + } + } + + // Deprecated 2021-12-11 + [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] + public virtual void OnClientSceneChanged(NetworkConnection conn) => OnClientSceneChanged(); + + // Since there are multiple versions of StartServer, StartClient and + // StartHost, to reliably customize their functionality, users would + // need override all the versions. Instead these callbacks are invoked + // from all versions, so users only need to implement this one case. + + /// This is invoked when a host is started. + public virtual void OnStartHost() {} + + /// This is invoked when a server is started - including when a host is started. + public virtual void OnStartServer() {} + + /// This is invoked when the client is started. + public virtual void OnStartClient() {} + + /// This is called when a server is stopped - including when a host is stopped. + public virtual void OnStopServer() {} + + /// This is called when a client is stopped. + public virtual void OnStopClient() {} + + /// This is called when a host is stopped. + public virtual void OnStopHost() {} + } +} diff --git a/Assets/Mirror/Runtime/NetworkManager.cs.meta b/Assets/Mirror/Runtime/NetworkManager.cs.meta new file mode 100644 index 0000000..0a7564a --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs b/Assets/Mirror/Runtime/NetworkManagerHUD.cs new file mode 100644 index 0000000..cba968d --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkManagerHUD.cs @@ -0,0 +1,149 @@ +// vis2k: GUILayout instead of spacey += ...; removed Update hotkeys to avoid +// confusion if someone accidentally presses one. +using UnityEngine; + +namespace Mirror +{ + /// Shows NetworkManager controls in a GUI at runtime. + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Manager HUD")] + [RequireComponent(typeof(NetworkManager))] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager-hud")] + public class NetworkManagerHUD : MonoBehaviour + { + NetworkManager manager; + + public int offsetX; + public int offsetY; + + void Awake() + { + manager = GetComponent(); + } + + void OnGUI() + { + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999)); + if (!NetworkClient.isConnected && !NetworkServer.active) + { + StartButtons(); + } + else + { + StatusLabels(); + } + + // client ready + if (NetworkClient.isConnected && !NetworkClient.ready) + { + if (GUILayout.Button("Client Ready")) + { + NetworkClient.Ready(); + if (NetworkClient.localPlayer == null) + { + NetworkClient.AddPlayer(); + } + } + } + + StopButtons(); + + GUILayout.EndArea(); + } + + void StartButtons() + { + if (!NetworkClient.active) + { + // Server + Client + if (Application.platform != RuntimePlatform.WebGLPlayer) + { + if (GUILayout.Button("Host (Server + Client)")) + { + manager.StartHost(); + } + } + + // Client + IP + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Client")) + { + manager.StartClient(); + } + // This updates networkAddress every frame from the TextField + manager.networkAddress = GUILayout.TextField(manager.networkAddress); + GUILayout.EndHorizontal(); + + // Server Only + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + // cant be a server in webgl build + GUILayout.Box("( WebGL cannot be server )"); + } + else + { + if (GUILayout.Button("Server Only")) manager.StartServer(); + } + } + else + { + // Connecting + GUILayout.Label($"Connecting to {manager.networkAddress}.."); + if (GUILayout.Button("Cancel Connection Attempt")) + { + manager.StopClient(); + } + } + } + + void StatusLabels() + { + // host mode + // display separately because this always confused people: + // Server: ... + // Client: ... + if (NetworkServer.active && NetworkClient.active) + { + GUILayout.Label($"Host: running via {Transport.activeTransport}"); + } + // server only + else if (NetworkServer.active) + { + GUILayout.Label($"Server: running via {Transport.activeTransport}"); + } + // client only + else if (NetworkClient.isConnected) + { + GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.activeTransport}"); + } + } + + void StopButtons() + { + // stop host if host mode + if (NetworkServer.active && NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Host")) + { + manager.StopHost(); + } + } + // stop client if client-only + else if (NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Client")) + { + manager.StopClient(); + } + } + // stop server if server-only + else if (NetworkServer.active) + { + if (GUILayout.Button("Stop Server")) + { + manager.StopServer(); + } + } + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta b/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta new file mode 100644 index 0000000..a720b9c --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6442dc8070ceb41f094e44de0bf87274 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs b/Assets/Mirror/Runtime/NetworkMessage.cs new file mode 100644 index 0000000..7fc387a --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkMessage.cs @@ -0,0 +1,4 @@ +namespace Mirror +{ + public interface NetworkMessage {} +} diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs.meta b/Assets/Mirror/Runtime/NetworkMessage.cs.meta new file mode 100644 index 0000000..73d3d8f --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eb04e4848a2e4452aa2dbd7adb801c51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkReader.cs b/Assets/Mirror/Runtime/NetworkReader.cs new file mode 100644 index 0000000..86eeef4 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReader.cs @@ -0,0 +1,202 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace Mirror +{ + /// Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool.GetReader() to avoid allocations. + // Note: This class is intended to be extremely pedantic, + // and throw exceptions whenever stuff is going slightly wrong. + // The exceptions will be handled in NetworkServer/NetworkClient. + public class NetworkReader + { + // internal buffer + // byte[] pointer would work, but we use ArraySegment to also support + // the ArraySegment constructor + ArraySegment buffer; + + /// Next position to read from the buffer + // 'int' is the best type for .Position. 'short' is too small if we send >32kb which would result in negative .Position + // -> converting long to int is fine until 2GB of data (MAX_INT), so we don't have to worry about overflows here + public int Position; + + /// Total number of bytes to read from buffer + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => buffer.Count; + } + + /// Remaining bytes that can be read, for convenience. + public int Remaining + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Length - Position; + } + + public NetworkReader(byte[] bytes) + { + buffer = new ArraySegment(bytes); + } + + public NetworkReader(ArraySegment segment) + { + buffer = segment; + } + + // sometimes it's useful to point a reader on another buffer instead of + // allocating a new reader (e.g. NetworkReaderPool) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBuffer(byte[] bytes) + { + buffer = new ArraySegment(bytes); + Position = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBuffer(ArraySegment segment) + { + buffer = segment; + Position = 0; + } + + // ReadBlittable from DOTSNET + // this is extremely fast, but only works for blittable types. + // => private to make sure nobody accidentally uses it for non-blittable + // + // Benchmark: see NetworkWriter.WriteBlittable! + // + // Note: + // ReadBlittable assumes same endianness for server & client. + // All Unity 2018+ platforms are little endian. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe T ReadBlittable() + where T : unmanaged + { + // check if blittable for safety +#if UNITY_EDITOR + if (!UnsafeUtility.IsBlittable(typeof(T))) + { + throw new ArgumentException($"{typeof(T)} is not blittable!"); + } +#endif + + // calculate size + // sizeof(T) gets the managed size at compile time. + // Marshal.SizeOf gets the unmanaged size at runtime (slow). + // => our 1mio writes benchmark is 6x slower with Marshal.SizeOf + // => for blittable types, sizeof(T) is even recommended: + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices + int size = sizeof(T); + + // enough data to read? + if (Position + size > buffer.Count) + { + throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> out of range: {ToString()}"); + } + + // read blittable + T value; + fixed (byte* ptr = &buffer.Array[buffer.Offset + Position]) + { +#if UNITY_ANDROID + // on some android systems, reading *(T*)ptr throws a NRE if + // the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.). + // here we have to use memcpy. + // + // => we can't get a pointer of a struct in C# without + // marshalling allocations + // => instead, we stack allocate an array of type T and use that + // => stackalloc avoids GC and is very fast. it only works for + // value types, but all blittable types are anyway. + // + // this way, we can still support blittable reads on android. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // (solution discovered by AIIO, FakeByte, mischa) + T* valueBuffer = stackalloc T[1]; + UnsafeUtility.MemCpy(valueBuffer, ptr, size); + value = valueBuffer[0]; +#else + // cast buffer to a T* pointer and then read from it. + value = *(T*)ptr; +#endif + } + Position += size; + return value; + } + + // blittable'?' template for code reuse + // note: bool isn't blittable. need to read as byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal T? ReadBlittableNullable() + where T : unmanaged => + ReadByte() != 0 ? ReadBlittable() : default(T?); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() => ReadBlittable(); + + /// Read 'count' bytes into the bytes array + // NOTE: returns byte[] because all reader functions return something. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] ReadBytes(byte[] bytes, int count) + { + // check if passed byte array is big enough + if (count > bytes.Length) + { + throw new EndOfStreamException($"ReadBytes can't read {count} + bytes because the passed byte[] only has length {bytes.Length}"); + } + // check if within buffer limits + if (Position + count > buffer.Count) + { + throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); + } + + Array.Copy(buffer.Array, buffer.Offset + Position, bytes, 0, count); + Position += count; + return bytes; + } + + /// Read 'count' bytes allocation-free as ArraySegment that points to the internal array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment ReadBytesSegment(int count) + { + // check if within buffer limits + if (Position + count > buffer.Count) + { + throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); + } + + // return the segment + ArraySegment result = new ArraySegment(buffer.Array, buffer.Offset + Position, count); + Position += count; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => + $"NetworkReader pos={Position} len={Length} buffer={BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)}"; + + /// Reads any data type that mirror supports. Uses weaver populated Reader(T).read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Read() + { + Func readerDelegate = Reader.read; + if (readerDelegate == null) + { + Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader"); + return default; + } + return readerDelegate(this); + } + } + + /// Helper class that weaver populates with all reader types. + // Note that c# creates a different static variable for each type + // -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it + public static class Reader + { + public static Func read; + } +} diff --git a/Assets/Mirror/Runtime/NetworkReader.cs.meta b/Assets/Mirror/Runtime/NetworkReader.cs.meta new file mode 100644 index 0000000..65ad3f0 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1610f05ec5bd14d6882e689f7372596a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs b/Assets/Mirror/Runtime/NetworkReaderExtensions.cs new file mode 100644 index 0000000..6137866 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderExtensions.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // Mirror's Weaver automatically detects all NetworkReader function types, + // but they do all need to be extensions. + public static class NetworkReaderExtensions + { + // cache encoding instead of creating it each time + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + // bool is not blittable. read as ushort. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable(); + + // bool is not blittable. read as byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable() != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool? ReadBoolNullable(this NetworkReader reader) + { + byte? value = reader.ReadBlittableNullable(); + return value.HasValue ? (value.Value != 0) : default(bool?); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + /// if an invalid utf8 string is sent + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ReadString(this NetworkReader reader) + { + // read number of bytes + ushort size = reader.ReadUShort(); + + // null support, see NetworkWriter + if (size == 0) + return null; + + int realSize = size - 1; + + // make sure it's within limits to avoid allocation attacks etc. + if (realSize >= NetworkWriter.MaxStringLength) + { + throw new EndOfStreamException($"ReadString too long: {realSize}. Limit is: {NetworkWriter.MaxStringLength}"); + } + + ArraySegment data = reader.ReadBytesSegment(realSize); + + // convert directly from buffer to string via encoding + return encoding.GetString(data.Array, data.Offset, data.Count); + } + + /// if count is invalid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadBytesAndSize(this NetworkReader reader) + { + // count = 0 means the array was null + // otherwise count -1 is the length of the array + uint count = reader.ReadUInt(); + // Use checked() to force it to throw OverflowException if data is invalid + return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadBytes(this NetworkReader reader, int count) + { + byte[] bytes = new byte[count]; + reader.ReadBytes(bytes, count); + return bytes; + } + + /// if count is invalid + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader reader) + { + // count = 0 means the array was null + // otherwise count - 1 is the length of the array + uint count = reader.ReadUInt(); + // Use checked() to force it to throw OverflowException if data is invalid + return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rect ReadRect(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Plane ReadPlane(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Ray ReadRay(this NetworkReader reader) => reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader)=> reader.ReadBlittable(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid ReadGuid(this NetworkReader reader) => new Guid(reader.ReadBytes(16)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) + { + uint netId = reader.ReadUInt(); + if (netId == 0) + return null; + + // NOTE: a netId not being in spawned is common. + // for example, "[SyncVar] NetworkIdentity target" netId would not + // be known on client if the monster walks out of proximity for a + // moment. no need to log any error or warning here. + return Utils.GetSpawnedInServerOrClient(netId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) + { + // read netId first. + // + // IMPORTANT: if netId != 0, writer always writes componentIndex. + // reusing ReadNetworkIdentity() might return a null NetworkIdentity + // even if netId was != 0 but the identity disappeared on the client, + // resulting in unequal amounts of data being written / read. + // https://github.com/vis2k/Mirror/issues/2972 + uint netId = reader.ReadUInt(); + if (netId == 0) + return null; + + // read component index in any case, BEFORE searching the spawned + // NetworkIdentity by netId. + byte componentIndex = reader.ReadByte(); + + // NOTE: a netId not being in spawned is common. + // for example, "[SyncVar] NetworkIdentity target" netId would not + // be known on client if the monster walks out of proximity for a + // moment. no need to log any error or warning here. + NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId); + + return identity != null + ? identity.NetworkBehaviours[componentIndex] + : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ReadNetworkBehaviour(this NetworkReader reader) where T : NetworkBehaviour + { + return reader.ReadNetworkBehaviour() as T; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader) + { + uint netId = reader.ReadUInt(); + byte componentIndex = default; + + // if netId is not 0, then index is also sent to read before returning + if (netId != 0) + { + componentIndex = reader.ReadByte(); + } + + return new NetworkBehaviour.NetworkBehaviourSyncVar(netId, componentIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Transform ReadTransform(this NetworkReader reader) + { + // Don't use null propagation here as it could lead to MissingReferenceException + NetworkIdentity networkIdentity = reader.ReadNetworkIdentity(); + return networkIdentity != null ? networkIdentity.transform : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GameObject ReadGameObject(this NetworkReader reader) + { + // Don't use null propagation here as it could lead to MissingReferenceException + NetworkIdentity networkIdentity = reader.ReadNetworkIdentity(); + return networkIdentity != null ? networkIdentity.gameObject : null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static List ReadList(this NetworkReader reader) + { + int length = reader.ReadInt(); + if (length < 0) + return null; + List result = new List(length); + for (int i = 0; i < length; i++) + { + result.Add(reader.Read()); + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] ReadArray(this NetworkReader reader) + { + int length = reader.ReadInt(); + + // we write -1 for null + if (length < 0) + return null; + + // todo throw an exception for other negative values (we never write them, likely to be attacker) + + // this assumes that a reader for T reads at least 1 bytes + // we can't know the exact size of T because it could have a user created reader + // NOTE: don't add to length as it could overflow if value is int.max + if (length > reader.Length - reader.Position) + { + throw new EndOfStreamException($"Received array that is too large: {length}"); + } + + T[] result = new T[length]; + for (int i = 0; i < length; i++) + { + result[i] = reader.Read(); + } + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Uri ReadUri(this NetworkReader reader) + { + string uriString = reader.ReadString(); + return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Texture2D ReadTexture2D(this NetworkReader reader) + { + Texture2D texture2D = new Texture2D(32, 32); + texture2D.SetPixels32(reader.Read()); + texture2D.Apply(); + return texture2D; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Sprite ReadSprite(this NetworkReader reader) + { + return Sprite.Create(reader.ReadTexture2D(), reader.ReadRect(), reader.ReadVector2()); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta b/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta new file mode 100644 index 0000000..66536c9 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 364a9f7ccd5541e19aa2ae0b81f0b3cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs b/Assets/Mirror/Runtime/NetworkReaderPool.cs new file mode 100644 index 0000000..ebbfac5 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderPool.cs @@ -0,0 +1,59 @@ +// API consistent with Microsoft's ObjectPool. +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + /// Pool of NetworkReaders to avoid allocations. + public static class NetworkReaderPool + { + // reuse Pool + // we still wrap it in NetworkReaderPool.Get/Recyle so we can reset the + // position and array before reusing. + static readonly Pool Pool = new Pool( + // byte[] will be assigned in GetReader + () => new NetworkReaderPooled(new byte[]{}), + // initial capacity to avoid allocations in the first few frames + 1000 + ); + + // DEPRECATED 2022-03-10 + [Obsolete("GetReader() was renamed to Get()")] + public static NetworkReaderPooled GetReader(byte[] bytes) => Get(bytes); + + /// Get the next reader in the pool. If pool is empty, creates a new Reader + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkReaderPooled Get(byte[] bytes) + { + // grab from pool & set buffer + NetworkReaderPooled reader = Pool.Get(); + reader.SetBuffer(bytes); + return reader; + } + + // DEPRECATED 2022-03-10 + [Obsolete("GetReader() was renamed to Get()")] + public static NetworkReaderPooled GetReader(ArraySegment segment) => Get(segment); + + /// Get the next reader in the pool. If pool is empty, creates a new Reader + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkReaderPooled Get(ArraySegment segment) + { + // grab from pool & set buffer + NetworkReaderPooled reader = Pool.Get(); + reader.SetBuffer(segment); + return reader; + } + + // DEPRECATED 2022-03-10 + [Obsolete("Recycle() was renamed to Return()")] + public static void Recycle(NetworkReaderPooled reader) => Return(reader); + + /// Returns a reader to the pool. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(NetworkReaderPooled reader) + { + Pool.Return(reader); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta b/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta new file mode 100644 index 0000000..2c94768 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2bacff63613ad634a98f9e4d15d29dbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs b/Assets/Mirror/Runtime/NetworkReaderPooled.cs new file mode 100644 index 0000000..fcfa792 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderPooled.cs @@ -0,0 +1,22 @@ +// "NetworkReaderPooled" instead of "PooledNetworkReader" to group files, for +// easier IDE workflow and more elegant code. +using System; + +namespace Mirror +{ + [Obsolete("PooledNetworkReader was renamed to NetworkReaderPooled. It's cleaner & slightly easier to use.")] + public sealed class PooledNetworkReader : NetworkReaderPooled + { + internal PooledNetworkReader(byte[] bytes) : base(bytes) {} + internal PooledNetworkReader(ArraySegment segment) : base(segment) {} + } + + /// Pooled NetworkReader, automatically returned to pool when using 'using' + // TODO make sealed again after removing obsolete NetworkReaderPooled! + public class NetworkReaderPooled : NetworkReader, IDisposable + { + internal NetworkReaderPooled(byte[] bytes) : base(bytes) {} + internal NetworkReaderPooled(ArraySegment segment) : base(segment) {} + public void Dispose() => NetworkReaderPool.Return(this); + } +} diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta b/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta new file mode 100644 index 0000000..4eb6e9d --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: faafa97c32e44adf8e8888de817a370a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs new file mode 100644 index 0000000..45d00c4 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -0,0 +1,1732 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mirror.RemoteCalls; +using UnityEngine; + +namespace Mirror +{ + /// NetworkServer handles remote connections and has a local connection for a local client. + public static class NetworkServer + { + static bool initialized; + public static int maxConnections; + + /// Connection to host mode client (if any) + public static NetworkConnectionToClient localConnection { get; private set; } + + /// True is a local client is currently active on the server + public static bool localClientActive => localConnection != null; + + /// Dictionary of all server connections, with connectionId as key + public static Dictionary connections = + new Dictionary(); + + /// Message Handlers dictionary, with mesageId as key + internal static Dictionary handlers = + new Dictionary(); + + /// All spawned NetworkIdentities by netId. + // server sees ALL spawned ones. + public static readonly Dictionary spawned = + new Dictionary(); + + /// Single player mode can use dontListen to not accept incoming connections + // see also: https://github.com/vis2k/Mirror/pull/2595 + public static bool dontListen; + + /// active checks if the server has been started + public static bool active { get; internal set; } + + // scene loading + public static bool isLoadingScene; + + // interest management component (optional) + // by default, everyone observes everyone + public static InterestManagement aoi; + + // OnConnected / OnDisconnected used to be NetworkMessages that were + // invoked. this introduced a bug where external clients could send + // Connected/Disconnected messages over the network causing undefined + // behaviour. + // => public so that custom NetworkManagers can hook into it + public static Action OnConnectedEvent; + public static Action OnDisconnectedEvent; + public static Action OnErrorEvent; + + // initialization / shutdown /////////////////////////////////////////// + static void Initialize() + { + if (initialized) + return; + + // Debug.Log($"NetworkServer Created version {Version.Current}"); + + //Make sure connections are cleared in case any old connections references exist from previous sessions + connections.Clear(); + + // reset Interest Management so that rebuild intervals + // start at 0 when starting again. + if (aoi != null) aoi.Reset(); + + // reset NetworkTime + NetworkTime.ResetStatics(); + + Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.activeTransport' first"); + AddTransportHandlers(); + + initialized = true; + } + + static void AddTransportHandlers() + { + // += so that other systems can also hook into it (i.e. statistics) + Transport.activeTransport.OnServerConnected += OnTransportConnected; + Transport.activeTransport.OnServerDataReceived += OnTransportData; + Transport.activeTransport.OnServerDisconnected += OnTransportDisconnected; + Transport.activeTransport.OnServerError += OnError; + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) + Transport.activeTransport.OnServerConnected -= OnTransportConnected; + Transport.activeTransport.OnServerDataReceived -= OnTransportData; + Transport.activeTransport.OnServerDisconnected -= OnTransportDisconnected; + Transport.activeTransport.OnServerError -= OnError; + } + + // calls OnStartClient for all SERVER objects in host mode once. + // client doesn't get spawn messages for those, so need to call manually. + public static void ActivateHostScene() + { + foreach (NetworkIdentity identity in spawned.Values) + { + if (!identity.isClient) + { + // Debug.Log($"ActivateHostScene {identity.netId} {identity}"); + identity.OnStartClient(); + } + } + } + + internal static void RegisterMessageHandlers() + { + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(NetworkTime.OnServerPing, false); + } + + /// Starts server and listens to incoming connections with max connections limit. + public static void Listen(int maxConns) + { + Initialize(); + maxConnections = maxConns; + + // only start server if we want to listen + if (!dontListen) + { + Transport.activeTransport.ServerStart(); + //Debug.Log("Server started listening"); + } + + active = true; + RegisterMessageHandlers(); + } + + // Note: NetworkClient.DestroyAllClientObjects does the same on client. + static void CleanupSpawned() + { + // iterate a COPY of spawned. + // DestroyObject removes them from the original collection. + // removing while iterating is not allowed. + foreach (NetworkIdentity identity in spawned.Values.ToList()) + { + if (identity != null) + { + // scene object + if (identity.sceneId != 0) + { + // spawned scene objects are unspawned and reset. + // afterwards we disable them again. + // (they always stay in the scene, we don't destroy them) + DestroyObject(identity, DestroyMode.Reset); + identity.gameObject.SetActive(false); + } + // spawned prefabs + else + { + // spawned prefabs are unspawned and destroyed. + DestroyObject(identity, DestroyMode.Destroy); + } + } + } + + spawned.Clear(); + } + + /// Shuts down the server and disconnects all clients + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Shutdown() + { + if (initialized) + { + DisconnectAll(); + + // stop the server. + // we do NOT call Transport.Shutdown, because someone only + // called NetworkServer.Shutdown. we can't assume that the + // client is supposed to be shut down too! + // + // NOTE: stop no matter what, even if 'dontListen': + // someone might enabled dontListen at runtime. + // but we still need to stop the server. + // fixes https://github.com/vis2k/Mirror/issues/2536 + Transport.activeTransport.ServerStop(); + + // transport handlers are hooked into when initializing. + // so only remove them when shutting down. + RemoveTransportHandlers(); + + initialized = false; + } + + // Reset all statics here.... + dontListen = false; + active = false; + isLoadingScene = false; + + localConnection = null; + + connections.Clear(); + connectionsCopy.Clear(); + handlers.Clear(); + newObservers.Clear(); + + // this calls spawned.Clear() + CleanupSpawned(); + + // sets nextNetworkId to 1 + // sets clientAuthorityCallback to null + // sets previousLocalPlayer to null + NetworkIdentity.ResetStatics(); + + // clear events. someone might have hooked into them before, but + // we don't want to use those hooks after Shutdown anymore. + OnConnectedEvent = null; + OnDisconnectedEvent = null; + OnErrorEvent = null; + + if (aoi != null) aoi.Reset(); + } + + // connections ///////////////////////////////////////////////////////// + /// Add a connection and setup callbacks. Returns true if not added yet. + public static bool AddConnection(NetworkConnectionToClient conn) + { + if (!connections.ContainsKey(conn.connectionId)) + { + // connection cannot be null here or conn.connectionId + // would throw NRE + connections[conn.connectionId] = conn; + return true; + } + // already a connection with this id + return false; + } + + /// Removes a connection by connectionId. Returns true if removed. + public static bool RemoveConnection(int connectionId) => + connections.Remove(connectionId); + + // called by LocalClient to add itself. don't call directly. + // TODO consider internal setter instead? + internal static void SetLocalConnection(LocalConnectionToClient conn) + { + if (localConnection != null) + { + Debug.LogError("Local Connection already exists"); + return; + } + + localConnection = conn; + } + + // removes local connection to client + internal static void RemoveLocalConnection() + { + if (localConnection != null) + { + localConnection.Disconnect(); + localConnection = null; + } + RemoveConnection(0); + } + + /// True if we have no external connections (host is allowed) + // DEPRECATED 2022-02-05 + [Obsolete("Use !HasExternalConnections() instead of NoExternalConnections() to avoid double negatives.")] + public static bool NoExternalConnections() => !HasExternalConnections(); + + /// True if we have external connections (that are not host) + public static bool HasExternalConnections() + { + // any connections? + if (connections.Count > 0) + { + // only host connection? + if (connections.Count == 1 && localConnection != null) + return false; + + // otherwise we have real external connections + return true; + } + return false; + } + + // send //////////////////////////////////////////////////////////////// + /// Send a message to all clients, even those that haven't joined the world yet (non ready) + public static void SendToAll(T message, int channelId = Channels.Reliable, bool sendToReadyOnly = false) + where T : struct, NetworkMessage + { + if (!active) + { + Debug.LogWarning("Can not send using NetworkServer.SendToAll(T msg) because NetworkServer is not active"); + return; + } + + // Debug.Log($"Server.SendToAll {typeof(T)}"); + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message only once + MessagePacking.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + // filter and then send to all internet connections at once + // -> makes code more complicated, but is HIGHLY worth it to + // avoid allocations, allow for multicast, etc. + int count = 0; + foreach (NetworkConnectionToClient conn in connections.Values) + { + if (sendToReadyOnly && !conn.isReady) + continue; + + count++; + conn.Send(segment, channelId); + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); + } + } + + /// Send a message to all clients which have joined the world (are ready). + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + public static void SendToReady(T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + if (!active) + { + Debug.LogWarning("Can not send using NetworkServer.SendToReady(T msg) because NetworkServer is not active"); + return; + } + + SendToAll(message, channelId, true); + } + + // this is like SendToReadyObservers - but it doesn't check the ready flag on the connection. + // this is used for ObjectDestroy messages. + static void SendToObservers(NetworkIdentity identity, T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + // Debug.Log($"Server.SendToObservers {typeof(T)}"); + if (identity == null || identity.observers == null || identity.observers.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message into byte[] once + MessagePacking.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + foreach (NetworkConnection conn in identity.observers.Values) + { + conn.Send(segment, channelId); + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, identity.observers.Count); + } + } + + /// Send a message to only clients which are ready with option to include the owner of the object identity + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + // Debug.Log($"Server.SendToReady {typeof(T)}"); + if (identity == null || identity.observers == null || identity.observers.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message only once + MessagePacking.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + int count = 0; + foreach (NetworkConnection conn in identity.observers.Values) + { + bool isOwner = conn == identity.connectionToClient; + if ((!isOwner || includeOwner) && conn.isReady) + { + count++; + conn.Send(segment, channelId); + } + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); + } + } + + // Deprecated 2021-09-19 + [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] + public static void SendToReady(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) + where T : struct, NetworkMessage => + SendToReadyObservers(identity, message, includeOwner, channelId); + + /// Send a message to only clients which are ready including the owner of the NetworkIdentity + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) + where T : struct, NetworkMessage + { + SendToReadyObservers(identity, message, true, channelId); + } + + // Deprecated 2021-09-19 + [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] + public static void SendToReady(NetworkIdentity identity, T message, int channelId) + where T : struct, NetworkMessage => + SendToReadyObservers(identity, message, channelId); + + // transport events //////////////////////////////////////////////////// + // called by transport + static void OnTransportConnected(int connectionId) + { + // Debug.Log($"Server accepted client:{connectionId}"); + + // connectionId needs to be != 0 because 0 is reserved for local player + // note that some transports like kcp generate connectionId by + // hashing which can be < 0 as well, so we need to allow < 0! + if (connectionId == 0) + { + Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); + Transport.activeTransport.ServerDisconnect(connectionId); + return; + } + + // connectionId not in use yet? + if (connections.ContainsKey(connectionId)) + { + Transport.activeTransport.ServerDisconnect(connectionId); + // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); + return; + } + + // are more connections allowed? if not, kick + // (it's easier to handle this in Mirror, so Transports can have + // less code and third party transport might not do that anyway) + // (this way we could also send a custom 'tooFull' message later, + // Transport can't do that) + if (connections.Count < maxConnections) + { + // add connection + NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId); + OnConnected(conn); + } + else + { + // kick + Transport.activeTransport.ServerDisconnect(connectionId); + // Debug.Log($"Server full, kicked client {connectionId}"); + } + } + + internal static void OnConnected(NetworkConnectionToClient conn) + { + // Debug.Log($"Server accepted client:{conn}"); + + // add connection and invoke connected event + AddConnection(conn); + OnConnectedEvent?.Invoke(conn); + } + + static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) + { + if (MessagePacking.Unpack(reader, out ushort msgType)) + { + // try to invoke the handler for that message + if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) + { + handler.Invoke(connection, reader, channelId); + connection.lastMessageTime = Time.time; + return true; + } + else + { + // message in a batch are NOT length prefixed to save bandwidth. + // every message needs to be handled and read until the end. + // otherwise it would overlap into the next message. + // => need to warn and disconnect to avoid undefined behaviour. + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"Unknown message id: {msgType} for connection: {connection}. This can happen if no handler was registered for this message."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + else + { + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"Invalid message header for connection: {connection}."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + + // called by transport + internal static void OnTransportData(int connectionId, ArraySegment data, int channelId) + { + if (connections.TryGetValue(connectionId, out NetworkConnectionToClient connection)) + { + // client might batch multiple messages into one packet. + // feed it to the Unbatcher. + // NOTE: we don't need to associate a channelId because we + // always process all messages in the batch. + if (!connection.unbatcher.AddBatch(data)) + { + Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)"); + connection.Disconnect(); + return; + } + + // process all messages in the batch. + // only while NOT loading a scene. + // if we get a scene change message, then we need to stop + // processing. otherwise we might apply them to the old scene. + // => fixes https://github.com/vis2k/Mirror/issues/2651 + // + // NOTE: if scene starts loading, then the rest of the batch + // would only be processed when OnTransportData is called + // the next time. + // => consider moving processing to NetworkEarlyUpdate. + while (!isLoadingScene && + connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) + { + // enough to read at least header size? + if (reader.Remaining >= MessagePacking.HeaderSize) + { + // make remoteTimeStamp available to the user + connection.remoteTimeStamp = remoteTimestamp; + + // handle message + if (!UnpackAndInvoke(connection, reader, channelId)) + { + // warn, disconnect and return if failed + // -> warning because attackers might send random data + // -> messages in a batch aren't length prefixed. + // failing to read one would cause undefined + // behaviour for every message afterwards. + // so we need to disconnect. + // -> return to avoid the below unbatches.count error. + // we already disconnected and handled it. + Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); + connection.Disconnect(); + return; + } + } + // otherwise disconnect + else + { + // WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}"); + connection.Disconnect(); + return; + } + } + + // if we weren't interrupted by a scene change, + // then all batched messages should have been processed now. + // otherwise batches would silently grow. + // we need to log an error to avoid debugging hell. + // + // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882 + // -> UnpackAndInvoke silently returned because no handler for id + // -> Reader would never be read past the end + // -> Batch would never be retired because end is never reached + // + // NOTE: prefixing every message in a batch with a length would + // avoid ever not reading to the end. for extra bandwidth. + // + // IMPORTANT: always keep this check to detect memory leaks. + // this took half a day to debug last time. + if (!isLoadingScene && connection.unbatcher.BatchesCount > 0) + { + Debug.LogError($"Still had {connection.unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end."); + } + } + else Debug.LogError($"HandleData Unknown connectionId:{connectionId}"); + } + + // called by transport + // IMPORTANT: often times when disconnecting, we call this from Mirror + // too because we want to remove the connection and handle + // the disconnect immediately. + // => which is fine as long as we guarantee it only runs once + // => which we do by removing the connection! + internal static void OnTransportDisconnected(int connectionId) + { + // Debug.Log($"Server disconnect client:{connectionId}"); + if (connections.TryGetValue(connectionId, out NetworkConnectionToClient conn)) + { + RemoveConnection(connectionId); + // Debug.Log($"Server lost client:{connectionId}"); + + // NetworkManager hooks into OnDisconnectedEvent to make + // DestroyPlayerForConnection(conn) optional, e.g. for PvP MMOs + // where players shouldn't be able to escape combat instantly. + if (OnDisconnectedEvent != null) + { + OnDisconnectedEvent.Invoke(conn); + } + // if nobody hooked into it, then simply call DestroyPlayerForConnection + else + { + DestroyPlayerForConnection(conn); + } + } + } + + static void OnError(int connectionId, Exception exception) + { + Debug.LogException(exception); + // try get connection. passes null otherwise. + connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); + OnErrorEvent?.Invoke(conn, exception); + } + + // message handlers //////////////////////////////////////////////////// + /// Register a handler for message type T. Most should require authentication. + // TODO obsolete this some day to always use the channelId version. + // all handlers in this version are wrapped with 1 extra action. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + } + + /// Register a handler for message type T. Most should require authentication. + // This version passes channelId to the handler. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); + } + + /// Unregister a handler for a message type T. + public static void UnregisterHandler() + where T : struct, NetworkMessage + { + ushort msgType = MessagePacking.GetId(); + handlers.Remove(msgType); + } + + /// Clears all registered message handlers. + public static void ClearHandlers() => handlers.Clear(); + + internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) + { + identity = go.GetComponent(); + if (identity == null) + { + Debug.LogError($"GameObject {go.name} doesn't have NetworkIdentity."); + return false; + } + return true; + } + + // disconnect ////////////////////////////////////////////////////////// + /// Disconnect all connections, including the local connection. + // synchronous: handles disconnect events and cleans up fully before returning! + public static void DisconnectAll() + { + // disconnect and remove all connections. + // we can not use foreach here because if + // conn.Disconnect -> Transport.ServerDisconnect calls + // OnDisconnect -> NetworkServer.OnDisconnect(connectionId) + // immediately then OnDisconnect would remove the connection while + // we are iterating here. + // see also: https://github.com/vis2k/Mirror/issues/2357 + // this whole process should be simplified some day. + // until then, let's copy .Values to avoid InvalidOperatinException. + // note that this is only called when stopping the server, so the + // copy is no performance problem. + foreach (NetworkConnectionToClient conn in connections.Values.ToList()) + { + // disconnect via connection->transport + conn.Disconnect(); + + // we want this function to be synchronous: handle disconnect + // events and clean up fully before returning. + // -> OnTransportDisconnected can safely be called without + // waiting for the Transport's callback. + // -> it has checks to only run once. + + // call OnDisconnected unless local player in host mod + // TODO unnecessary check? + if (conn.connectionId != NetworkConnection.LocalConnectionId) + OnTransportDisconnected(conn.connectionId); + } + + // cleanup + connections.Clear(); + localConnection = null; + active = false; + } + + // add/remove/replace player /////////////////////////////////////////// + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player) + { + NetworkIdentity identity = player.GetComponent(); + if (identity == null) + { + Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); + return false; + } + + // cannot have a player object in "Add" version + if (conn.identity != null) + { + Debug.Log("AddPlayer: player object already exists"); + return false; + } + + // make sure we have a controller before we call SetClientReady + // because the observers will be rebuilt only if we have a controller + conn.identity = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.SetClientOwner(conn); + + // special case, we are in host mode, set hasAuthority to true so that all overrides see it + if (conn is LocalConnectionToClient) + { + identity.hasAuthority = true; + NetworkClient.InternalAddPlayer(identity); + } + + // set ready if not set yet + SetClientReady(conn); + + // Debug.Log($"Adding new playerGameObject object netId: {identity.netId} asset ID: {identity.assetId}"); + + Respawn(identity); + return true; + } + + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, player); + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can + // safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) + { + NetworkIdentity identity = player.GetComponent(); + if (identity == null) + { + Debug.LogError($"ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); + return false; + } + + if (identity.connectionToClient != null && identity.connectionToClient != conn) + { + Debug.LogError($"Cannot replace player for connection. New player is already owned by a different connection{player}"); + return false; + } + + //NOTE: there can be an existing player + //Debug.Log("NetworkServer ReplacePlayer"); + + NetworkIdentity previousPlayer = conn.identity; + + conn.identity = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.SetClientOwner(conn); + + // special case, we are in host mode, set hasAuthority to true so that all overrides see it + if (conn is LocalConnectionToClient) + { + identity.hasAuthority = true; + NetworkClient.InternalAddPlayer(identity); + } + + // add connection to observers AFTER the playerController was set. + // by definition, there is nothing to observe if there is no player + // controller. + // + // IMPORTANT: do this in AddPlayerForConnection & ReplacePlayerForConnection! + SpawnObserversForConnection(conn); + + //Debug.Log($"Replacing playerGameObject object netId:{player.GetComponent().netId} asset ID {player.GetComponent().assetId}"); + + Respawn(identity); + + if (keepAuthority) + { + // This needs to be sent to clear isLocalPlayer on + // client while keeping hasAuthority true + SendChangeOwnerMessage(previousPlayer, conn); + } + else + { + // This clears both isLocalPlayer and hasAuthority on client + previousPlayer.RemoveClientAuthority(); + } + + return true; + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can + // safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId, bool keepAuthority = false) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return ReplacePlayerForConnection(conn, player, keepAuthority); + } + + // ready /////////////////////////////////////////////////////////////// + /// Flags client connection as ready (=joined world). + // When a client has signaled that it is ready, this method tells the + // server that the client is ready to receive spawned objects and state + // synchronization updates. This is usually called in a handler for the + // SYSTEM_READY message. If there is not specific action a game needs to + // take for this message, relying on the default ready handler function + // is probably fine, so this call wont be needed. + public static void SetClientReady(NetworkConnectionToClient conn) + { + // Debug.Log($"SetClientReadyInternal for conn:{conn}"); + + // set ready + conn.isReady = true; + + // client is ready to start spawning objects + if (conn.identity != null) + SpawnObserversForConnection(conn); + } + + /// Marks the client of the connection to be not-ready. + // Clients that are not ready do not receive spawned objects or state + // synchronization updates. They client can be made ready again by + // calling SetClientReady(). + public static void SetClientNotReady(NetworkConnectionToClient conn) + { + conn.isReady = false; + conn.RemoveFromObservingsObservers(); + conn.Send(new NotReadyMessage()); + } + + /// Marks all connected clients as no longer ready. + // All clients will no longer be sent state synchronization updates. The + // player's clients can call ClientManager.Ready() again to re-enter the + // ready state. This is useful when switching scenes. + public static void SetAllClientsNotReady() + { + foreach (NetworkConnectionToClient conn in connections.Values) + { + SetClientNotReady(conn); + } + } + + // default ready handler. + static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) + { + // Debug.Log($"Default handler for ready message from {conn}"); + SetClientReady(conn); + } + + // show / hide for connection ////////////////////////////////////////// + internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) + { + if (conn.isReady) + SendSpawnMessage(identity, conn); + } + + internal static void HideForConnection(NetworkIdentity identity, NetworkConnection conn) + { + ObjectHideMessage msg = new ObjectHideMessage + { + netId = identity.netId + }; + conn.Send(msg); + } + + /// Removes the player object from the connection + // destroyServerObject: Indicates whether the server object should be destroyed + public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) + { + if (conn.identity != null) + { + if (destroyServerObject) + Destroy(conn.identity.gameObject); + else + UnSpawn(conn.identity.gameObject); + + conn.identity = null; + } + //else Debug.Log($"Connection {conn} has no identity"); + } + + // remote calls //////////////////////////////////////////////////////// + // Handle command from specific player, this could be one of multiple + // players on a single client + static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) + { + if (!conn.isReady) + { + // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. + // Ignore commands that may have been in flight before client received NotReadyMessage message. + // Unreliable messages may be out of order, so don't spam warnings for those. + if (channelId == Channels.Reliable) + Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + // over reliable channel, commands should always come after spawn. + // over unreliable, they might come in before the object was spawned. + // for example, NetworkTransform. + // let's not spam the console for unreliable out of order messages. + if (channelId == Channels.Reliable) + Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); + return; + } + + // Commands can be for player objects, OR other objects with client-authority + // -> so if this connection's controller has a different netId then + // only allow the command if clientAuthorityOwner + bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); + if (requiresAuthority && identity.connectionToClient != conn) + { + Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); + return; + } + + // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); + + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn as NetworkConnectionToClient); + } + + // spawning //////////////////////////////////////////////////////////// + static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) + { + // Only call OnSerializeAllSafely if there are NetworkBehaviours + if (identity.NetworkBehaviours.Length == 0) + { + return default; + } + + // serialize all components with initialState = true + // (can be null if has none) + identity.OnSerializeAllSafely(true, ownerWriter, observersWriter); + + // convert to ArraySegment to avoid reader allocations + // if nothing was written, .ToArraySegment returns an empty segment. + ArraySegment ownerSegment = ownerWriter.ToArraySegment(); + ArraySegment observersSegment = observersWriter.ToArraySegment(); + + // use owner segment if 'conn' owns this identity, otherwise + // use observers segment + ArraySegment payload = isOwner ? ownerSegment : observersSegment; + + return payload; + } + + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) + { + if (identity.serverOnly) return; + + //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}"); + + // one writer for owner, one for observers + using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get()) + { + bool isOwner = identity.connectionToClient == conn; + ArraySegment payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter); + SpawnMessage message = new SpawnMessage + { + netId = identity.netId, + isLocalPlayer = conn.identity == identity, + isOwner = isOwner, + sceneId = identity.sceneId, + assetId = identity.assetId, + // use local values for VR support + position = identity.transform.localPosition, + rotation = identity.transform.localRotation, + scale = identity.transform.localScale, + payload = payload + }; + conn.Send(message); + } + } + + internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) + { + // Don't send if identity isn't spawned or only exists on server + if (identity.netId == 0 || identity.serverOnly) return; + + // Don't send if conn doesn't have the identity spawned yet + // May be excluded from the client by interest management + if (!conn.observing.Contains(identity)) return; + + //Debug.Log($"Server SendChangeOwnerMessage: name={identity.name} netid={identity.netId}"); + + conn.Send(new ChangeOwnerMessage + { + netId = identity.netId, + isOwner = identity.connectionToClient == conn, + isLocalPlayer = conn.identity == identity + }); + } + + static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) + { + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); + return; + } + + if (!active) + { + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); + return; + } + + NetworkIdentity identity = obj.GetComponent(); + if (identity == null) + { + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); + return; + } + + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; + } + + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.hasAuthority = true; + + identity.OnStartServer(); + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + RebuildObservers(identity, true); + } + + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) + { + SpawnObject(obj, ownerConnection); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, GameObject ownerPlayer) + { + NetworkIdentity identity = ownerPlayer.GetComponent(); + if (identity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (identity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(obj, identity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) + { + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + SpawnObject(obj, ownerConnection); + } + + internal static bool ValidateSceneObject(NetworkIdentity identity) + { + if (identity.gameObject.hideFlags == HideFlags.NotEditable || + identity.gameObject.hideFlags == HideFlags.HideAndDontSave) + return false; + +#if UNITY_EDITOR + if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) + return false; +#endif + + // If not a scene object + return identity.sceneId != 0; + } + + /// Spawns NetworkIdentities in the scene on the server. + // NetworkIdentity objects in a scene are disabled by default. Calling + // SpawnObjects() causes these scene objects to be enabled and spawned. + // It is like calling NetworkServer.Spawn() for each of them. + public static bool SpawnObjects() + { + // only if server active + if (!active) + return false; + + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); + + // first pass: activate all scene objects + foreach (NetworkIdentity identity in identities) + { + if (ValidateSceneObject(identity)) + { + // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); + identity.gameObject.SetActive(true); + + // fix https://github.com/vis2k/Mirror/issues/2778: + // -> SetActive(true) does NOT call Awake() if the parent + // is inactive + // -> we need Awake() to initialize NetworkBehaviours[] etc. + // because our second pass below spawns and works with it + // => detect this situation and manually call Awake for + // proper initialization + if (!identity.gameObject.activeInHierarchy) + identity.Awake(); + } + } + + // second pass: spawn all scene objects + foreach (NetworkIdentity identity in identities) + { + if (ValidateSceneObject(identity)) + // pass connection so that authority is not lost when server loads a scene + // https://github.com/vis2k/Mirror/pull/2987 + Spawn(identity.gameObject, identity.connectionToClient); + } + + return true; + } + + static void Respawn(NetworkIdentity identity) + { + if (identity.netId == 0) + { + // If the object has not been spawned, then do a full spawn and update observers + Spawn(identity.gameObject, identity.connectionToClient); + } + else + { + // otherwise just replace his data + SendSpawnMessage(identity, identity.connectionToClient); + } + } + + static void SpawnObserversForConnection(NetworkConnectionToClient conn) + { + //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + + if (!conn.isReady) + { + // client needs to finish initializing before we can spawn objects + // otherwise it would not find them. + return; + } + + // let connection know that we are about to start spawning... + conn.Send(new ObjectSpawnStartedMessage()); + + // add connection to each nearby NetworkIdentity's observers, which + // internally sends a spawn message for each one to the connection. + foreach (NetworkIdentity identity in spawned.Values) + { + // try with far away ones in ummorpg! + if (identity.gameObject.activeSelf) //TODO this is different + { + //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); + + // we need to support three cases: + // - legacy system (identity has .visibility) + // - new system (networkserver has .aoi) + // - default case: no .visibility and no .aoi means add all + // connections by default) + // + // ForceHidden/ForceShown overwrite all systems so check it + // first! + + // ForceShown: add no matter what + if (identity.visible == Visibility.ForceShown) + { + identity.AddObserver(conn); + } + // ForceHidden: don't show no matter what + else if (identity.visible == Visibility.ForceHidden) + { + // do nothing + } + // default: legacy system / new system / no system support + else if (identity.visible == Visibility.Default) + { + // aoi system + if (aoi != null) + { + // call OnCheckObserver + if (aoi.OnCheckObserver(identity, conn)) + identity.AddObserver(conn); + } + // no system: add all observers by default + else + { + identity.AddObserver(conn); + } + } + } + } + + // let connection know that we finished spawning, so it can call + // OnStartClient on each one (only after all were spawned, which + // is how Unity's Start() function works too) + conn.Send(new ObjectSpawnFinishedMessage()); + } + + /// This takes an object that has been spawned and un-spawns it. + // The object will be removed from clients that it was spawned on, or + // the custom spawn handler function on the client will be called for + // the object. + // Unlike when calling NetworkServer.Destroy(), on the server the object + // will NOT be destroyed. This allows the server to re-use the object, + // even spawn it again later. + public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); + + // destroy ///////////////////////////////////////////////////////////// + /// Destroys all of the connection's owned objects on the server. + // This is used when a client disconnects, to remove the players for + // that client. This also destroys non-player objects that have client + // authority set for this connection. + public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) + { + // destroy all objects owned by this connection, including the player object + conn.DestroyOwnedObjects(); + // remove connection from all of its observing entities observers + // fixes https://github.com/vis2k/Mirror/issues/2737 + // -> cleaning those up in NetworkConnection.Disconnect is NOT enough + // because voluntary disconnects from the other end don't call + // NetworkConnectionn.Disconnect() + conn.RemoveFromObservingsObservers(); + conn.identity = null; + } + + // sometimes we want to GameObject.Destroy it. + // sometimes we want to just unspawn on clients and .Reset() it on server. + // => 'bool destroy' isn't obvious enough. it's really destroy OR reset! + enum DestroyMode { Destroy, Reset } + + static void DestroyObject(NetworkIdentity identity, DestroyMode mode) + { + // Debug.Log($"DestroyObject instance:{identity.netId}"); + + // only call OnRebuildObservers while active, + // not while shutting down + // (https://github.com/vis2k/Mirror/issues/2977) + if (active && aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnDestroyed(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + // remove from NetworkServer (this) dictionary + spawned.Remove(identity.netId); + + identity.connectionToClient?.RemoveOwnedObject(identity); + + // send object destroy message to all observers, clear observers + SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); + identity.ClearObservers(); + + // in host mode, call OnStopClient/OnStopLocalPlayer manually + if (NetworkClient.active && localClientActive) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + // The object may have been spawned with host client ownership, + // e.g. a pet so we need to clear hasAuthority and call + // NotifyAuthority which invokes OnStopAuthority if hasAuthority. + identity.hasAuthority = false; + identity.NotifyAuthority(); + + // remove from NetworkClient dictionary + NetworkClient.spawned.Remove(identity.netId); + } + + // we are on the server. call OnStopServer. + identity.OnStopServer(); + + // are we supposed to GameObject.Destroy() it completely? + if (mode == DestroyMode.Destroy) + { + identity.destroyCalled = true; + + // Destroy if application is running + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(identity.gameObject); + } + // Destroy can't be used in Editor during tests. use DestroyImmediate. + else + { + GameObject.DestroyImmediate(identity.gameObject); + } + } + // otherwise simply .Reset() and set inactive again + else if (mode == DestroyMode.Reset) + { + identity.Reset(); + } + } + + static void DestroyObject(GameObject obj, DestroyMode mode) + { + if (obj == null) + { + Debug.Log("NetworkServer DestroyObject is null"); + return; + } + + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + DestroyObject(identity, mode); + } + } + + /// Destroys this object and corresponding objects on all clients. + // In some cases it is useful to remove an object but not delete it on + // the server. For that, use NetworkServer.UnSpawn() instead of + // NetworkServer.Destroy(). + public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); + + // interest management ///////////////////////////////////////////////// + // Helper function to add all server connections as observers. + // This is used if none of the components provides their own + // OnRebuildObservers function. + internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) + { + // add all server connections + foreach (NetworkConnectionToClient conn in connections.Values) + { + // only if authenticated (don't send to people during logins) + if (conn.isReady) + identity.AddObserver(conn); + } + + // add local host connection (if any) + if (localConnection != null && localConnection.isReady) + { + identity.AddObserver(localConnection); + } + } + + // allocate newObservers helper HashSet only once + // internal for tests + internal static readonly HashSet newObservers = new HashSet(); + + // rebuild observers default method (no AOI) - adds all connections + static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) + { + // only add all connections when rebuilding the first time. + // second time we just keep them without rebuilding anything. + if (initialize) + { + // not force hidden? + if (identity.visible != Visibility.ForceHidden) + { + AddAllReadyServerConnectionsToObservers(identity); + } + } + } + + // rebuild observers via interest management system + static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) + { + // clear newObservers hashset before using it + newObservers.Clear(); + + // not force hidden? + if (identity.visible != Visibility.ForceHidden) + { + aoi.OnRebuildObservers(identity, newObservers); + } + + // IMPORTANT: AFTER rebuilding add own player connection in any case + // to ensure player always sees himself no matter what. + // -> OnRebuildObservers might clear observers, so we need to add + // the player's own connection AFTER. 100% fail safe. + // -> fixes https://github.com/vis2k/Mirror/issues/692 where a + // player might teleport out of the ProximityChecker's cast, + // losing the own connection as observer. + if (identity.connectionToClient != null) + { + newObservers.Add(identity.connectionToClient); + } + + bool changed = false; + + // add all newObservers that aren't in .observers yet + foreach (NetworkConnectionToClient conn in newObservers) + { + // only add ready connections. + // otherwise the player might not be in the world yet or anymore + if (conn != null && conn.isReady) + { + if (initialize || !identity.observers.ContainsKey(conn.connectionId)) + { + // new observer + conn.AddToObserving(identity); + // Debug.Log($"New Observer for {gameObject} {conn}"); + changed = true; + } + } + } + + // remove all old .observers that aren't in newObservers anymore + foreach (NetworkConnectionToClient conn in identity.observers.Values) + { + if (!newObservers.Contains(conn)) + { + // removed observer + conn.RemoveFromObserving(identity, false); + // Debug.Log($"Removed Observer for {gameObjec} {conn}"); + changed = true; + } + } + + // copy new observers to observers + if (changed) + { + identity.observers.Clear(); + foreach (NetworkConnectionToClient conn in newObservers) + { + if (conn != null && conn.isReady) + identity.observers.Add(conn.connectionId, conn); + } + } + + // special case for host mode: we use SetHostVisibility to hide + // NetworkIdentities that aren't in observer range from host. + // this is what games like Dota/Counter-Strike do too, where a host + // does NOT see all players by default. they are in memory, but + // hidden to the host player. + // + // this code is from UNET, it's a bit strange but it works: + // * it hides newly connected identities in host mode + // => that part was the intended behaviour + // * it hides ALL NetworkIdentities in host mode when the host + // connects but hasn't selected a character yet + // => this only works because we have no .localConnection != null + // check. at this stage, localConnection is null because + // StartHost starts the server first, then calls this code, + // then starts the client and sets .localConnection. so we can + // NOT add a null check without breaking host visibility here. + // * it hides ALL NetworkIdentities in server-only mode because + // observers never contain the 'null' .localConnection + // => that was not intended, but let's keep it as it is so we + // don't break anything in host mode. it's way easier than + // iterating all identities in a special function in StartHost. + if (initialize) + { + if (!newObservers.Contains(localConnection)) + { + if (aoi != null) + aoi.SetHostVisibility(identity, false); + } + } + } + + // RebuildObservers does a local rebuild for the NetworkIdentity. + // This causes the set of players that can see this object to be rebuild. + // + // IMPORTANT: + // => global rebuild would be more simple, BUT + // => local rebuild is way faster for spawn/despawn because we can + // simply rebuild a select NetworkIdentity only + // => having both .observers and .observing is necessary for local + // rebuilds + // + // in other words, this is the perfect solution even though it's not + // completely simple (due to .observers & .observing) + // + // Mirror maintains .observing automatically in the background. best of + // both worlds without any worrying now! + public static void RebuildObservers(NetworkIdentity identity, bool initialize) + { + // observers are null until OnStartServer creates them + if (identity.observers == null) + return; + + // if there is no interest management system, + // or if 'force shown' then add all connections + if (aoi == null || identity.visible == Visibility.ForceShown) + { + RebuildObserversDefault(identity, initialize); + } + // otherwise let interest management system rebuild + else + { + RebuildObserversCustom(identity, initialize); + } + } + + // broadcasting //////////////////////////////////////////////////////// + // helper function to get the right serialization for a connection + static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + { + // get serialization for this entity (cached) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + NetworkIdentitySerialization serialization = identity.GetSerializationAtTick(Time.frameCount); + + // is this entity owned by this connection? + bool owned = identity.connectionToClient == connection; + + // send serialized data + // owner writer if owned + if (owned) + { + // was it dirty / did we actually serialize anything? + if (serialization.ownerWriter.Position > 0) + return serialization.ownerWriter; + } + // observers writer if not owned + else + { + // was it dirty / did we actually serialize anything? + if (serialization.observersWriter.Position > 0) + return serialization.observersWriter; + } + + // nothing was serialized + return null; + } + + // helper function to broadcast the world to a connection + static void BroadcastToConnection(NetworkConnectionToClient connection) + { + // for each entity that this connection is seeing + foreach (NetworkIdentity identity in connection.observing) + { + // make sure it's not null or destroyed. + // (which can happen if someone uses + // GameObject.Destroy instead of + // NetworkServer.Destroy) + if (identity != null) + { + // get serialization for this entity viewed by this connection + // (if anything was serialized this time) + NetworkWriter serialization = GetEntitySerializationForConnection(identity, connection); + if (serialization != null) + { + EntityStateMessage message = new EntityStateMessage + { + netId = identity.netId, + payload = serialization.ToArraySegment() + }; + connection.Send(message); + } + } + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + // if it does have null then someone used + // GameObject.Destroy instead of NetworkServer.Destroy. + else Debug.LogWarning($"Found 'null' entry in observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); + } + } + + // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate + // (we add this to the UnityEngine in NetworkLoop) + // internal for tests + internal static readonly List connectionsCopy = + new List(); + + static void Broadcast() + { + // copy all connections into a helper collection so that + // OnTransportDisconnected can be called while iterating. + // -> OnTransportDisconnected removes from the collection + // -> which would throw 'can't modify while iterating' errors + // => see also: https://github.com/vis2k/Mirror/issues/2739 + // (copy nonalloc) + // TODO remove this when we move to 'lite' transports with only + // socket send/recv later. + connectionsCopy.Clear(); + connections.Values.CopyTo(connectionsCopy); + + // go through all connections + foreach (NetworkConnectionToClient connection in connectionsCopy) + { + // has this connection joined the world yet? + // for each READY connection: + // pull in UpdateVarsMessage for each entity it observes + if (connection.isReady) + { + // broadcast world state to this connection + BroadcastToConnection(connection); + } + + // update connection to flush out batched messages + connection.Update(); + } + + // TODO this is way too slow because we iterate ALL spawned :/ + // TODO this is way too complicated :/ + // to understand what this tries to prevent, consider this example: + // monster has health=100 + // we change health=200, dirty bit is set + // player comes in range, gets full serialization spawn packet. + // next Broadcast(), player gets the health=200 change because dirty bit was set. + // + // this code clears all dirty bits if no players are around to prevent it. + // BUT there are two issues: + // 1. what if a playerB was around the whole time? + // 2. why don't we handle broadcast and spawn packets both HERE? + // handling spawn separately is why we need this complex magic + // + // see test: DirtyBitsAreClearedForSpawnedWithoutObservers() + // see test: SyncObjectChanges_DontGrowWithoutObservers() + // + // PAUL: we also do this to avoid ever growing SyncList .changes + //ClearSpawnedDirtyBits(); + // + // this was moved to NetworkIdentity.AddObserver! + // same result, but no more O(N) loop in here! + // TODO remove this comment after moving spawning into Broadcast()! + } + + // update ////////////////////////////////////////////////////////////// + // NetworkEarlyUpdate called before any Update/FixedUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkEarlyUpdate() + { + // process all incoming messages first before updating the world + if (Transport.activeTransport != null) + Transport.activeTransport.ServerEarlyUpdate(); + } + + internal static void NetworkLateUpdate() + { + // only broadcast world if active + if (active) + Broadcast(); + + // process all outgoing messages after updating the world + // (even if not active. still want to process disconnects etc.) + if (Transport.activeTransport != null) + Transport.activeTransport.ServerLateUpdate(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkServer.cs.meta b/Assets/Mirror/Runtime/NetworkServer.cs.meta new file mode 100644 index 0000000..9861342 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5f5ec068f5604c32b160bc49ee97b75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs b/Assets/Mirror/Runtime/NetworkStartPosition.cs new file mode 100644 index 0000000..e11029b --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkStartPosition.cs @@ -0,0 +1,21 @@ +using UnityEngine; + +namespace Mirror +{ + /// Start position for player spawning, automatically registers itself in the NetworkManager. + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Start Position")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-start-position")] + public class NetworkStartPosition : MonoBehaviour + { + public void Awake() + { + NetworkManager.RegisterStartPosition(transform); + } + + public void OnDestroy() + { + NetworkManager.UnRegisterStartPosition(transform); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta b/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta new file mode 100644 index 0000000..ae9ab89 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41f84591ce72545258ea98cb7518d8b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkTime.cs b/Assets/Mirror/Runtime/NetworkTime.cs new file mode 100644 index 0000000..1721524 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkTime.cs @@ -0,0 +1,159 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +#if !UNITY_2020_3_OR_NEWER +using Stopwatch = System.Diagnostics.Stopwatch; +#endif + +namespace Mirror +{ + /// Synchronizes server time to clients. + public static class NetworkTime + { + /// Ping message frequency, used to calculate network time and RTT + public static float PingFrequency = 2.0f; + + /// Average out the last few results from Ping + public static int PingWindowSize = 10; + + static double lastPingTime; + + static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(10); + static ExponentialMovingAverage _offset = new ExponentialMovingAverage(10); + + // the true offset guaranteed to be in this range + static double offsetMin = double.MinValue; + static double offsetMax = double.MaxValue; + + /// Returns double precision clock time _in this system_, unaffected by the network. +#if UNITY_2020_3_OR_NEWER + public static double localTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Time.timeAsDouble; + } +#else + // need stopwatch for older Unity versions, but it's quite slow. + // CAREFUL: unlike Time.time, this is not a FRAME time. + // it changes during the frame too. + static readonly Stopwatch stopwatch = new Stopwatch(); + static NetworkTime() => stopwatch.Start(); + public static double localTime => stopwatch.Elapsed.TotalSeconds; +#endif + + /// The time in seconds since the server started. + // + // I measured the accuracy of float and I got this: + // for the same day, accuracy is better than 1 ms + // after 1 day, accuracy goes down to 7 ms + // after 10 days, accuracy is 61 ms + // after 30 days , accuracy is 238 ms + // after 60 days, accuracy is 454 ms + // in other words, if the server is running for 2 months, + // and you cast down to float, then the time will jump in 0.4s intervals. + // + // TODO consider using Unbatcher's remoteTime for NetworkTime + public static double time + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => localTime - _offset.Value; + } + + /// Time measurement variance. The higher, the less accurate the time is. + // TODO does this need to be public? user should only need NetworkTime.time + public static double timeVariance => _offset.Var; + + /// Time standard deviation. The highe, the less accurate the time is. + // TODO does this need to be public? user should only need NetworkTime.time + public static double timeStandardDeviation => Math.Sqrt(timeVariance); + + /// Clock difference in seconds between the client and the server. Always 0 on server. + public static double offset => _offset.Value; + + /// Round trip time (in seconds) that it takes a message to go client->server->client. + public static double rtt => _rtt.Value; + + /// Round trip time variance. The higher, the less accurate the rtt is. + // TODO does this need to be public? user should only need NetworkTime.time + public static double rttVariance => _rtt.Var; + + /// Round trip time standard deviation. The higher, the less accurate the rtt is. + // TODO does this need to be public? user should only need NetworkTime.time + public static double rttStandardDeviation => Math.Sqrt(rttVariance); + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [UnityEngine.RuntimeInitializeOnLoadMethod] + public static void ResetStatics() + { + PingFrequency = 2.0f; + PingWindowSize = 10; + lastPingTime = 0; + _rtt = new ExponentialMovingAverage(PingWindowSize); + _offset = new ExponentialMovingAverage(PingWindowSize); + offsetMin = double.MinValue; + offsetMax = double.MaxValue; +#if !UNITY_2020_3_OR_NEWER + stopwatch.Restart(); +#endif + } + + internal static void UpdateClient() + { + // localTime (double) instead of Time.time for accuracy over days + if (localTime - lastPingTime >= PingFrequency) + { + NetworkPingMessage pingMessage = new NetworkPingMessage(localTime); + NetworkClient.Send(pingMessage, Channels.Unreliable); + lastPingTime = localTime; + } + } + + // executed at the server when we receive a ping message + // reply with a pong containing the time from the client + // and time from the server + internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message) + { + // Debug.Log($"OnPingServerMessage conn:{conn}"); + NetworkPongMessage pongMessage = new NetworkPongMessage + { + clientTime = message.clientTime, + serverTime = localTime + }; + conn.Send(pongMessage, Channels.Unreliable); + } + + // Executed at the client when we receive a Pong message + // find out how long it took since we sent the Ping + // and update time offset + internal static void OnClientPong(NetworkPongMessage message) + { + double now = localTime; + + // how long did this message take to come back + double newRtt = now - message.clientTime; + _rtt.Add(newRtt); + + // the difference in time between the client and the server + // but subtract half of the rtt to compensate for latency + // half of rtt is the best approximation we have + double newOffset = now - newRtt * 0.5f - message.serverTime; + + double newOffsetMin = now - newRtt - message.serverTime; + double newOffsetMax = now - message.serverTime; + offsetMin = Math.Max(offsetMin, newOffsetMin); + offsetMax = Math.Min(offsetMax, newOffsetMax); + + if (_offset.Value < offsetMin || _offset.Value > offsetMax) + { + // the old offset was offrange, throw it away and use new one + _offset = new ExponentialMovingAverage(PingWindowSize); + _offset.Add(newOffset); + } + else if (newOffset >= offsetMin || newOffset <= offsetMax) + { + // new offset looks reasonable, add to the average + _offset.Add(newOffset); + } + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkTime.cs.meta b/Assets/Mirror/Runtime/NetworkTime.cs.meta new file mode 100644 index 0000000..1dc9e0a --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkTime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09a0c241fc4a5496dbf4a0ab6e9a312c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs b/Assets/Mirror/Runtime/NetworkWriter.cs new file mode 100644 index 0000000..442075f --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriter.cs @@ -0,0 +1,187 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace Mirror +{ + /// Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool.GetReader() to avoid allocations. + public class NetworkWriter + { + public const int MaxStringLength = 1024 * 32; + + // create writer immediately with it's own buffer so no one can mess with it and so that we can resize it. + // note: BinaryWriter allocates too much, so we only use a MemoryStream + // => 1500 bytes by default because on average, most packets will be <= MTU + byte[] buffer = new byte[1500]; + + /// Next position to write to the buffer + public int Position; + + /// Reset both the position and length of the stream + // Leaves the capacity the same so that we can reuse this writer without + // extra allocations + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + Position = 0; + } + + // NOTE that our runtime resizing comes at no extra cost because: + // 1. 'has space' checks are necessary even for fixed sized writers. + // 2. all writers will eventually be large enough to stop resizing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnsureCapacity(int value) + { + if (buffer.Length < value) + { + int capacity = Math.Max(value, buffer.Length * 2); + Array.Resize(ref buffer, capacity); + } + } + + /// Copies buffer until 'Position' to a new array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte[] ToArray() + { + byte[] data = new byte[Position]; + Array.ConstrainedCopy(buffer, 0, data, 0, Position); + return data; + } + + /// Returns allocation-free ArraySegment until 'Position'. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment ToArraySegment() + { + return new ArraySegment(buffer, 0, Position); + } + + // WriteBlittable from DOTSNET. + // this is extremely fast, but only works for blittable types. + // + // Benchmark: + // WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2018 LTS (debug mode) + // + // | Median | Min | Max | Avg | Std | (ms) + // before | 30.35 | 29.86 | 48.99 | 32.54 | 4.93 | + // blittable* | 5.69 | 5.52 | 27.51 | 7.78 | 5.65 | + // + // * without IsBlittable check + // => 4-6x faster! + // + // WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2020.1 (release mode) + // + // | Median | Min | Max | Avg | Std | (ms) + // before | 9.41 | 8.90 | 23.02 | 10.72 | 3.07 | + // blittable* | 1.48 | 1.40 | 16.03 | 2.60 | 2.71 | + // + // * without IsBlittable check + // => 6x faster! + // + // Note: + // WriteBlittable assumes same endianness for server & client. + // All Unity 2018+ platforms are little endian. + // => run NetworkWriterTests.BlittableOnThisPlatform() to verify! + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void WriteBlittable(T value) + where T : unmanaged + { + // check if blittable for safety +#if UNITY_EDITOR + if (!UnsafeUtility.IsBlittable(typeof(T))) + { + Debug.LogError($"{typeof(T)} is not blittable!"); + return; + } +#endif + // calculate size + // sizeof(T) gets the managed size at compile time. + // Marshal.SizeOf gets the unmanaged size at runtime (slow). + // => our 1mio writes benchmark is 6x slower with Marshal.SizeOf + // => for blittable types, sizeof(T) is even recommended: + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices + int size = sizeof(T); + + // ensure capacity + // NOTE that our runtime resizing comes at no extra cost because: + // 1. 'has space' checks are necessary even for fixed sized writers. + // 2. all writers will eventually be large enough to stop resizing. + EnsureCapacity(Position + size); + + // write blittable + fixed (byte* ptr = &buffer[Position]) + { +#if UNITY_ANDROID + // on some android systems, assigning *(T*)ptr throws a NRE if + // the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.). + // here we have to use memcpy. + // + // => we can't get a pointer of a struct in C# without + // marshalling allocations + // => instead, we stack allocate an array of type T and use that + // => stackalloc avoids GC and is very fast. it only works for + // value types, but all blittable types are anyway. + // + // this way, we can still support blittable reads on android. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // (solution discovered by AIIO, FakeByte, mischa) + T* valueBuffer = stackalloc T[1]{value}; + UnsafeUtility.MemCpy(ptr, valueBuffer, size); +#else + // cast buffer to T* pointer, then assign value to the area + *(T*)ptr = value; +#endif + } + Position += size; + } + + // blittable'?' template for code reuse + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteBlittableNullable(T? value) + where T : unmanaged + { + // bool isn't blittable. write as byte. + WriteByte((byte)(value.HasValue ? 0x01 : 0x00)); + + // only write value if exists. saves bandwidth. + if (value.HasValue) + WriteBlittable(value.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(byte value) => WriteBlittable(value); + + // for byte arrays with consistent size, where the reader knows how many to read + // (like a packet opcode that's always the same) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(byte[] buffer, int offset, int count) + { + EnsureCapacity(Position + count); + Array.ConstrainedCopy(buffer, offset, this.buffer, Position, count); + Position += count; + } + + /// Writes any type that mirror supports. Uses weaver populated Writer(T).write. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(T value) + { + Action writeDelegate = Writer.write; + if (writeDelegate == null) + { + Debug.LogError($"No writer found for {typeof(T)}. This happens either if you are missing a NetworkWriter extension for your custom type, or if weaving failed. Try to reimport a script to weave again."); + } + else + { + writeDelegate(this, value); + } + } + } + + /// Helper class that weaver populates with all writer types. + // Note that c# creates a different static variable for each type + // -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it + public static class Writer + { + public static Action write; + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs.meta b/Assets/Mirror/Runtime/NetworkWriter.cs.meta new file mode 100644 index 0000000..c938496 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 48d2207bcef1f4477b624725f075f9bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs b/Assets/Mirror/Runtime/NetworkWriterExtensions.cs new file mode 100644 index 0000000..cf0954e --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterExtensions.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // Mirror's Weaver automatically detects all NetworkWriter function types, + // but they do all need to be extensions. + public static class NetworkWriterExtensions + { + // cache encoding instead of creating it with BinaryWriter each time + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + static readonly byte[] stringBuffer = new byte[NetworkWriter.MaxStringLength]; + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value); + + // char is not blittable. convert to ushort. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value); + + // bool is not blittable. convert to byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); + + [StructLayout(LayoutKind.Explicit)] + internal struct UIntDouble + { + [FieldOffset(0)] + public double doubleValue; + + [FieldOffset(0)] + public ulong longValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteDouble(this NetworkWriter writer, double value) + { + // DEBUG: try to find the exact value that fails. + //UIntDouble convert = new UIntDouble{doubleValue = value}; + //Debug.Log($"=> NetworkWriter.WriteDouble: {value} => 0x{convert.longValue:X8}"); + + + writer.WriteBlittable(value); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteString(this NetworkWriter writer, string value) + { + // write 0 for null support, increment real size by 1 + // (note: original HLAPI would write "" for null strings, but if a + // string is null on the server then it should also be null + // on the client) + if (value == null) + { + writer.WriteUShort(0); + return; + } + + // write string with same method as NetworkReader + // convert to byte[] + int size = encoding.GetBytes(value, 0, value.Length, stringBuffer, 0); + + // check if within max size + if (size >= NetworkWriter.MaxStringLength) + { + throw new IndexOutOfRangeException($"NetworkWriter.Write(string) too long: {size}. Limit: {NetworkWriter.MaxStringLength}"); + } + + // write size and bytes + writer.WriteUShort(checked((ushort)(size + 1))); + writer.WriteBytes(stringBuffer, 0, size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBytesAndSizeSegment(this NetworkWriter writer, ArraySegment buffer) + { + writer.WriteBytesAndSize(buffer.Array, buffer.Offset, buffer.Count); + } + + // Weaver needs a write function with just one byte[] parameter + // (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer) + { + // buffer might be null, so we can't use .Length in that case + writer.WriteBytesAndSize(buffer, 0, buffer != null ? buffer.Length : 0); + } + + // for byte arrays with dynamic size, where the reader doesn't know how many will come + // (like an inventory with different items etc.) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count) + { + // null is supported because [SyncVar]s might be structs with null byte[] arrays + // write 0 for null array, increment normal size by 1 to save bandwidth + // (using size=-1 for null would limit max size to 32kb instead of 64kb) + if (buffer == null) + { + writer.WriteUInt(0u); + return; + } + writer.WriteUInt(checked((uint)count) + 1u); + writer.WriteBytes(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteArraySegment(this NetworkWriter writer, ArraySegment segment) + { + int length = segment.Count; + writer.WriteInt(length); + for (int i = 0; i < length; i++) + { + writer.Write(segment.Array[segment.Offset + i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteRect(this NetworkWriter writer, Rect value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteRectNullable(this NetworkWriter writer, Rect? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WritePlane(this NetworkWriter writer, Plane value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteRay(this NetworkWriter writer, Ray value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteRayNullable(this NetworkWriter writer, Ray? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteGuid(this NetworkWriter writer, Guid value) + { + byte[] data = value.ToByteArray(); + writer.WriteBytes(data, 0, data.Length); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteGuidNullable(this NetworkWriter writer, Guid? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteGuid(value.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // users might try to use unspawned / prefab GameObjects in + // rpcs/cmds/syncvars/messages. they would be null on the other + // end, and it might not be obvious why. let's make it obvious. + // https://github.com/vis2k/Mirror/issues/2060 + // + // => warning (instead of exception) because we also use a warning + // if a GameObject doesn't have a NetworkIdentity component etc. + if (value.netId == 0) + Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc."); + + writer.WriteUInt(value.netId); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + writer.WriteUInt(value.netId); + writer.WriteByte((byte)value.ComponentIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteTransform(this NetworkWriter writer, Transform value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + NetworkIdentity identity = value.GetComponent(); + if (identity != null) + { + writer.WriteUInt(identity.netId); + } + else + { + Debug.LogWarning($"NetworkWriter {value} has no NetworkIdentity"); + writer.WriteUInt(0); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteGameObject(this NetworkWriter writer, GameObject value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // warn if the GameObject doesn't have a NetworkIdentity, + NetworkIdentity identity = value.GetComponent(); + if (identity == null) + Debug.LogWarning($"NetworkWriter {value} has no NetworkIdentity"); + + // serialize the correct amount of data in any case to make sure + // that the other end can read the expected amount of data too. + writer.WriteNetworkIdentity(identity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteList(this NetworkWriter writer, List list) + { + if (list is null) + { + writer.WriteInt(-1); + return; + } + writer.WriteInt(list.Count); + for (int i = 0; i < list.Count; i++) + writer.Write(list[i]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteArray(this NetworkWriter writer, T[] array) + { + if (array is null) + { + writer.WriteInt(-1); + return; + } + writer.WriteInt(array.Length); + for (int i = 0; i < array.Length; i++) + writer.Write(array[i]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUri(this NetworkWriter writer, Uri uri) + { + writer.WriteString(uri?.ToString()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D) + { + writer.WriteArray(texture2D.GetPixels32()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSprite(this NetworkWriter writer, Sprite sprite) + { + writer.WriteTexture2D(sprite.texture); + writer.WriteRect(sprite.rect); + writer.WriteVector2(sprite.pivot); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta b/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta new file mode 100644 index 0000000..9bbdaf0 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94259792df2a404892c3e2377f58d0cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs b/Assets/Mirror/Runtime/NetworkWriterPool.cs new file mode 100644 index 0000000..c63323d --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterPool.cs @@ -0,0 +1,47 @@ +// API consistent with Microsoft's ObjectPool. +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + /// Pool of NetworkWriters to avoid allocations. + public static class NetworkWriterPool + { + // reuse Pool + // we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the + // position before reusing. + // this is also more consistent with NetworkReaderPool where we need to + // assign the internal buffer before reusing. + static readonly Pool Pool = new Pool( + () => new NetworkWriterPooled(), + // initial capacity to avoid allocations in the first few frames + // 1000 * 1200 bytes = around 1 MB. + 1000 + ); + + // DEPRECATED 2022-03-10 + [Obsolete("GetWriter() was renamed to Get()")] + public static NetworkWriterPooled GetWriter() => Get(); + + /// Get a writer from the pool. Creates new one if pool is empty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkWriterPooled Get() + { + // grab from pool & reset position + NetworkWriterPooled writer = Pool.Get(); + writer.Reset(); + return writer; + } + + // DEPRECATED 2022-03-10 + [Obsolete("Recycle() was renamed to Return()")] + public static void Recycle(NetworkWriterPooled writer) => Return(writer); + + /// Return a writer to the pool. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(NetworkWriterPooled writer) + { + Pool.Return(writer); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta b/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta new file mode 100644 index 0000000..19d2bb7 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f34b53bea38e4f259eb8dc211e4fdb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs b/Assets/Mirror/Runtime/NetworkWriterPooled.cs new file mode 100644 index 0000000..ce113bc --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterPooled.cs @@ -0,0 +1,17 @@ +// "NetworkWriterPooled" instead of "PooledNetworkWriter" to group files, for +// easier IDE workflow and more elegant code. +using System; + +namespace Mirror +{ + // DEPRECATED 2022-03-10 + [Obsolete("PooledNetworkWriter was renamed to NetworkWriterPooled. It's cleaner & slightly easier to use.")] + public sealed class PooledNetworkWriter : NetworkWriterPooled {} + + /// Pooled NetworkWriter, automatically returned to pool when using 'using' + // TODO make sealed again after removing obsolete NetworkWriterPooled! + public class NetworkWriterPooled : NetworkWriter, IDisposable + { + public void Dispose() => NetworkWriterPool.Return(this); + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta b/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta new file mode 100644 index 0000000..5571d6f --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9fab936bf3c4716a452d94ad5ecbebe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Pool.cs b/Assets/Mirror/Runtime/Pool.cs new file mode 100644 index 0000000..e526139 --- /dev/null +++ b/Assets/Mirror/Runtime/Pool.cs @@ -0,0 +1,43 @@ +// Pool to avoid allocations (from libuv2k) +// API consistent with Microsoft's ObjectPool. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public class Pool + { + // Mirror is single threaded, no need for concurrent collections + readonly Stack objects = new Stack(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + public Pool(Func objectGenerator, int initialCapacity) + { + this.objectGenerator = objectGenerator; + + // allocate an initial pool so we have fewer (if any) + // allocations in the first few frames (or seconds). + for (int i = 0; i < initialCapacity; ++i) + objects.Push(objectGenerator()); + } + + // DEPRECATED 2022-03-10 + [Obsolete("Take() was renamed to Get()")] + public T Take() => Get(); + + // take an element from the pool, or create a new one if empty + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Get() => objects.Count > 0 ? objects.Pop() : objectGenerator(); + + // return an element to the pool + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Return(T item) => objects.Push(item); + + // count to see how many objects are in the pool. useful for tests. + public int Count => objects.Count; + } +} diff --git a/Assets/Mirror/Runtime/Pool.cs.meta b/Assets/Mirror/Runtime/Pool.cs.meta new file mode 100644 index 0000000..7d12a20 --- /dev/null +++ b/Assets/Mirror/Runtime/Pool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 845bb05fa349344c3811022f4f15dfbc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs b/Assets/Mirror/Runtime/RemoteCalls.cs new file mode 100644 index 0000000..127e241 --- /dev/null +++ b/Assets/Mirror/Runtime/RemoteCalls.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror.RemoteCalls +{ + // invoke type for Cmd/Rpc + public enum RemoteCallType { Command, ClientRpc } + + // remote call function delegate + public delegate void RemoteCallDelegate(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection); + + class Invoker + { + // GameObjects might have multiple components of TypeA.CommandA(). + // when invoking, we check if 'TypeA' is an instance of the type. + // the hash itself isn't enough because we wouldn't know which component + // to invoke it on if there are multiple of the same type. + public Type componentType; + public RemoteCallType callType; + public RemoteCallDelegate function; + public bool cmdRequiresAuthority; + + public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate invokeFunction) => + this.componentType == componentType && + this.callType == remoteCallType && + this.function == invokeFunction; + } + + /// Used to help manage remote calls for NetworkBehaviours + public static class RemoteProcedureCalls + { + // one lookup for all remote calls. + // allows us to easily add more remote call types without duplicating code. + // note: do not clear those with [RuntimeInitializeOnLoad] + // + // IMPORTANT: cmd/rpc functions are identified via **HASHES**. + // an index would requires half the bandwidth, but introduces issues + // where static constructors are lazily called, so index order isn't + // guaranteed: + // https://github.com/vis2k/Mirror/pull/3135 + // https://github.com/vis2k/Mirror/issues/3138 + // keep the 4 byte hash for stability! + static readonly Dictionary remoteCallDelegates = new Dictionary(); + + static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, int functionHash) + { + if (remoteCallDelegates.ContainsKey(functionHash)) + { + // something already registered this hash. + // it's okay if it was the same function. + Invoker oldInvoker = remoteCallDelegates[functionHash]; + if (oldInvoker.AreEqual(componentType, remoteCallType, func)) + { + return true; + } + + // otherwise notify user. there is a rare chance of string + // hash collisions. + Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them"); + } + + return false; + } + + // pass full function name to avoid ClassA.Func & ClassB.Func collisions + internal static int RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) + { + // type+func so Inventory.RpcUse != Equipment.RpcUse + int hash = functionFullName.GetStableHashCode(); + + if (CheckIfDelegateExists(componentType, remoteCallType, func, hash)) + return hash; + + remoteCallDelegates[hash] = new Invoker + { + callType = remoteCallType, + componentType = componentType, + function = func, + cmdRequiresAuthority = cmdRequiresAuthority + }; + return hash; + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + // need to pass componentType to support invoking on GameObjects with + // multiple components of same type with same remote call. + public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) => + RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority); + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + // need to pass componentType to support invoking on GameObjects with + // multiple components of same type with same remote call. + public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) => + RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func); + + // to clean up tests + internal static void RemoveDelegate(int hash) => + remoteCallDelegates.Remove(hash); + + // note: no need to throw an error if not found. + // an attacker might just try to call a cmd with an rpc's hash etc. + // returning false is enough. + static bool GetInvokerForHash(int functionHash, RemoteCallType remoteCallType, out Invoker invoker) => + remoteCallDelegates.TryGetValue(functionHash, out invoker) && + invoker != null && + invoker.callType == remoteCallType; + + // InvokeCmd/Rpc Delegate can all use the same function here + internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) + { + // IMPORTANT: we check if the message's componentIndex component is + // actually of the right type. prevents attackers trying + // to invoke remote calls on wrong components. + if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) && + invoker.componentType.IsInstanceOfType(component)) + { + // invoke function on this component + invoker.function(component, reader, senderConnection); + return true; + } + return false; + } + + // check if the command 'requiresAuthority' which is set in the attribute + internal static bool CommandRequiresAuthority(int cmdHash) => + GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) && + invoker.cmdRequiresAuthority; + + /// Gets the handler function by hash. Useful for profilers and debuggers. + public static RemoteCallDelegate GetDelegate(int functionHash) => + remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) + ? invoker.function + : null; + } +} + diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs.meta b/Assets/Mirror/Runtime/RemoteCalls.cs.meta new file mode 100644 index 0000000..7bbc087 --- /dev/null +++ b/Assets/Mirror/Runtime/RemoteCalls.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f50cefa9e65db5f4f85c893b9661c6f0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation.meta b/Assets/Mirror/Runtime/SnapshotInterpolation.meta new file mode 100644 index 0000000..85ac9a9 --- /dev/null +++ b/Assets/Mirror/Runtime/SnapshotInterpolation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4468e736f87964eaebb9d55fc3e132f7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs new file mode 100644 index 0000000..fbf2c24 --- /dev/null +++ b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs @@ -0,0 +1,20 @@ +// Snapshot interface so we can reuse it for all kinds of systems. +// for example, NetworkTransform, NetworkRigidbody, CharacterController etc. +// NOTE: we use '' and 'where T : Snapshot' to avoid boxing. +// List would cause allocations through boxing. +namespace Mirror +{ + public interface Snapshot + { + // snapshots have two timestamps: + // -> the remote timestamp (when it was sent by the remote) + // used to interpolate. + // -> the local timestamp (when we received it) + // used to know if the first two snapshots are old enough to start. + // + // IMPORTANT: the timestamp does _NOT_ need to be sent over the + // network. simply get it from batching. + double remoteTimestamp { get; set; } + double localTimestamp { get; set; } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta new file mode 100644 index 0000000..24eedd7 --- /dev/null +++ b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12afea28fdb94154868a0a3b7a9df55b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs new file mode 100644 index 0000000..bc685d7 --- /dev/null +++ b/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs @@ -0,0 +1,325 @@ +// snapshot interpolation algorithms only, +// independent from Unity/NetworkTransform/MonoBehaviour/Mirror/etc. +// the goal is to remove all the magic from it. +// => a standalone snapshot interpolation algorithm +// => that can be simulated with unit tests easily +// +// BOXING: in C#, uses does not box! passing the interface would box! +using System; +using System.Collections.Generic; + +namespace Mirror +{ + public static class SnapshotInterpolation + { + // insert into snapshot buffer if newer than first entry + // this should ALWAYS be used when inserting into a snapshot buffer! + public static void InsertIfNewEnough(T snapshot, SortedList buffer) + where T : Snapshot + { + // we need to drop any snapshot which is older ('<=') + // the snapshots we are already working with. + double timestamp = snapshot.remoteTimestamp; + + // if size == 1, then only add snapshots that are newer. + // for example, a snapshot before the first one might have been + // lagging. + if (buffer.Count == 1 && + timestamp <= buffer.Values[0].remoteTimestamp) + return; + + // for size >= 2, we are already interpolating between the first two + // so only add snapshots that are newer than the second entry. + // aka the 'ACB' problem: + // if we have a snapshot A at t=0 and C at t=2, + // we start interpolating between them. + // if suddenly B at t=1 comes in unexpectely, + // we should NOT suddenly steer towards B. + if (buffer.Count >= 2 && + timestamp <= buffer.Values[1].remoteTimestamp) + return; + + // otherwise sort it into the list + // an UDP messages might arrive twice sometimes. + // SortedList throws if key already exists, so check. + if (!buffer.ContainsKey(timestamp)) + buffer.Add(timestamp, snapshot); + } + + // helper function to check if we have 'bufferTime' worth of snapshots + // to start. + // + // glenn fiedler article: + // "Now for the trick with snapshots. What we do is instead of + // immediately rendering snapshot data received is that we buffer + // snapshots for a short amount of time in an interpolation buffer. + // This interpolation buffer holds on to snapshots for a period of time + // such that you have not only the snapshot you want to render but also, + // statistically speaking, you are very likely to have the next snapshot + // as well." + // + // => 'statistically' implies that we always wait for a fixed amount + // aka LOCAL TIME has passed. + // => it does NOT imply to wait for a remoteTime span of bufferTime. + // that would not be 'statistically'. it would be 'exactly'. + public static bool HasAmountOlderThan(SortedList buffer, double threshold, int amount) + where T : Snapshot => + buffer.Count >= amount && + buffer.Values[amount - 1].localTimestamp <= threshold; + + // for convenience, hide the 'bufferTime worth of snapshots' check in an + // easy to use function. this way we can have several conditions etc. + public static bool HasEnough(SortedList buffer, double time, double bufferTime) + where T : Snapshot => + // two snapshots with local time older than threshold? + HasAmountOlderThan(buffer, time - bufferTime, 2); + + // sometimes we need to know if it's still safe to skip past the first + // snapshot. + public static bool HasEnoughWithoutFirst(SortedList buffer, double time, double bufferTime) + where T : Snapshot => + // still two snapshots with local time older than threshold if + // we remove the first one? (in other words, need three older) + HasAmountOlderThan(buffer, time - bufferTime, 3); + + // calculate catchup. + // the goal is to buffer 'bufferTime' snapshots. + // for whatever reason, we might see growing buffers. + // in which case we should speed up to avoid ever growing delay. + // -> everything after 'threshold' is multiplied by 'multiplier' + public static double CalculateCatchup(SortedList buffer, int catchupThreshold, double catchupMultiplier) + where T : Snapshot + { + // NOTE: we count ALL buffer entires > threshold as excess. + // not just the 'old enough' ones. + // if buffer keeps growing, we have to catch up no matter what. + int excess = buffer.Count - catchupThreshold; + return excess > 0 ? excess * catchupMultiplier : 0; + } + + // get first & second buffer entries and delta between them. + // helper function because we use this several times. + // => assumes at least two entries in buffer. + public static void GetFirstSecondAndDelta(SortedList buffer, out T first, out T second, out double delta) + where T : Snapshot + { + // get first & second + first = buffer.Values[0]; + second = buffer.Values[1]; + + // delta between first & second is needed a lot + delta = second.remoteTimestamp - first.remoteTimestamp; + } + + // the core snapshot interpolation algorithm. + // for a given remoteTime, interpolationTime and buffer, + // we tick the snapshot simulation once. + // => it's the same one on server and client + // => should be called every Update() depending on authority + // + // time: LOCAL time since startup in seconds. like Unity's Time.time. + // deltaTime: Time.deltaTime from Unity. parameter for easier tests. + // interpolationTime: time in interpolation. moved along deltaTime. + // between [0, delta] where delta is snapshot + // B.timestamp - A.timestamp. + // IMPORTANT: + // => we use actual time instead of a relative + // t [0,1] because overshoot is easier to handle. + // if relative t overshoots but next snapshots are + // further apart than the current ones, it's not + // obvious how to calculate it. + // => for example, if t = 3 every time we skip we would have to + // make sure to adjust the subtracted value relative to the + // skipped delta. way too complex. + // => actual time can overshoot without problems. + // we know it's always by actual time. + // bufferTime: time in seconds that we buffer snapshots. + // buffer: our buffer of snapshots. + // Compute() assumes full integrity of the snapshots. + // for example, when interpolating between A=0 and C=2, + // make sure that you don't add B=1 between A and C if that + // snapshot arrived after we already started interpolating. + // => InsertIfNewEnough needs to protect against the 'ACB' problem + // catchupThreshold: amount of buffer entries after which we start to + // accelerate to catch up. + // if 'bufferTime' is 'sendInterval * 3', then try + // a value > 3 like 6. + // catchupMultiplier: catchup by % per additional excess buffer entry + // over the amount of 'catchupThreshold'. + // Interpolate: interpolates one snapshot to another, returns the result + // T Interpolate(T from, T to, double t); + // => needs to be Func instead of a function in the Snapshot + // interface because that would require boxing. + // => make sure to only allocate that function once. + // + // returns + // 'true' if it spit out a snapshot to apply. + // 'false' means computation moved along, but nothing to apply. + public static bool Compute( + double time, + double deltaTime, + ref double interpolationTime, + double bufferTime, + SortedList buffer, + int catchupThreshold, + float catchupMultiplier, + Func Interpolate, + out T computed) + where T : Snapshot + { + // we buffer snapshots for 'bufferTime' + // for example: + // * we buffer for 3 x sendInterval = 300ms + // * the idea is to wait long enough so we at least have a few + // snapshots to interpolate between + // * we process anything older 100ms immediately + // + // IMPORTANT: snapshot timestamps are _remote_ time + // we need to interpolate and calculate buffer lifetimes based on it. + // -> we don't know remote's current time + // -> NetworkTime.time fluctuates too much, that's no good + // -> we _could_ calculate an offset when the first snapshot arrives, + // but if there was high latency then we'll always calculate time + // with high latency + // -> at any given time, we are interpolating from snapshot A to B + // => seems like A.timestamp += deltaTime is a good way to do it + + computed = default; + //Debug.Log($"{name} snapshotbuffer={buffer.Count}"); + + // do we have enough buffered to start interpolating? + if (!HasEnough(buffer, time, bufferTime)) + return false; + + // multiply deltaTime by catchup. + // for example, assuming a catch up of 50%: + // - deltaTime = 1s => 1.5s + // - deltaTime = 0.1s => 0.15s + // in other words, variations in deltaTime don't matter. + // simply multiply. that's just how time works. + // (50% catch up means 0.5, so we multiply by 1.5) + // + // if '0' catchup then we multiply by '1', which changes nothing. + // (faster branch prediction) + double catchup = CalculateCatchup(buffer, catchupThreshold, catchupMultiplier); + deltaTime *= (1 + catchup); + + // interpolationTime starts at 0 and we add deltaTime to move + // along the interpolation. + // + // ONLY while we have snapshots to interpolate. + // otherwise we might increase it to infinity which would lead + // to skipping the next snapshots entirely. + // + // IMPORTANT: interpolationTime as actual time instead of + // t [0,1] allows us to overshoot and subtract easily. + // if t was [0,1], and we overshoot by 0.1, that's a + // RELATIVE overshoot for the delta between B.time - A.time. + // => if the next C.time - B.time is not the same delta, + // then the relative overshoot would speed up or slow + // down the interpolation! CAREFUL. + // + // IMPORTANT: we NEVER add deltaTime to 'time'. + // 'time' is already NOW. that's how Unity works. + interpolationTime += deltaTime; + + // get first & second & delta + GetFirstSecondAndDelta(buffer, out T first, out T second, out double delta); + + // reached goal and have more old enough snapshots in buffer? + // then skip and move to next. + // for example, if we have snapshots at t=1,2,3 + // and we are at interpolationTime = 2.5, then + // we should skip the first one, subtract delta and interpolate + // between 2,3 instead. + // + // IMPORTANT: we only ever use old enough snapshots. + // if we wouldn't check for old enough, then we would + // move to the next one, interpolate a little bit, + // and then in next compute() wait again because it + // wasn't old enough yet. + while (interpolationTime >= delta && + HasEnoughWithoutFirst(buffer, time, bufferTime)) + { + // subtract exactly delta from interpolation time + // instead of setting to '0', where we would lose the + // overshoot part and see jitter again. + // + // IMPORTANT: subtracting delta TIME works perfectly. + // subtracting '1' from a ratio of t [0,1] would + // leave the overshoot as relative between the + // next delta. if next delta is different, then + // overshoot would be bigger than planned and + // speed up the interpolation. + interpolationTime -= delta; + //Debug.LogWarning($"{name} overshot and is now at: {interpolationTime}"); + + // remove first, get first, second & delta again after change. + buffer.RemoveAt(0); + GetFirstSecondAndDelta(buffer, out first, out second, out delta); + + // NOTE: it's worth consider spitting out all snapshots + // that we skipped, in case someone still wants to move + // along them to avoid physics collisions. + // * for NetworkTransform it's unnecessary as we always + // set transform.position, which can go anywhere. + // * for CharacterController it's worth considering + } + + // interpolationTime is actual time, NOT a 't' ratio [0,1]. + // we need 't' between [0,1] relative. + // InverseLerp calculates just that. + // InverseLerp CLAMPS between [0,1] and DOES NOT extrapolate! + // => we already skipped ahead as many as possible above. + // => we do NOT extrapolate for the reasons below. + // + // IMPORTANT: + // we should NOT extrapolate & predict while waiting for more + // snapshots as this would introduce a whole range of issues: + // * player might be extrapolated WAY out if we wait for long + // * player might be extrapolated behind walls + // * once we receive a new snapshot, we would interpolate + // not from the last valid position, but from the + // extrapolated position. this could be ANYWHERE. the + // player might get stuck in walls, etc. + // => we are NOT doing client side prediction & rollback here + // => we are simply interpolating with known, valid positions + // + // SEE TEST: Compute_Step5_OvershootWithoutEnoughSnapshots_NeverExtrapolates() + double t = Mathd.InverseLerp(first.remoteTimestamp, second.remoteTimestamp, first.remoteTimestamp + interpolationTime); + //Debug.Log($"InverseLerp({first.remoteTimestamp:F2}, {second.remoteTimestamp:F2}, {first.remoteTimestamp} + {interpolationTime:F2}) = {t:F2} snapshotbuffer={buffer.Count}"); + + // interpolate snapshot, return true to indicate we computed one + computed = Interpolate(first, second, t); + + // interpolationTime: + // overshooting is ONLY allowed for smooth transitions when + // immediately moving to the NEXT snapshot afterwards. + // + // if there is ANY break, for example: + // * reached second snapshot and waiting for more + // * reached second snapshot and next one isn't old enough yet + // + // then we SHOULD NOT overshoot because: + // * increasing interpolationTime by deltaTime while waiting + // would make it grow HUGE to 100+. + // * once we have more snapshots, we would skip most of them + // instantly instead of actually interpolating through them. + // + // in other words: cap time if we WOULDN'T have enough after removing + if (!HasEnoughWithoutFirst(buffer, time, bufferTime)) + { + // interpolationTime is always from 0..delta. + // so we cap it at delta. + // DO NOT cap it at second.remoteTimestamp. + // (that's why when interpolating the third parameter is + // first.time + interpolationTime) + // => covered with test: + // Compute_Step5_OvershootWithEnoughSnapshots_NextIsntOldEnough() + interpolationTime = Math.Min(interpolationTime, delta); + } + + return true; + } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta b/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta new file mode 100644 index 0000000..244c5fb --- /dev/null +++ b/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72c16070d85334011853813488ab1431 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs b/Assets/Mirror/Runtime/SyncDictionary.cs new file mode 100644 index 0000000..c63077c --- /dev/null +++ b/Assets/Mirror/Runtime/SyncDictionary.cs @@ -0,0 +1,310 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncIDictionary : SyncObject, IDictionary, IReadOnlyDictionary + { + public delegate void SyncDictionaryChanged(Operation op, TKey key, TValue item); + + protected readonly IDictionary objects; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncDictionaryChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_REMOVE, + OP_SET + } + + struct Change + { + internal Operation operation; + internal TKey key; + internal TValue item; + } + + // list of changes. + // -> insert/delete/clear is only ONE change + // -> changing the same slot 10x caues 10 changes. + // -> note that this grows until next sync(!) + // TODO Dictionary to avoid ever growing changes / redundant changes! + readonly List changes = new List(); + + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + public override void Reset() + { + IsReadOnly = false; + changes.Clear(); + changesAhead = 0; + objects.Clear(); + } + + public ICollection Keys => objects.Keys; + + public ICollection Values => objects.Values; + + IEnumerable IReadOnlyDictionary.Keys => objects.Keys; + + IEnumerable IReadOnlyDictionary.Values => objects.Values; + + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); + + public SyncIDictionary(IDictionary objects) + { + this.objects = objects; + } + + void AddOperation(Operation op, TKey key, TValue item) + { + if (IsReadOnly) + { + throw new System.InvalidOperationException("SyncDictionaries can only be modified by the server"); + } + + Change change = new Change + { + operation = op, + key = key, + item = item + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + Callback?.Invoke(op, key, item); + } + + public override void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WriteUInt((uint)objects.Count); + + foreach (KeyValuePair syncItem in objects) + { + writer.Write(syncItem.Key); + writer.Write(syncItem.Value); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WriteUInt((uint)changes.Count); + } + + public override void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WriteUInt((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + case Operation.OP_REMOVE: + case Operation.OP_SET: + writer.Write(change.key); + writer.Write(change.item); + break; + case Operation.OP_CLEAR: + break; + } + } + } + + public override void OnDeserializeAll(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + // if init, write the full list content + int count = (int)reader.ReadUInt(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + TKey key = reader.Read(); + TValue obj = reader.Read(); + objects.Add(key, obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadUInt(); + } + + public override void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadUInt(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + TKey key = default; + TValue item = default; + + switch (operation) + { + case Operation.OP_ADD: + case Operation.OP_SET: + key = reader.Read(); + item = reader.Read(); + if (apply) + { + objects[key] = item; + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + key = reader.Read(); + item = reader.Read(); + if (apply) + { + objects.Remove(key); + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, key, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR, default, default); + } + + public bool ContainsKey(TKey key) => objects.ContainsKey(key); + + public bool Remove(TKey key) + { + if (objects.TryGetValue(key, out TValue item) && objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, item); + return true; + } + return false; + } + + public TValue this[TKey i] + { + get => objects[i]; + set + { + if (ContainsKey(i)) + { + objects[i] = value; + AddOperation(Operation.OP_SET, i, value); + } + else + { + objects[i] = value; + AddOperation(Operation.OP_ADD, i, value); + } + } + } + + public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); + + public void Add(TKey key, TValue value) + { + objects.Add(key, value); + AddOperation(Operation.OP_ADD, key, value); + } + + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new System.ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range"); + } + if (array.Length - arrayIndex < Count) + { + throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); + } + + int i = arrayIndex; + foreach (KeyValuePair item in objects) + { + array[i] = item; + i++; + } + } + + public bool Remove(KeyValuePair item) + { + bool result = objects.Remove(item.Key); + if (result) + { + AddOperation(Operation.OP_REMOVE, item.Key, item.Value); + } + return result; + } + + public IEnumerator> GetEnumerator() => objects.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator(); + } + + public class SyncDictionary : SyncIDictionary + { + public SyncDictionary() : base(new Dictionary()) {} + public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) {} + public SyncDictionary(IDictionary d) : base(new Dictionary(d)) {} + public new Dictionary.ValueCollection Values => ((Dictionary)objects).Values; + public new Dictionary.KeyCollection Keys => ((Dictionary)objects).Keys; + public new Dictionary.Enumerator GetEnumerator() => ((Dictionary)objects).GetEnumerator(); + } +} diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs.meta b/Assets/Mirror/Runtime/SyncDictionary.cs.meta new file mode 100644 index 0000000..1c20b57 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b346c49cfdb668488a364c3023590e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncList.cs b/Assets/Mirror/Runtime/SyncList.cs new file mode 100644 index 0000000..9eb0a59 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncList.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncList : SyncObject, IList, IReadOnlyList + { + public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem); + + readonly IList objects; + readonly IEqualityComparer comparer; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncListChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_INSERT, + OP_REMOVEAT, + OP_SET + } + + struct Change + { + internal Operation operation; + internal int index; + internal T item; + } + + // list of changes. + // -> insert/delete/clear is only ONE change + // -> changing the same slot 10x caues 10 changes. + // -> note that this grows until next sync(!) + readonly List changes = new List(); + + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + public SyncList() : this(EqualityComparer.Default) {} + + public SyncList(IEqualityComparer comparer) + { + this.comparer = comparer ?? EqualityComparer.Default; + objects = new List(); + } + + public SyncList(IList objects, IEqualityComparer comparer = null) + { + this.comparer = comparer ?? EqualityComparer.Default; + this.objects = objects; + } + + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); + + public override void Reset() + { + IsReadOnly = false; + changes.Clear(); + changesAhead = 0; + objects.Clear(); + } + + void AddOperation(Operation op, int itemIndex, T oldItem, T newItem) + { + if (IsReadOnly) + { + throw new InvalidOperationException("Synclists can only be modified at the server"); + } + + Change change = new Change + { + operation = op, + index = itemIndex, + item = newItem + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + Callback?.Invoke(op, itemIndex, oldItem, newItem); + } + + public override void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WriteUInt((uint)objects.Count); + + for (int i = 0; i < objects.Count; i++) + { + T obj = objects[i]; + writer.Write(obj); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WriteUInt((uint)changes.Count); + } + + public override void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WriteUInt((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + writer.Write(change.item); + break; + + case Operation.OP_CLEAR: + break; + + case Operation.OP_REMOVEAT: + writer.WriteUInt((uint)change.index); + break; + + case Operation.OP_INSERT: + case Operation.OP_SET: + writer.WriteUInt((uint)change.index); + writer.Write(change.item); + break; + } + } + } + + public override void OnDeserializeAll(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + // if init, write the full list content + int count = (int)reader.ReadUInt(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + T obj = reader.Read(); + objects.Add(obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadUInt(); + } + + public override void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadUInt(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + int index = 0; + T oldItem = default; + T newItem = default; + + switch (operation) + { + case Operation.OP_ADD: + newItem = reader.Read(); + if (apply) + { + index = objects.Count; + objects.Add(newItem); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_INSERT: + index = (int)reader.ReadUInt(); + newItem = reader.Read(); + if (apply) + { + objects.Insert(index, newItem); + } + break; + + case Operation.OP_REMOVEAT: + index = (int)reader.ReadUInt(); + if (apply) + { + oldItem = objects[index]; + objects.RemoveAt(index); + } + break; + + case Operation.OP_SET: + index = (int)reader.ReadUInt(); + newItem = reader.Read(); + if (apply) + { + oldItem = objects[index]; + objects[index] = newItem; + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, index, oldItem, newItem); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public void Add(T item) + { + objects.Add(item); + AddOperation(Operation.OP_ADD, objects.Count - 1, default, item); + } + + public void AddRange(IEnumerable range) + { + foreach (T entry in range) + { + Add(entry); + } + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR, 0, default, default); + } + + public bool Contains(T item) => IndexOf(item) >= 0; + + public void CopyTo(T[] array, int index) => objects.CopyTo(array, index); + + public int IndexOf(T item) + { + for (int i = 0; i < objects.Count; ++i) + if (comparer.Equals(item, objects[i])) + return i; + return -1; + } + + public int FindIndex(Predicate match) + { + for (int i = 0; i < objects.Count; ++i) + if (match(objects[i])) + return i; + return -1; + } + + public T Find(Predicate match) + { + int i = FindIndex(match); + return (i != -1) ? objects[i] : default; + } + + public List FindAll(Predicate match) + { + List results = new List(); + for (int i = 0; i < objects.Count; ++i) + if (match(objects[i])) + results.Add(objects[i]); + return results; + } + + public void Insert(int index, T item) + { + objects.Insert(index, item); + AddOperation(Operation.OP_INSERT, index, default, item); + } + + public void InsertRange(int index, IEnumerable range) + { + foreach (T entry in range) + { + Insert(index, entry); + index++; + } + } + + public bool Remove(T item) + { + int index = IndexOf(item); + bool result = index >= 0; + if (result) + { + RemoveAt(index); + } + return result; + } + + public void RemoveAt(int index) + { + T oldItem = objects[index]; + objects.RemoveAt(index); + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default); + } + + public int RemoveAll(Predicate match) + { + List toRemove = new List(); + for (int i = 0; i < objects.Count; ++i) + if (match(objects[i])) + toRemove.Add(objects[i]); + + foreach (T entry in toRemove) + { + Remove(entry); + } + + return toRemove.Count; + } + + public T this[int i] + { + get => objects[i]; + set + { + if (!comparer.Equals(objects[i], value)) + { + T oldItem = objects[i]; + objects[i] = value; + AddOperation(Operation.OP_SET, i, oldItem, value); + } + } + } + + public Enumerator GetEnumerator() => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + // default Enumerator allocates. we need a custom struct Enumerator to + // not allocate on the heap. + // (System.Collections.Generic.List source code does the same) + // + // benchmark: + // uMMORPG with 800 monsters, Skills.GetHealthBonus() which runs a + // foreach on skills SyncList: + // before: 81.2KB GC per frame + // after: 0KB GC per frame + // => this is extremely important for MMO scale networking + public struct Enumerator : IEnumerator + { + readonly SyncList list; + int index; + public T Current { get; private set; } + + public Enumerator(SyncList list) + { + this.list = list; + index = -1; + Current = default; + } + + public bool MoveNext() + { + if (++index >= list.Count) + { + return false; + } + Current = list[index]; + return true; + } + + public void Reset() => index = -1; + object IEnumerator.Current => Current; + public void Dispose() {} + } + } +} diff --git a/Assets/Mirror/Runtime/SyncList.cs.meta b/Assets/Mirror/Runtime/SyncList.cs.meta new file mode 100644 index 0000000..088ef1e --- /dev/null +++ b/Assets/Mirror/Runtime/SyncList.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 744fc71f748fe40d5940e04bf42b29f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncObject.cs b/Assets/Mirror/Runtime/SyncObject.cs new file mode 100644 index 0000000..7df3b67 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncObject.cs @@ -0,0 +1,51 @@ +using System; + +namespace Mirror +{ + /// SyncObjects sync state between server and client. E.g. SyncLists. + // SyncObject should be a class (instead of an interface) for a few reasons: + // * NetworkBehaviour stores SyncObjects in a list. structs would be a copy + // and OnSerialize would use the copy instead of the original struct. + // * Obsolete functions like Flush() don't need to be defined by each type + // * OnDirty/IsRecording etc. default functions can be defined once here + // for example, handling 'OnDirty wasn't initialized' with a default + // function that throws an exception will be useful for SyncVar + public abstract class SyncObject + { + /// Used internally to set owner NetworkBehaviour's dirty mask bit when changed. + public Action OnDirty; + + /// Used internally to check if we are currently tracking changes. + // prevents ever growing .changes lists: + // if a monster has no observers but we keep modifing a SyncObject, + // then the changes would never be flushed and keep growing, + // because OnSerialize isn't called without observers. + // => Func so we can set it to () => observers.Count > 0 + // without depending on NetworkComponent/NetworkIdentity here. + // => virtual so it sipmly always records by default + public Func IsRecording = () => true; + + /// Discard all the queued changes + // Consider the object fully synchronized with clients + public abstract void ClearChanges(); + + // Deprecated 2021-09-17 + [Obsolete("Deprecated: Use ClearChanges instead.")] + public void Flush() => ClearChanges(); + + /// Write a full copy of the object + public abstract void OnSerializeAll(NetworkWriter writer); + + /// Write the changes made to the object since last sync + public abstract void OnSerializeDelta(NetworkWriter writer); + + /// Reads a full copy of the object + public abstract void OnDeserializeAll(NetworkReader reader); + + /// Reads the changes made to the object since last sync + public abstract void OnDeserializeDelta(NetworkReader reader); + + /// Resets the SyncObject so that it can be re-used + public abstract void Reset(); + } +} diff --git a/Assets/Mirror/Runtime/SyncObject.cs.meta b/Assets/Mirror/Runtime/SyncObject.cs.meta new file mode 100644 index 0000000..736c651 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae226d17a0c844041aa24cc2c023dd49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncSet.cs b/Assets/Mirror/Runtime/SyncSet.cs new file mode 100644 index 0000000..94e353f --- /dev/null +++ b/Assets/Mirror/Runtime/SyncSet.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncSet : SyncObject, ISet + { + public delegate void SyncSetChanged(Operation op, T item); + + protected readonly ISet objects; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncSetChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_REMOVE + } + + struct Change + { + internal Operation operation; + internal T item; + } + + // list of changes. + // -> insert/delete/clear is only ONE change + // -> changing the same slot 10x caues 10 changes. + // -> note that this grows until next sync(!) + // TODO Dictionary to avoid ever growing changes / redundant changes! + readonly List changes = new List(); + + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + public SyncSet(ISet objects) + { + this.objects = objects; + } + + public override void Reset() + { + IsReadOnly = false; + changes.Clear(); + changesAhead = 0; + objects.Clear(); + } + + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); + + void AddOperation(Operation op, T item) + { + if (IsReadOnly) + { + throw new InvalidOperationException("SyncSets can only be modified at the server"); + } + + Change change = new Change + { + operation = op, + item = item + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + Callback?.Invoke(op, item); + } + + void AddOperation(Operation op) => AddOperation(op, default); + + public override void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WriteUInt((uint)objects.Count); + + foreach (T obj in objects) + { + writer.Write(obj); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WriteUInt((uint)changes.Count); + } + + public override void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WriteUInt((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + writer.Write(change.item); + break; + + case Operation.OP_CLEAR: + break; + + case Operation.OP_REMOVE: + writer.Write(change.item); + break; + } + } + } + + public override void OnDeserializeAll(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + // if init, write the full list content + int count = (int)reader.ReadUInt(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + T obj = reader.Read(); + objects.Add(obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadUInt(); + } + + public override void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadUInt(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + T item = default; + + switch (operation) + { + case Operation.OP_ADD: + item = reader.Read(); + if (apply) + { + objects.Add(item); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + item = reader.Read(); + if (apply) + { + objects.Remove(item); + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public bool Add(T item) + { + if (objects.Add(item)) + { + AddOperation(Operation.OP_ADD, item); + return true; + } + return false; + } + + void ICollection.Add(T item) + { + if (objects.Add(item)) + { + AddOperation(Operation.OP_ADD, item); + } + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR); + } + + public bool Contains(T item) => objects.Contains(item); + + public void CopyTo(T[] array, int index) => objects.CopyTo(array, index); + + public bool Remove(T item) + { + if (objects.Remove(item)) + { + AddOperation(Operation.OP_REMOVE, item); + return true; + } + return false; + } + + public IEnumerator GetEnumerator() => objects.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void ExceptWith(IEnumerable other) + { + if (other == this) + { + Clear(); + return; + } + + // remove every element in other from this + foreach (T element in other) + { + Remove(element); + } + } + + public void IntersectWith(IEnumerable other) + { + if (other is ISet otherSet) + { + IntersectWithSet(otherSet); + } + else + { + HashSet otherAsSet = new HashSet(other); + IntersectWithSet(otherAsSet); + } + } + + void IntersectWithSet(ISet otherSet) + { + List elements = new List(objects); + + foreach (T element in elements) + { + if (!otherSet.Contains(element)) + { + Remove(element); + } + } + } + + public bool IsProperSubsetOf(IEnumerable other) => objects.IsProperSubsetOf(other); + + public bool IsProperSupersetOf(IEnumerable other) => objects.IsProperSupersetOf(other); + + public bool IsSubsetOf(IEnumerable other) => objects.IsSubsetOf(other); + + public bool IsSupersetOf(IEnumerable other) => objects.IsSupersetOf(other); + + public bool Overlaps(IEnumerable other) => objects.Overlaps(other); + + public bool SetEquals(IEnumerable other) => objects.SetEquals(other); + + // custom implementation so we can do our own Clear/Add/Remove for delta + public void SymmetricExceptWith(IEnumerable other) + { + if (other == this) + { + Clear(); + } + else + { + foreach (T element in other) + { + if (!Remove(element)) + { + Add(element); + } + } + } + } + + // custom implementation so we can do our own Clear/Add/Remove for delta + public void UnionWith(IEnumerable other) + { + if (other != this) + { + foreach (T element in other) + { + Add(element); + } + } + } + } + + public class SyncHashSet : SyncSet + { + public SyncHashSet() : this(EqualityComparer.Default) {} + public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) {} + + // allocation free enumerator + public new HashSet.Enumerator GetEnumerator() => ((HashSet)objects).GetEnumerator(); + } + + public class SyncSortedSet : SyncSet + { + public SyncSortedSet() : this(Comparer.Default) {} + public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) {} + + // allocation free enumerator + public new SortedSet.Enumerator GetEnumerator() => ((SortedSet)objects).GetEnumerator(); + } +} diff --git a/Assets/Mirror/Runtime/SyncSet.cs.meta b/Assets/Mirror/Runtime/SyncSet.cs.meta new file mode 100644 index 0000000..6eeef1c --- /dev/null +++ b/Assets/Mirror/Runtime/SyncSet.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a31599d9f9dd4ef9999f7b9707c832c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVar.cs b/Assets/Mirror/Runtime/SyncVar.cs new file mode 100644 index 0000000..5c1790b --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVar.cs @@ -0,0 +1,148 @@ +// SyncVar to make [SyncVar] weaving easier. +// +// we can possibly move a lot of complex logic out of weaver: +// * set dirty bit +// * calling the hook +// * hook guard in host mode +// * GameObject/NetworkIdentity internal netId storage +// +// here is the plan: +// 1. develop SyncVar along side [SyncVar] +// 2. internally replace [SyncVar]s with SyncVar +// 3. eventually obsolete [SyncVar] +// +// downsides: +// - generic types don't show in Unity Inspector +// +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // 'class' so that we can track it in SyncObjects list, and iterate it for + // de/serialization. + [Serializable] + public class SyncVar : SyncObject, IEquatable + { + // Unity 2020+ can show [SerializeField] in inspector. + // (only if SyncVar isn't readonly though) + [SerializeField] T _Value; + + // Value property with hooks + // virtual for SyncFieldNetworkIdentity netId trick etc. + public virtual T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _Value; + set + { + // only if value changed. otherwise don't dirty/hook. + // we have .Equals(T), simply reuse it here. + if (!Equals(value)) + { + // set value, set dirty bit + T old = _Value; + _Value = value; + OnDirty(); + + // Value.set calls the hook if changed. + // calling Value.set from within the hook would call the + // hook again and deadlock. prevent it with hookGuard. + // (see test: Hook_Set_DoesntDeadlock) + if (!hookGuard && + // original [SyncVar] only calls hook on clients. + // let's keep it for consistency for now + // TODO remove check & dependency in the future. + // use isClient/isServer in the hook instead. + NetworkClient.active) + { + hookGuard = true; + InvokeCallback(old, value); + hookGuard = false; + } + } + } + } + + // OnChanged Callback. + // named 'Callback' for consistency with SyncList etc. + // needs to be public so we can assign it in OnStartClient. + // (ctor passing doesn't work, it can only take static functions) + // assign via: field.Callback += ...! + public event Action Callback; + + // OnCallback is responsible for calling the callback. + // this is necessary for inheriting classes like SyncVarGameObject, + // where the netIds should be converted to GOs and call the GO hook. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void InvokeCallback(T oldValue, T newValue) => + Callback?.Invoke(oldValue, newValue); + + // Value.set calls the hook if changed. + // calling Value.set from within the hook would call the hook again and + // deadlock. prevent it with a simple 'are we inside the hook' bool. + bool hookGuard; + + public override void ClearChanges() {} + public override void Reset() {} + + // ctor from value and OnChanged hook. + // it was always called 'hook'. let's keep naming for convenience. + public SyncVar(T value) + { + // recommend explicit GameObject, NetworkIdentity, NetworkBehaviour + // with persistent netId method + if (this is SyncVar) + Debug.LogWarning($"Use explicit {nameof(SyncVarGameObject)} class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); + + if (this is SyncVar) + Debug.LogWarning($"Use explicit {nameof(SyncVarNetworkIdentity)} class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); + + if (this is SyncVar) + Debug.LogWarning($"Use explicit SyncVarNetworkBehaviour class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); + + _Value = value; + } + + // NOTE: copy ctor is unnecessary. + // SyncVars are readonly and only initialized by 'Value' once. + + // implicit conversion: int value = SyncVar + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(SyncVar field) => field.Value; + + // implicit conversion: SyncVar = value + // even if SyncVar is readonly, it's still useful: SyncVar = 1; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SyncVar(T value) => new SyncVar(value); + + // serialization (use .Value instead of _Value so hook is called!) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnSerializeAll(NetworkWriter writer) => writer.Write(Value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnSerializeDelta(NetworkWriter writer) => writer.Write(Value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnDeserializeAll(NetworkReader reader) => Value = reader.Read(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnDeserializeDelta(NetworkReader reader) => Value = reader.Read(); + + // IEquatable should compare Value. + // SyncVar should act invisibly like [SyncVar] before. + // this way we can do SyncVar health == 0 etc. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(T other) => + // from NetworkBehaviour.SyncVarEquals: + // EqualityComparer method avoids allocations. + // otherwise would have to be :IEquatable (not all structs are) + EqualityComparer.Default.Equals(Value, other); + + // ToString should show Value. + // SyncVar should act invisibly like [SyncVar] before. + public override string ToString() => Value.ToString(); + } +} diff --git a/Assets/Mirror/Runtime/SyncVar.cs.meta b/Assets/Mirror/Runtime/SyncVar.cs.meta new file mode 100644 index 0000000..fffb472 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e87cb681af8459fbbb1f467e1c7632c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs b/Assets/Mirror/Runtime/SyncVarGameObject.cs new file mode 100644 index 0000000..81961c2 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarGameObject.cs @@ -0,0 +1,145 @@ +// persistent GameObject SyncField which stores .netId internally. +// this is necessary for cases like a player's target. +// the target might run in and out of visibility range and become 'null'. +// but the 'netId' remains and will always point to the monster if around. +// +// NOTE that SyncFieldNetworkIdentity is faster (no .gameObject/GetComponent<>)! +// +// original Weaver code with netId workaround: +/* + // USER: + [SyncVar(hook = "OnTargetChanged")] + public GameObject target; + + // WEAVER: + private uint ___targetNetId; + + public GameObject Networktarget + { + get + { + return GetSyncVarGameObject(___targetNetId, ref target); + } + [param: In] + set + { + if (!NetworkBehaviour.SyncVarGameObjectEqual(value, ___targetNetId)) + { + GameObject networktarget = Networktarget; + SetSyncVarGameObject(value, ref target, 1uL, ref ___targetNetId); + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) + { + SetSyncVarHookGuard(1uL, value: true); + OnTargetChanged(networktarget, value); + SetSyncVarHookGuard(1uL, value: false); + } + } + } + } + + private void OnTargetChanged(GameObject old, GameObject value) + { + } +*/ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // SyncField only stores an uint netId. + // while providing .spawned lookup for convenience. + // NOTE: server always knows all spawned. consider caching the field again. + public class SyncVarGameObject : SyncVar + { + // .spawned lookup from netId overwrites base uint .Value + public new GameObject Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetGameObject(base.Value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => base.Value = GetNetId(value); + } + + // OnChanged Callback is for . + // Let's also have one for + public new event Action Callback; + + // overwrite CallCallback to use the GameObject version instead + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void InvokeCallback(uint oldValue, uint newValue) => + Callback?.Invoke(GetGameObject(oldValue), GetGameObject(newValue)); + + // ctor + // 'value = null' so we can do: + // SyncVarGameObject = new SyncVarGameObject() + // instead of + // SyncVarGameObject = new SyncVarGameObject(null); + public SyncVarGameObject(GameObject value = null) + : base(GetNetId(value)) {} + + // helper function to get netId from GameObject (if any) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static uint GetNetId(GameObject go) + { + if (go != null) + { + NetworkIdentity identity = go.GetComponent(); + return identity != null ? identity.netId : 0; + } + return 0; + } + + // helper function to get GameObject from netId (if spawned) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static GameObject GetGameObject(uint netId) + { + NetworkIdentity spawned = Utils.GetSpawnedInServerOrClient(netId); + return spawned != null ? spawned.gameObject : null; + } + + // implicit conversion: GameObject value = SyncFieldGameObject + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator GameObject(SyncVarGameObject field) => field.Value; + + // implicit conversion: SyncFieldGameObject = value + // even if SyncField is readonly, it's still useful: SyncFieldGameObject = target; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SyncVarGameObject(GameObject value) => new SyncVarGameObject(value); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarGameObject a, SyncVarGameObject b) => + a.Value == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarGameObject a, SyncVarGameObject b) => !(a == b); + + // NOTE: overloading all == operators blocks '== null' checks with an + // "ambiguous invocation" error. that's good. this way user code like + // "player.target == null" won't compile instead of silently failing! + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarGameObject a, GameObject b) => + a.Value == b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarGameObject a, GameObject b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(GameObject a, SyncVarGameObject b) => + a == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(GameObject a, SyncVarGameObject b) => !(a == b); + + // if we overwrite == operators, we also need to overwrite .Equals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object obj) => obj is SyncVarGameObject value && this == value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => Value.GetHashCode(); + } +} diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta b/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta new file mode 100644 index 0000000..4e924f0 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84da90dae05442e3a149753c9b25ae98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs new file mode 100644 index 0000000..623b890 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs @@ -0,0 +1,168 @@ +// persistent NetworkBehaviour SyncField which stores netId and component index. +// this is necessary for cases like a player's target. +// the target might run in and out of visibility range and become 'null'. +// but the 'netId' remains and will always point to the monster if around. +// (we also store the component index because GameObject can have multiple +// NetworkBehaviours of same type) +// +// original Weaver code was broken because it didn't store by netId. +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + // SyncField needs an uint netId and a byte componentIndex. + // we use an ulong SyncField internally to store both. + // while providing .spawned lookup for convenience. + // NOTE: server always knows all spawned. consider caching the field again. + // to support abstract NetworkBehaviour and classes inheriting from it. + // => hooks can be OnHook(Monster, Monster) instead of OnHook(NB, NB) + // => implicit cast can be to/from Monster instead of only NetworkBehaviour + // => Weaver needs explicit types for hooks too, not just OnHook(NB, NB) + public class SyncVarNetworkBehaviour : SyncVar + where T : NetworkBehaviour + { + // .spawned lookup from netId overwrites base uint .Value + public new T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ULongToNetworkBehaviour(base.Value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => base.Value = NetworkBehaviourToULong(value); + } + + // OnChanged Callback is for . + // Let's also have one for + public new event Action Callback; + + // overwrite CallCallback to use the NetworkIdentity version instead + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void InvokeCallback(ulong oldValue, ulong newValue) => + Callback?.Invoke(ULongToNetworkBehaviour(oldValue), ULongToNetworkBehaviour(newValue)); + + // ctor + // 'value = null' so we can do: + // SyncVarNetworkBehaviour = new SyncVarNetworkBehaviour() + // instead of + // SyncVarNetworkBehaviour = new SyncVarNetworkBehaviour(null); + public SyncVarNetworkBehaviour(T value = null) + : base(NetworkBehaviourToULong(value)) {} + + // implicit conversion: NetworkBehaviour value = SyncFieldNetworkBehaviour + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(SyncVarNetworkBehaviour field) => field.Value; + + // implicit conversion: SyncFieldNetworkBehaviour = value + // even if SyncField is readonly, it's still useful: SyncFieldNetworkBehaviour = target; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SyncVarNetworkBehaviour(T value) => new SyncVarNetworkBehaviour(value); + + // NOTE: overloading all == operators blocks '== null' checks with an + // "ambiguous invocation" error. that's good. this way user code like + // "player.target == null" won't compile instead of silently failing! + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarNetworkBehaviour a, SyncVarNetworkBehaviour b) => + a.Value == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarNetworkBehaviour a, SyncVarNetworkBehaviour b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarNetworkBehaviour a, NetworkBehaviour b) => + a.Value == b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarNetworkBehaviour a, NetworkBehaviour b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarNetworkBehaviour a, T b) => + a.Value == b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarNetworkBehaviour a, T b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NetworkBehaviour a, SyncVarNetworkBehaviour b) => + a == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NetworkBehaviour a, SyncVarNetworkBehaviour b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(T a, SyncVarNetworkBehaviour b) => + a == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(T a, SyncVarNetworkBehaviour b) => !(a == b); + + // if we overwrite == operators, we also need to overwrite .Equals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object obj) => obj is SyncVarNetworkBehaviour value && this == value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => Value.GetHashCode(); + + // helper functions to get/set netId, componentIndex from ulong + // netId on the 4 left bytes. compIndex on the right most byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong Pack(uint netId, byte componentIndex) => + (ulong)netId << 32 | componentIndex; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Unpack(ulong value, out uint netId, out byte componentIndex) + { + netId = (uint)(value >> 32); + componentIndex = (byte)(value & 0xFF); + } + + // helper function to find/get NetworkBehaviour to ulong (netId/compIndex) + static T ULongToNetworkBehaviour(ulong value) + { + // unpack ulong to netId, componentIndex + Unpack(value, out uint netId, out byte componentIndex); + + // find spawned NetworkIdentity by netId + NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId); + + // get the nth component + return identity != null ? (T)identity.NetworkBehaviours[componentIndex] : null; + } + + static ulong NetworkBehaviourToULong(T value) + { + // pack netId, componentIndex to ulong + return value != null ? Pack(value.netId, (byte)value.ComponentIndex) : 0; + } + + // Serialize should only write 4+1 bytes, not 8 bytes ulong + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnSerializeAll(NetworkWriter writer) + { + Unpack(base.Value, out uint netId, out byte componentIndex); + writer.WriteUInt(netId); + writer.WriteByte(componentIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnSerializeDelta(NetworkWriter writer) => + OnSerializeAll(writer); + + // Deserialize should only write 4+1 bytes, not 8 bytes ulong + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnDeserializeAll(NetworkReader reader) + { + uint netId = reader.ReadUInt(); + byte componentIndex = reader.ReadByte(); + base.Value = Pack(netId, componentIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void OnDeserializeDelta(NetworkReader reader) => + OnDeserializeAll(reader); + } +} diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta new file mode 100644 index 0000000..0ceab50 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0fff77f1a624ba8ad6e4bdef6c14a8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs new file mode 100644 index 0000000..0f3fdf2 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs @@ -0,0 +1,118 @@ +// persistent NetworkIdentity SyncField which stores .netId internally. +// this is necessary for cases like a player's target. +// the target might run in and out of visibility range and become 'null'. +// but the 'netId' remains and will always point to the monster if around. +// +// original Weaver code with netId workaround: +/* + // USER: + [SyncVar(hook = "OnTargetChanged")] + public NetworkIdentity target; + + // WEAVER GENERATED: + private uint ___targetNetId; + + public NetworkIdentity Networktarget + { + get + { + return GetSyncVarNetworkIdentity(___targetNetId, ref target); + } + [param: In] + set + { + if (!SyncVarNetworkIdentityEqual(value, ___targetNetId)) + { + NetworkIdentity networktarget = Networktarget; + SetSyncVarNetworkIdentity(value, ref target, 1uL, ref ___targetNetId); + if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) + { + SetSyncVarHookGuard(1uL, value: true); + OnTargetChanged(networktarget, value); + SetSyncVarHookGuard(1uL, value: false); + } + } + } + } +*/ +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + // SyncField only stores an uint netId. + // while providing .spawned lookup for convenience. + // NOTE: server always knows all spawned. consider caching the field again. + public class SyncVarNetworkIdentity : SyncVar + { + // .spawned lookup from netId overwrites base uint .Value + public new NetworkIdentity Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Utils.GetSpawnedInServerOrClient(base.Value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => base.Value = value != null ? value.netId : 0; + } + + // OnChanged Callback is for . + // Let's also have one for + public new event Action Callback; + + // overwrite CallCallback to use the NetworkIdentity version instead + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void InvokeCallback(uint oldValue, uint newValue) => + Callback?.Invoke(Utils.GetSpawnedInServerOrClient(oldValue), Utils.GetSpawnedInServerOrClient(newValue)); + + // ctor + // 'value = null' so we can do: + // SyncVarNetworkIdentity = new SyncVarNetworkIdentity() + // instead of + // SyncVarNetworkIdentity = new SyncVarNetworkIdentity(null); + public SyncVarNetworkIdentity(NetworkIdentity value = null) + : base(value != null ? value.netId : 0) {} + + // implicit conversion: NetworkIdentity value = SyncFieldNetworkIdentity + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator NetworkIdentity(SyncVarNetworkIdentity field) => field.Value; + + // implicit conversion: SyncFieldNetworkIdentity = value + // even if SyncField is readonly, it's still useful: SyncFieldNetworkIdentity = target; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SyncVarNetworkIdentity(NetworkIdentity value) => new SyncVarNetworkIdentity(value); + + // NOTE: overloading all == operators blocks '== null' checks with an + // "ambiguous invocation" error. that's good. this way user code like + // "player.target == null" won't compile instead of silently failing! + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarNetworkIdentity a, SyncVarNetworkIdentity b) => + a.Value == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarNetworkIdentity a, SyncVarNetworkIdentity b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SyncVarNetworkIdentity a, NetworkIdentity b) => + a.Value == b; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SyncVarNetworkIdentity a, NetworkIdentity b) => !(a == b); + + // == operator for comparisons like Player.target==monster + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(NetworkIdentity a, SyncVarNetworkIdentity b) => + a == b.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(NetworkIdentity a, SyncVarNetworkIdentity b) => !(a == b); + + // if we overwrite == operators, we also need to overwrite .Equals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object obj) => obj is SyncVarNetworkIdentity value && this == value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => Value.GetHashCode(); + } +} diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta new file mode 100644 index 0000000..53271d4 --- /dev/null +++ b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f9a6d4d2741477999ad9588261870fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transport.cs b/Assets/Mirror/Runtime/Transport.cs new file mode 100644 index 0000000..f831bf1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transport.cs @@ -0,0 +1,192 @@ +// For future reference, here is what Transports need to do in Mirror: +// +// Connecting: +// * Transports are responsible to call either OnConnected || OnDisconnected +// in a certain time after a Connect was called. It can not end in limbo. +// +// Disconnecting: +// * Connections might disconnect voluntarily by the other end. +// * Connections might be disconnect involuntarily by the server. +// * Either way, Transports need to detect it and call OnDisconnected. +// +// Timeouts: +// * Transports should expose a configurable timeout +// * Transports are responsible for calling OnDisconnected after a timeout +// +// Channels: +// * Default channel is Reliable, as in reliable ordered (OR DISCONNECT) +// * Where possible, Unreliable should be supported (unordered, no guarantee) +// +// Other: +// * Transports functions are all bound to the main thread. +// (Transports can use other threads in the background if they manage them) +// * Transports should only process messages while the component is enabled. +// +using System; +using UnityEngine; + +namespace Mirror +{ + /// Abstract transport layer component + public abstract class Transport : MonoBehaviour + { + // common ////////////////////////////////////////////////////////////// + /// The current transport used by Mirror. + public static Transport activeTransport; + + /// Is this transport available in the current platform? + public abstract bool Available(); + + // client ////////////////////////////////////////////////////////////// + /// Called by Transport when the client connected to the server. + public Action OnClientConnected; + + /// Called by Transport when the client received a message from the server. + public Action, int> OnClientDataReceived; + + /// Called by Transport when the client sent a message to the server. + // Transports are responsible for calling it because: + // - groups it together with OnReceived responsibility + // - allows transports to decide if anything was sent or not + // - allows transports to decide the actual used channel (i.e. tcp always sending reliable) + public Action, int> OnClientDataSent; + + /// Called by Transport when the client encountered an error. + public Action OnClientError; + + /// Called by Transport when the client disconnected from the server. + public Action OnClientDisconnected; + + // server ////////////////////////////////////////////////////////////// + /// Called by Transport when a new client connected to the server. + public Action OnServerConnected; + + /// Called by Transport when the server received a message from a client. + public Action, int> OnServerDataReceived; + + /// Called by Transport when the server sent a message to a client. + // Transports are responsible for calling it because: + // - groups it together with OnReceived responsibility + // - allows transports to decide if anything was sent or not + // - allows transports to decide the actual used channel (i.e. tcp always sending reliable) + public Action, int> OnServerDataSent; + + /// Called by Transport when a server's connection encountered a problem. + /// If a Disconnect will also be raised, raise the Error first. + public Action OnServerError; + + /// Called by Transport when a client disconnected from the server. + public Action OnServerDisconnected; + + // client functions //////////////////////////////////////////////////// + /// True if the client is currently connected to the server. + public abstract bool ClientConnected(); + + /// Connects the client to the server at the address. + public abstract void ClientConnect(string address); + + /// Connects the client to the server at the Uri. + public virtual void ClientConnect(Uri uri) + { + // By default, to keep backwards compatibility, just connect to the host + // in the uri + ClientConnect(uri.Host); + } + + /// Sends a message to the server over the given channel. + // The ArraySegment is only valid until returning. Copy if needed. + public abstract void ClientSend(ArraySegment segment, int channelId = Channels.Reliable); + + /// Disconnects the client from the server + public abstract void ClientDisconnect(); + + // server functions //////////////////////////////////////////////////// + /// Returns server address as Uri. + // Useful for NetworkDiscovery. + public abstract Uri ServerUri(); + + /// True if the server is currently listening for connections. + public abstract bool ServerActive(); + + /// Start listening for connections. + public abstract void ServerStart(); + + /// Send a message to a client over the given channel. + public abstract void ServerSend(int connectionId, ArraySegment segment, int channelId = Channels.Reliable); + + /// Disconnect a client from the server. + public abstract void ServerDisconnect(int connectionId); + + /// Get a client's address on the server. + // Can be useful for Game Master IP bans etc. + public abstract string ServerGetClientAddress(int connectionId); + + /// Stop listening and disconnect all connections. + public abstract void ServerStop(); + + /// Maximum message size for the given channel. + // Different channels often have different sizes, ranging from MTU to + // several megabytes. + // + // Needs to return a value at all times, even if the Transport isn't + // running or available because it's needed for initializations. + public abstract int GetMaxPacketSize(int channelId = Channels.Reliable); + + /// Recommended Batching threshold for this transport. + // Uses GetMaxPacketSize by default. + // Some transports like kcp support large max packet sizes which should + // not be used for batching all the time because they end up being too + // slow (head of line blocking etc.). + public virtual int GetBatchThreshold(int channelId = Channels.Reliable) + { + return GetMaxPacketSize(channelId); + } + + // block Update & LateUpdate to show warnings if Transports still use + // them instead of using + // Client/ServerEarlyUpdate: to process incoming messages + // Client/ServerLateUpdate: to process outgoing messages + // those are called by NetworkClient/Server at the right time. + // + // allows transports to implement the proper network update order of: + // process_incoming() + // update_world() + // process_outgoing() + // + // => see NetworkLoop.cs for detailed explanations! +#pragma warning disable UNT0001 // Empty Unity message + public void Update() {} + public void LateUpdate() {} +#pragma warning restore UNT0001 // Empty Unity message + + /// + /// NetworkLoop NetworkEarly/LateUpdate were added for a proper network + /// update order. the goal is to: + /// process_incoming() + /// update_world() + /// process_outgoing() + /// in order to avoid unnecessary latency and data races. + /// + // => split into client and server parts so that we can cleanly call + // them from NetworkClient/Server + // => VIRTUAL for now so we can take our time to convert transports + // without breaking anything. + public virtual void ClientEarlyUpdate() {} + public virtual void ServerEarlyUpdate() {} + public virtual void ClientLateUpdate() {} + public virtual void ServerLateUpdate() {} + + /// Shut down the transport, both as client and server + public abstract void Shutdown(); + + /// Called by Unity when quitting. Inheriting Transports should call base for proper Shutdown. + public virtual void OnApplicationQuit() + { + // stop transport (e.g. to shut down threads) + // (when pressing Stop in the Editor, Unity keeps threads alive + // until we press Start again. so if Transports use threads, we + // really want them to end now and not after next start) + Shutdown(); + } + } +} diff --git a/Assets/Mirror/Runtime/Transport.cs.meta b/Assets/Mirror/Runtime/Transport.cs.meta new file mode 100644 index 0000000..55072e1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfffcac25d6d64ced9de620159e221b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports.meta b/Assets/Mirror/Runtime/Transports.meta new file mode 100644 index 0000000..fc29442 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7825d46cd73fe47938869eb5427b40fa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP.meta b/Assets/Mirror/Runtime/Transports/KCP.meta new file mode 100644 index 0000000..ba9d190 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 953bb5ec5ab2346a092f58061e01ba65 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta new file mode 100644 index 0000000..dedea2f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7bdb797750d0a490684410110bf48192 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs new file mode 100644 index 0000000..026e66b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs @@ -0,0 +1,357 @@ +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Linq; +using System.Net; +using UnityEngine; +using Mirror; +using Unity.Collections; + +namespace kcp2k +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] + [DisallowMultipleComponent] + public class KcpTransport : Transport + { + // scheme used by this transport + public const string Scheme = "kcp"; + + // common + [Header("Transport Configuration")] + public ushort Port = 7777; + [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; + + [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. Enabled in normal mode, disabled in turbo mode. Disable this for high scale games if connections get choked regularly.")] + 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.")] + public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. + [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 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 use where-allocation NonAlloc KcpServer/Client/Connection versions. Highly recommended on all Unity platforms.")] + public bool NonAlloc = true; + [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.")] + public bool MaximizeSendReceiveBuffersToOSLimit = true; + + [Header("Calculated Max (based 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 + + // server & client (where-allocation NonAlloc versions) + KcpServer server; + 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 + static int FromKcpChannel(KcpChannel channel) => + channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable; + + static KcpChannel ToKcpChannel(int channel) => + channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable; + + 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; + +#if ENABLE_IL2CPP + // NonAlloc doesn't work with IL2CPP builds + NonAlloc = false; +#endif + + // client + client = NonAlloc + ? new KcpClientNonAlloc( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected.Invoke(), + (error, reason) => OnClientError.Invoke(new Exception(reason))) + : new KcpClient( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected.Invoke(), + (error, reason) => OnClientError.Invoke(new Exception(reason))); + + // server + server = NonAlloc + ? new KcpServerNonAlloc( + (connectionId) => OnServerConnected.Invoke(connectionId), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, new Exception(reason)), + DualMode, + NoDelay, + Interval, + FastResend, + CongestionWindow, + SendWindowSize, + ReceiveWindowSize, + Timeout, + MaxRetransmit, + MaximizeSendReceiveBuffersToOSLimit) + : new KcpServer( + (connectionId) => OnServerConnected.Invoke(connectionId), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, new Exception(reason)), + DualMode, + NoDelay, + Interval, + FastResend, + CongestionWindow, + SendWindowSize, + ReceiveWindowSize, + Timeout, + MaxRetransmit, + MaximizeSendReceiveBuffersToOSLimit); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + Debug.Log("KcpTransport initialized!"); + } + + void OnValidate() + { + // show max message sizes in inspector for convenience + ReliableMaxMessageSize = KcpConnection.ReliableMaxMessageSize(ReceiveWindowSize); + UnreliableMaxMessageSize = KcpConnection.UnreliableMaxMessageSize; + } + + // all except WebGL + public override bool Available() => + Application.platform != RuntimePlatform.WebGLPlayer; + + // client + public override bool ClientConnected() => client.connected; + public override void ClientConnect(string address) + { + client.Connect(address, Port, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit, MaximizeSendReceiveBuffersToOSLimit); + } + 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, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit, MaximizeSendReceiveBuffersToOSLimit); + } + public override void ClientSend(ArraySegment 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 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 != null ? endPoint.Address.ToString() : ""; + } + 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 KcpConnection.UnreliableMaxMessageSize; + default: + return KcpConnection.ReliableMaxMessageSize(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) => + KcpConnection.UnreliableMaxMessageSize; + + // 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 => (long)conn.MaxSendRate) / server.connections.Count + : 0; + public long GetAverageMaxReceiveRate() => + server.connections.Count > 0 + ? server.connections.Values.Sum(conn => (long)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"; + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + void OnGUI() + { + if (!statisticsGUI) return; + + 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.connection.MaxSendRate)}/s"); + GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s"); + GUILayout.Label($" SendQueue: {client.connection.SendQueueCount}"); + GUILayout.Label($" ReceiveQueue: {client.connection.ReceiveQueueCount}"); + GUILayout.Label($" SendBuffer: {client.connection.SendBufferCount}"); + GUILayout.Label($" ReceiveBuffer: {client.connection.ReceiveBufferCount}"); + GUILayout.EndVertical(); + } + + GUILayout.EndArea(); + } +#endif + + 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"; + Debug.Log(log); + } + + if (ClientConnected()) + { + string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; + log += $" MaxSendRate: {PrettyBytes(client.connection.MaxSendRate)}/s\n"; + log += $" MaxRecvRate: {PrettyBytes(client.connection.MaxReceiveRate)}/s\n"; + log += $" SendQueue: {client.connection.SendQueueCount}\n"; + log += $" ReceiveQueue: {client.connection.ReceiveQueueCount}\n"; + log += $" SendBuffer: {client.connection.SendBufferCount}\n"; + log += $" ReceiveBuffer: {client.connection.ReceiveBufferCount}\n\n"; + Debug.Log(log); + } + } + + public override string ToString() => "KCP"; + } +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs.meta new file mode 100644 index 0000000..f7280c8 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b0fecffa3f624585964b0d0eb21b18e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k.meta new file mode 100644 index 0000000..1dceadf --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 71a1c8e8c022d4731a481c1808f37e5d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef new file mode 100644 index 0000000..9a90c82 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef @@ -0,0 +1,15 @@ +{ + "name": "kcp2k", + "references": [ + "GUID:63c380d6dae6946209ed0832388a657c" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta new file mode 100644 index 0000000..1d70e80 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6806a62c384838046a3c66c44f06d75f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE new file mode 100644 index 0000000..c77582e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE @@ -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. \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta new file mode 100644 index 0000000..49dc767 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a3e8369060cf4e94ac117603de47aa6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION new file mode 100644 index 0000000..992fe9f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION @@ -0,0 +1,136 @@ +V1.19 [2022-05-12] +- feature: OnError ErrorCodes + +V1.18 [2022-05-08] +- feature: OnError to allow higher level to show popups etc. +- feature: KcpServer.GetClientAddress is now GetClientEndPoint in order to + expose more details +- ResolveHostname: include exception in log for easier debugging +- fix: KcpClientConnection.RawReceive now logs the SocketException even if + it was expected. makes debugging easier. +- fix: KcpServer.TickIncoming now logs the SocketException even if it was + expected. makes debugging easier. +- fix: KcpClientConnection.RawReceive now calls Disconnect() if the other end + has closed the connection. better than just remaining in a state with unusable + sockets. + +V1.17 [2022-01-09] +- perf: server/client MaximizeSendReceiveBuffersToOSLimit option to set send/recv + buffer sizes to OS limit. avoids drops due to small buffers under heavy load. + +V1.16 [2022-01-06] +- fix: SendUnreliable respects ArraySegment.Offset +- fix: potential bug with negative length (see PR #2) +- breaking: removed pause handling because it's not necessary for Mirror anymore + +V1.15 [2021-12-11] +- feature: feature: MaxRetransmits aka dead_link now configurable +- dead_link disconnect message improved to show exact retransmit count + +V1.14 [2021-11-30] +- fix: Send() now throws an exception for messages which require > 255 fragments +- fix: ReliableMaxMessageSize is now limited to messages which require <= 255 fragments + +V1.13 [2021-11-28] +- fix: perf: uncork max message size from 144 KB to as much as we want based on + receive window size. + fixes https://github.com/vis2k/kcp2k/issues/22 + fixes https://github.com/skywind3000/kcp/pull/291 +- feature: OnData now includes channel it was received on + +V1.12 [2021-07-16] +- Tests: don't depend on Unity anymore +- fix: #26 - Kcp now catches exception if host couldn't be resolved, and calls + OnDisconnected to let the user now. +- fix: KcpServer.DualMode is now configurable in the constructor instead of + using #if UNITY_SWITCH. makes it run on all other non dual mode platforms too. +- fix: where-allocation made optional via virtuals and inheriting + KcpServer/Client/Connection NonAlloc classes. fixes a bug where some platforms + might not support where-allocation. + +V1.11 rollback [2021-06-01] +- perf: Segment MemoryStream initial capacity set to MTU to avoid early runtime + resizing/allocations + +V1.10 [2021-05-28] +- feature: configurable Timeout +- allocations explained with comments (C# ReceiveFrom / IPEndPoint.GetHashCode) +- fix: #17 KcpConnection.ReceiveNextReliable now assigns message default so it + works in .net too +- fix: Segment pool is not static anymore. Each kcp instance now has it's own + Pool. fixes #18 concurrency issues + +V1.9 [2021-03-02] +- Tick() split into TickIncoming()/TickOutgoing() to use in Mirror's new update + functions. allows to minimize latency. + => original Tick() is still supported for convenience. simply processes both! + +V1.8 [2021-02-14] +- fix: Unity IPv6 errors on Nintendo Switch +- fix: KcpConnection now disconnects if data message was received without content. + previously it would call OnData with an empty ArraySegment, causing all kinds of + weird behaviour in Mirror/DOTSNET. Added tests too. +- fix: KcpConnection.SendData: don't allow sending empty messages anymore. disconnect + and log a warning to make it completely obvious. + +V1.7 [2021-01-13] +- fix: unreliable messages reset timeout now too +- perf: KcpConnection OnCheckEnabled callback changed to a simple 'paused' boolean. + This is faster than invoking a Func every time and allows us to fix #8 more + easily later by calling .Pause/.Unpause from OnEnable/OnDisable in MirrorTransport. +- fix #8: Unpause now resets timeout to fix a bug where Mirror would pause kcp, + change the scene which took >10s, then unpause and kcp would detect the lack of + any messages for >10s as timeout. Added test to make sure it never happens again. +- MirrorTransport: statistics logging for headless servers +- Mirror Transport: Send/Receive window size increased once more from 2048 to 4096. + +V1.6 [2021-01-10] +- Unreliable channel added! +- perf: KcpHeader byte added to every kcp message to indicate + Handshake/Data/Ping/Disconnect instead of scanning each message for Hello/Byte/Ping + content via SegmentEquals. It's a lot cleaner, should be faster and should avoid + edge cases where a message content would equal Hello/Ping/Bye sequence accidentally. +- Kcp.Input: offset moved to parameters for cases where it's needed +- Kcp.SetMtu from original Kcp.c + +V1.5 [2021-01-07] +- KcpConnection.MaxSend/ReceiveRate calculation based on the article +- MirrorTransport: large send/recv window size defaults to avoid high latencies caused + by packets not being processed fast enough +- MirrorTransport: show MaxSend/ReceiveRate in debug gui +- MirrorTransport: don't Log.Info to console in headless mode if debug log is disabled + +V1.4 [2020-11-27] +- fix: OnCheckEnabled added. KcpConnection message processing while loop can now + be interrupted immediately. fixes Mirror Transport scene changes which need to stop + processing any messages immediately after a scene message) +- perf: Mirror KcpTransport: FastResend enabled by default. turbo mode according to: + https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration +- perf: Mirror KcpTransport: CongestionControl disabled by default (turbo mode) + +V1.3 [2020-11-17] +- Log.Info/Warning/Error so logging doesn't depend on UnityEngine anymore +- fix: Server.Tick catches SocketException which happens if Android client is killed +- MirrorTransport: debugLog option added that can be checked in Unity Inspector +- Utils.Clamp so Kcp.cs doesn't depend on UnityEngine +- Utils.SegmentsEqual: use Linq SequenceEqual so it doesn't depend on UnityEngine +=> kcp2k can now be used in any C# project even without Unity + +V1.2 [2020-11-10] +- more tests added +- fix: raw receive buffers are now all of MTU size +- fix: raw receive detects error where buffer was too small for msgLength and + result in excess data being dropped silently +- KcpConnection.MaxMessageSize added for use in high level +- KcpConnection.MaxMessageSize increased from 1200 bytes to to maximum allowed + message size of 145KB for kcp (based on mtu, overhead, wnd_rcv) + +V1.1 [2020-10-30] +- high level cleanup, fixes, improvements + +V1.0 [2020-10-22] +- Kcp.cs now mirrors original Kcp.c behaviour + (this fixes dozens of bugs) + +V0.1 +- initial kcp-csharp based version \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta new file mode 100644 index 0000000..2a07daa --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ed3f2cf1bbf1b4d53a6f2c103d311f71 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel.meta new file mode 100644 index 0000000..1c11c3d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5a54d18b954cb4407a28b633fc32ea6d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs new file mode 100644 index 0000000..15b872f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs @@ -0,0 +1,15 @@ +// kcp specific error codes to allow for error switching, localization, +// translation to Mirror errors, etc. +namespace kcp2k +{ + public enum ErrorCode : byte + { + DnsResolve, // failed to resolve a host name + Timeout, // ping timeout or dead link + Congestion, // more messages than transport / network can process + InvalidReceive, // recv invalid packet (possibly intentional attack) + InvalidSend, // user tried to send invalid data + ConnectionClosed, // connection closed voluntarily or lost involuntarily + Unexpected // unexpected error / exception, requires fix. + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta new file mode 100644 index 0000000..42f163f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3abbeffc1d794f11a45b7fcf110353f5 +timeCreated: 1652320712 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs new file mode 100644 index 0000000..6115bc8 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs @@ -0,0 +1,33 @@ +using System.Net.Sockets; + +namespace kcp2k +{ + public static class Extensions + { + // 100k attempts of 1 KB increases = default + 100 MB max + public static void SetReceiveBufferToOSLimit(this Socket socket, int stepSize = 1024, int attempts = 100_000) + { + // setting a too large size throws a socket exception. + // so let's keep increasing until we encounter it. + for (int i = 0; i < attempts; ++i) + { + // increase in 1 KB steps + try { socket.ReceiveBufferSize += stepSize; } + catch (SocketException) { break; } + } + } + + // 100k attempts of 1 KB increases = default + 100 MB max + public static void SetSendBufferToOSLimit(this Socket socket, int stepSize = 1024, int attempts = 100_000) + { + // setting a too large size throws a socket exception. + // so let's keep increasing until we encounter it. + for (int i = 0; i < attempts; ++i) + { + // increase in 1 KB steps + try { socket.SendBufferSize += stepSize; } + catch (SocketException) { break; } + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta new file mode 100644 index 0000000..36d3193 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c0649195e5ba4fcf8e0e1231fee7d5f6 +timeCreated: 1641701011 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs new file mode 100644 index 0000000..ccb19ba --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs @@ -0,0 +1,10 @@ +namespace kcp2k +{ + // channel type and header for raw messages + public enum KcpChannel : byte + { + // don't react on 0x00. might help to filter out random noise. + Reliable = 0x01, + Unreliable = 0x02 + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta new file mode 100644 index 0000000..2721025 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9e852b2532fb248d19715cfebe371db3 +timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs new file mode 100644 index 0000000..58249e7 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs @@ -0,0 +1,148 @@ +// kcp client logic abstracted into a class. +// for use in Mirror, DOTSNET, testing, etc. +using System; + +namespace kcp2k +{ + public class KcpClient + { + // events + public Action OnConnected; + public Action, KcpChannel> OnData; + public Action OnDisconnected; + // error callback instead of logging. + // allows libraries to show popups etc. + // (string instead of Exception for ease of use and to avoid user panic) + public Action OnError; + + // state + public KcpClientConnection connection; + public bool connected; + + public KcpClient(Action OnConnected, + Action, + KcpChannel> OnData, + Action OnDisconnected, + Action OnError) + { + this.OnConnected = OnConnected; + this.OnData = OnData; + this.OnDisconnected = OnDisconnected; + this.OnError = OnError; + } + + // CreateConnection can be overwritten for where-allocation: + // https://github.com/vis2k/where-allocation + protected virtual KcpClientConnection CreateConnection() => + new KcpClientConnection(); + + public void Connect(string address, + ushort port, + bool noDelay, + uint interval, + int fastResend = 0, + bool congestionWindow = true, + uint sendWindowSize = Kcp.WND_SND, + uint receiveWindowSize = Kcp.WND_RCV, + int timeout = KcpConnection.DEFAULT_TIMEOUT, + uint maxRetransmits = Kcp.DEADLINK, + bool maximizeSendReceiveBuffersToOSLimit = false) + { + if (connected) + { + Log.Warning("KCP: client already connected!"); + return; + } + + // create connection + connection = CreateConnection(); + + // setup events + connection.OnAuthenticated = () => + { + Log.Info($"KCP: OnClientConnected"); + connected = true; + OnConnected(); + }; + connection.OnData = (message, channel) => + { + //Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})"); + OnData(message, channel); + }; + connection.OnDisconnected = () => + { + Log.Info($"KCP: OnClientDisconnected"); + connected = false; + connection = null; + OnDisconnected(); + }; + connection.OnError = (error, reason) => + { + OnError(error, reason); + }; + + // connect + connection.Connect(address, + port, + noDelay, + interval, + fastResend, + congestionWindow, + sendWindowSize, + receiveWindowSize, + timeout, + maxRetransmits, + maximizeSendReceiveBuffersToOSLimit); + } + + public void Send(ArraySegment segment, KcpChannel channel) + { + if (connected) + { + connection.SendData(segment, channel); + } + else Log.Warning("KCP: can't send because client not connected!"); + } + + public void Disconnect() + { + // only if connected + // otherwise we end up in a deadlock because of an open Mirror bug: + // https://github.com/vis2k/Mirror/issues/2353 + if (connected) + { + // call Disconnect and let the connection handle it. + // DO NOT set it to null yet. it needs to be updated a few more + // times first. let the connection handle it! + connection?.Disconnect(); + } + } + + // process incoming messages. should be called before updating the world. + public void TickIncoming() + { + // recv on socket first, then process incoming + // (even if we didn't receive anything. need to tick ping etc.) + // (connection is null if not active) + connection?.RawReceive(); + connection?.TickIncoming(); + } + + // process outgoing messages. should be called after updating the world. + public void TickOutgoing() + { + // process outgoing + // (connection is null if not active) + connection?.TickOutgoing(); + } + + // process incoming and outgoing for convenience + // => ideally call ProcessIncoming() before updating the world and + // ProcessOutgoing() after updating the world for minimum latency + public void Tick() + { + TickIncoming(); + TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta new file mode 100644 index 0000000..e55306b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6aa069a28ed24fedb533c102d9742b36 +timeCreated: 1603786960 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs new file mode 100644 index 0000000..a843a8d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs @@ -0,0 +1,156 @@ +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + public class KcpClientConnection : KcpConnection + { + // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even + // if MaxMessageSize is larger. kcp always sends in MTU + // segments and having a buffer smaller than MTU would + // silently drop excess data. + // => we need the MTU to fit channel + message! + readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; + + // helper function to resolve host to IPAddress + public static bool ResolveHostname(string hostname, out IPAddress[] addresses) + { + try + { + // NOTE: dns lookup is blocking. this can take a second. + addresses = Dns.GetHostAddresses(hostname); + return addresses.Length >= 1; + } + catch (SocketException exception) + { + Log.Info($"Failed to resolve host: {hostname} reason: {exception}"); + addresses = null; + return false; + } + } + + // EndPoint & Receive functions can be overwritten for where-allocation: + // https://github.com/vis2k/where-allocation + // NOTE: Client's SendTo doesn't allocate, don't need a virtual. + protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) => + remoteEndPoint = new IPEndPoint(addresses[0], port); + + protected virtual int ReceiveFrom(byte[] buffer) => + socket.ReceiveFrom(buffer, ref remoteEndPoint); + + // if connections drop under heavy load, increase to OS limit. + // if still not enough, increase the OS limit. + void ConfigureSocketBufferSizes(bool maximizeSendReceiveBuffersToOSLimit) + { + if (maximizeSendReceiveBuffersToOSLimit) + { + // log initial size for comparison. + // remember initial size for log comparison + int initialReceive = socket.ReceiveBufferSize; + int initialSend = socket.SendBufferSize; + + socket.SetReceiveBufferToOSLimit(); + socket.SetSendBufferToOSLimit(); + Log.Info($"KcpClient: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x) increased to OS limits!"); + } + // otherwise still log the defaults for info. + else Log.Info($"KcpClient: RecvBuf = {socket.ReceiveBufferSize} SendBuf = {socket.SendBufferSize}. If connections drop under heavy load, enable {nameof(maximizeSendReceiveBuffersToOSLimit)} to increase it to OS limit. If they still drop, increase the OS limit."); + } + + public void Connect(string host, + ushort port, + bool noDelay, + uint interval = Kcp.INTERVAL, + int fastResend = 0, + bool congestionWindow = true, + uint sendWindowSize = Kcp.WND_SND, + uint receiveWindowSize = Kcp.WND_RCV, + int timeout = DEFAULT_TIMEOUT, + uint maxRetransmits = Kcp.DEADLINK, + bool maximizeSendReceiveBuffersToOSLimit = false) + { + Log.Info($"KcpClient: connect to {host}:{port}"); + + // try resolve host name + if (ResolveHostname(host, out IPAddress[] addresses)) + { + // create remote endpoint + CreateRemoteEndPoint(addresses, port); + + // create socket + socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + + // configure buffer sizes + ConfigureSocketBufferSizes(maximizeSendReceiveBuffersToOSLimit); + + // connect + socket.Connect(remoteEndPoint); + + // set up kcp + SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits); + + // client should send handshake to server as very first message + SendHandshake(); + + RawReceive(); + } + // otherwise call OnDisconnected to let the user know. + else + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {host}"); + OnDisconnected(); + } + } + + // call from transport update + public void RawReceive() + { + try + { + if (socket != null) + { + while (socket.Poll(0, SelectMode.SelectRead)) + { + int msgLength = ReceiveFrom(rawReceiveBuffer); + // IMPORTANT: detect if buffer was too small for the + // received msgLength. otherwise the excess + // data would be silently lost. + // (see ReceiveFrom documentation) + if (msgLength <= rawReceiveBuffer.Length) + { + //Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); + RawInput(rawReceiveBuffer, msgLength); + } + else + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting."); + Disconnect(); + } + } + } + } + // this is fine, the socket might have been closed in the other end + catch (SocketException ex) + { + // the other end closing the connection is not an 'error'. + // but connections should never just end silently. + // at least log a message for easier debugging. + Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}"); + Disconnect(); + } + } + + protected override void Dispose() + { + socket.Close(); + socket = null; + } + + protected override void RawSend(byte[] data, int length) + { + socket.Send(data, length, SocketFlags.None); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta new file mode 100644 index 0000000..3369918 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 96512e74aa8214a6faa8a412a7a07877 +timeCreated: 1602601237 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs new file mode 100644 index 0000000..e5bc0f3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs @@ -0,0 +1,668 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + enum KcpState { Connected, Authenticated, Disconnected } + + public abstract class KcpConnection + { + protected Socket socket; + protected EndPoint remoteEndPoint; + internal Kcp kcp; + + // kcp can have several different states, let's use a state machine + KcpState state = KcpState.Disconnected; + + public Action OnAuthenticated; + public Action, KcpChannel> OnData; + public Action OnDisconnected; + // error callback instead of logging. + // allows libraries to show popups etc. + // (string instead of Exception for ease of use and to avoid user panic) + public Action OnError; + + // If we don't receive anything these many milliseconds + // then consider us disconnected + public const int DEFAULT_TIMEOUT = 10000; + public int timeout = DEFAULT_TIMEOUT; + uint lastReceiveTime; + + // internal time. + // StopWatch offers ElapsedMilliSeconds and should be more precise than + // Unity's time.deltaTime over long periods. + readonly Stopwatch refTime = new Stopwatch(); + + // we need to subtract the channel byte from every MaxMessageSize + // calculation. + // we also need to tell kcp to use MTU-1 to leave space for the byte. + const int CHANNEL_HEADER_SIZE = 1; + + // reliable channel (= kcp) MaxMessageSize so the outside knows largest + // allowed message to send. the calculation in Send() is not obvious at + // all, so let's provide the helper here. + // + // kcp does fragmentation, so max message is way larger than MTU. + // + // -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD + // -> Send() checks if fragment count < rcv_wnd, so we use rcv_wnd - 1. + // NOTE that original kcp has a bug where WND_RCV default is used + // instead of configured rcv_wnd, limiting max message size to 144 KB + // https://github.com/skywind3000/kcp/pull/291 + // we fixed this in kcp2k. + // -> we add 1 byte KcpHeader enum to each message, so -1 + // + // IMPORTANT: max message is MTU * rcv_wnd, in other words it completely + // fills the receive window! due to head of line blocking, + // all other messages have to wait while a maxed size message + // is being delivered. + // => in other words, DO NOT use max size all the time like + // for batching. + // => sending UNRELIABLE max message size most of the time is + // best for performance (use that one for batching!) + static int ReliableMaxMessageSize_Unconstrained(uint rcv_wnd) => (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * ((int)rcv_wnd - 1) - 1; + + // kcp encodes 'frg' as 1 byte. + // max message size can only ever allow up to 255 fragments. + // WND_RCV gives 127 fragments. + // WND_RCV * 2 gives 255 fragments. + // so we can limit max message size by limiting rcv_wnd parameter. + public static int ReliableMaxMessageSize(uint rcv_wnd) => + ReliableMaxMessageSize_Unconstrained(Math.Min(rcv_wnd, Kcp.FRG_MAX)); + + // unreliable max message size is simply MTU - channel header size + public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE; + + // buffer to receive kcp's processed messages (avoids allocations). + // IMPORTANT: this is for KCP messages. so it needs to be of size: + // 1 byte header + MaxMessageSize content + byte[] kcpMessageBuffer;// = new byte[1 + ReliableMaxMessageSize]; + + // send buffer for handing user messages to kcp for processing. + // (avoids allocations). + // IMPORTANT: needs to be of size: + // 1 byte header + MaxMessageSize content + byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize]; + + // raw send buffer is exactly MTU. + byte[] rawSendBuffer = new byte[Kcp.MTU_DEF]; + + // send a ping occasionally so we don't time out on the other end. + // for example, creating a character in an MMO could easily take a + // minute of no data being sent. which doesn't mean we want to time out. + // same goes for slow paced card games etc. + public const int PING_INTERVAL = 1000; + uint lastPingTime; + + // if we send more than kcp can handle, we will get ever growing + // send/recv buffers and queues and minutes of latency. + // => if a connection can't keep up, it should be disconnected instead + // to protect the server under heavy load, and because there is no + // point in growing to gigabytes of memory or minutes of latency! + // => 2k isn't enough. we reach 2k when spawning 4k monsters at once + // easily, but it does recover over time. + // => 10k seems safe. + // + // note: we have a ChokeConnectionAutoDisconnects test for this too! + internal const int QueueDisconnectThreshold = 10000; + + // getters for queue and buffer counts, used for debug info + public int SendQueueCount => kcp.snd_queue.Count; + public int ReceiveQueueCount => kcp.rcv_queue.Count; + public int SendBufferCount => kcp.snd_buf.Count; + public int ReceiveBufferCount => kcp.rcv_buf.Count; + + // maximum send rate per second can be calculated from kcp parameters + // source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html + // + // KCP can send/receive a maximum of WND*MTU per interval. + // multiple by 1000ms / interval to get the per-second rate. + // + // example: + // WND(32) * MTU(1400) = 43.75KB + // => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s + // + // returns bytes/second! + public uint MaxSendRate => + kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval; + + public uint MaxReceiveRate => + kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval; + + // SetupKcp creates and configures a new KCP instance. + // => useful to start from a fresh state every time the client connects + // => NoDelay, interval, wnd size are the most important configurations. + // let's force require the parameters so we don't forget it anywhere. + protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) + { + // set up kcp over reliable channel (that's what kcp is for) + kcp = new Kcp(0, RawSendReliable); + // set nodelay. + // note that kcp uses 'nocwnd' internally so we negate the parameter + kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow); + kcp.SetWindowSize(sendWindowSize, receiveWindowSize); + + // IMPORTANT: high level needs to add 1 channel byte to each raw + // message. so while Kcp.MTU_DEF is perfect, we actually need to + // tell kcp to use MTU-1 so we can still put the header into the + // message afterwards. + kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE); + + // set maximum retransmits (aka dead_link) + kcp.dead_link = maxRetransmits; + + // create message buffers AFTER window size is set + // see comments on buffer definition for the "+1" part + kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize(receiveWindowSize)]; + kcpSendBuffer = new byte[1 + ReliableMaxMessageSize(receiveWindowSize)]; + + this.timeout = timeout; + state = KcpState.Connected; + + refTime.Start(); + } + + void HandleTimeout(uint time) + { + // note: we are also sending a ping regularly, so timeout should + // only ever happen if the connection is truly gone. + if (time >= lastReceiveTime + timeout) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.Timeout, $"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); + Disconnect(); + } + } + + void HandleDeadLink() + { + // kcp has 'dead_link' detection. might as well use it. + if (kcp.state == -1) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.Timeout, $"KCP Connection dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting."); + Disconnect(); + } + } + + // send a ping occasionally in order to not time out on the other end. + void HandlePing(uint time) + { + // enough time elapsed since last ping? + if (time >= lastPingTime + PING_INTERVAL) + { + // ping again and reset time + //Log.Debug("KCP: sending ping..."); + SendPing(); + lastPingTime = time; + } + } + + void HandleChoked() + { + // disconnect connections that can't process the load. + // see QueueSizeDisconnect comments. + // => include all of kcp's buffers and the unreliable queue! + int total = kcp.rcv_queue.Count + kcp.snd_queue.Count + + kcp.rcv_buf.Count + kcp.snd_buf.Count; + if (total >= QueueDisconnectThreshold) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.Congestion, + $"KCP: disconnecting connection because it can't process data fast enough.\n" + + $"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" + + $"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" + + $"* Or perhaps the network is simply too slow on our end, or on the other end."); + + // let's clear all pending sends before disconnting with 'Bye'. + // otherwise a single Flush in Disconnect() won't be enough to + // flush thousands of messages to finally deliver 'Bye'. + // this is just faster and more robust. + kcp.snd_queue.Clear(); + + Disconnect(); + } + } + + // reads the next reliable message type & content from kcp. + // -> to avoid buffering, unreliable messages call OnData directly. + bool ReceiveNextReliable(out KcpHeader header, out ArraySegment message) + { + int msgSize = kcp.PeekSize(); + if (msgSize > 0) + { + // only allow receiving up to buffer sized messages. + // otherwise we would get BlockCopy ArgumentException anyway. + if (msgSize <= kcpMessageBuffer.Length) + { + // receive from kcp + int received = kcp.Receive(kcpMessageBuffer, msgSize); + if (received >= 0) + { + // extract header & content without header + header = (KcpHeader)kcpMessageBuffer[0]; + message = new ArraySegment(kcpMessageBuffer, 1, msgSize - 1); + lastReceiveTime = (uint)refTime.ElapsedMilliseconds; + return true; + } + else + { + // if receive failed, close everything + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"Receive failed with error={received}. closing connection."); + Disconnect(); + } + } + // we don't allow sending messages > Max, so this must be an + // attacker. let's disconnect to avoid allocation attacks etc. + else + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); + Disconnect(); + } + } + + message = default; + header = KcpHeader.Disconnect; + return false; + } + + void TickIncoming_Connected(uint time) + { + // detect common events & ping + HandleTimeout(time); + HandleDeadLink(); + HandlePing(time); + HandleChoked(); + + // any reliable kcp message received? + if (ReceiveNextReliable(out KcpHeader header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeader.Handshake: + { + // we were waiting for a handshake. + // it proves that the other end speaks our protocol. + Log.Info("KCP: received handshake"); + state = KcpState.Authenticated; + OnAuthenticated?.Invoke(); + break; + } + case KcpHeader.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + case KcpHeader.Data: + case KcpHeader.Disconnect: + { + // everything else is not allowed during handshake! + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"KCP: received invalid header {header} while Connected. Disconnecting the connection."); + Disconnect(); + break; + } + } + } + } + + void TickIncoming_Authenticated(uint time) + { + // detect common events & ping + HandleTimeout(time); + HandleDeadLink(); + HandlePing(time); + HandleChoked(); + + // process all received messages + while (ReceiveNextReliable(out KcpHeader header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeader.Handshake: + { + // should never receive another handshake after auth + Log.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection."); + Disconnect(); + break; + } + case KcpHeader.Data: + { + // call OnData IF the message contained actual data + if (message.Count > 0) + { + //Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}"); + OnData?.Invoke(message, KcpChannel.Reliable); + } + // empty data = attacker, or something went wrong + else + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, "KCP: received empty Data message while Authenticated. Disconnecting the connection."); + Disconnect(); + } + break; + } + case KcpHeader.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + case KcpHeader.Disconnect: + { + // disconnect might happen + Log.Info("KCP: received disconnect message"); + Disconnect(); + break; + } + } + } + } + + public void TickIncoming() + { + uint time = (uint)refTime.ElapsedMilliseconds; + + try + { + switch (state) + { + case KcpState.Connected: + { + TickIncoming_Connected(time); + break; + } + case KcpState.Authenticated: + { + TickIncoming_Authenticated(time); + break; + } + case KcpState.Disconnected: + { + // do nothing while disconnected + break; + } + } + } + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (ObjectDisposedException exception) + { + // fine, socket was closed + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.Unexpected, $"KCP Connection: unexpected Exception: {exception}"); + Disconnect(); + } + } + + public void TickOutgoing() + { + uint time = (uint)refTime.ElapsedMilliseconds; + + try + { + switch (state) + { + case KcpState.Connected: + case KcpState.Authenticated: + { + // update flushes out messages + kcp.Update(time); + break; + } + case KcpState.Disconnected: + { + // do nothing while disconnected + break; + } + } + } + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (ObjectDisposedException exception) + { + // fine, socket was closed + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.Unexpected, $"KCP Connection: unexpected exception: {exception}"); + Disconnect(); + } + } + + public void RawInput(byte[] buffer, int msgLength) + { + // parse channel + if (msgLength > 0) + { + byte channel = buffer[0]; + switch (channel) + { + case (byte)KcpChannel.Reliable: + { + // input into kcp, but skip channel byte + int input = kcp.Input(buffer, 1, msgLength - 1); + if (input != 0) + { + Log.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}"); + } + break; + } + case (byte)KcpChannel.Unreliable: + { + // ideally we would queue all unreliable messages and + // then process them in ReceiveNext() together with the + // reliable messages, but: + // -> queues/allocations/pools are slow and complex. + // -> DOTSNET 10k is actually slower if we use pooled + // unreliable messages for transform messages. + // + // DOTSNET 10k benchmark: + // reliable-only: 170 FPS + // unreliable queued: 130-150 FPS + // unreliable direct: 183 FPS(!) + // + // DOTSNET 50k benchmark: + // reliable-only: FAILS (queues keep growing) + // unreliable direct: 18-22 FPS(!) + // + // -> all unreliable messages are DATA messages anyway. + // -> let's skip the magic and call OnData directly if + // the current state allows it. + if (state == KcpState.Authenticated) + { + ArraySegment message = new ArraySegment(buffer, 1, msgLength - 1); + OnData?.Invoke(message, KcpChannel.Unreliable); + + // set last receive time to avoid timeout. + // -> we do this in ANY case even if not enabled. + // a message is a message. + // -> we set last receive time for both reliable and + // unreliable messages. both count. + // otherwise a connection might time out even + // though unreliable were received, but no + // reliable was received. + lastReceiveTime = (uint)refTime.ElapsedMilliseconds; + } + else + { + // should never happen + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"KCP: received unreliable message in state {state}. Disconnecting the connection."); + Disconnect(); + } + break; + } + default: + { + // not a valid channel. random data or attacks. + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"Disconnecting connection because of invalid channel header: {channel}"); + Disconnect(); + break; + } + } + } + } + + // raw send puts the data into the socket + protected abstract void RawSend(byte[] data, int length); + + // raw send called by kcp + void RawSendReliable(byte[] data, int length) + { + // copy channel header, data into raw send buffer, then send + rawSendBuffer[0] = (byte)KcpChannel.Reliable; + Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length); + RawSend(rawSendBuffer, length + 1); + } + + void SendReliable(KcpHeader header, ArraySegment content) + { + // 1 byte header + content needs to fit into send buffer + if (1 + content.Count <= kcpSendBuffer.Length) // TODO + { + // copy header, content (if any) into send buffer + kcpSendBuffer[0] = (byte)header; + if (content.Count > 0) + Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count); + + // send to kcp for processing + int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count); + if (sent < 0) + { + Log.Warning($"Send failed with error={sent} for content with length={content.Count}"); + } + } + // otherwise content is larger than MaxMessageSize. let user know! + else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize(kcp.rcv_wnd)}"); + } + + void SendUnreliable(ArraySegment message) + { + // message size needs to be <= unreliable max size + if (message.Count <= UnreliableMaxMessageSize) + { + // copy channel header, data into raw send buffer, then send + rawSendBuffer[0] = (byte)KcpChannel.Unreliable; + Buffer.BlockCopy(message.Array, message.Offset, rawSendBuffer, 1, message.Count); + RawSend(rawSendBuffer, message.Count + 1); + } + // otherwise content is larger than MaxMessageSize. let user know! + else Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}"); + } + + // server & client need to send handshake at different times, so we need + // to expose the function. + // * client should send it immediately. + // * server should send it as reply to client's handshake, not before + // (server should not reply to random internet messages with handshake) + // => handshake info needs to be delivered, so it goes over reliable. + public void SendHandshake() + { + Log.Info("KcpConnection: sending Handshake to other end!"); + SendReliable(KcpHeader.Handshake, default); + } + + public void SendData(ArraySegment data, KcpChannel channel) + { + // sending empty segments is not allowed. + // nobody should ever try to send empty data. + // it means that something went wrong, e.g. in Mirror/DOTSNET. + // let's make it obvious so it's easy to debug. + if (data.Count == 0) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidSend, "KcpConnection: tried sending empty message. This should never happen. Disconnecting."); + Disconnect(); + return; + } + + switch (channel) + { + case KcpChannel.Reliable: + SendReliable(KcpHeader.Data, data); + break; + case KcpChannel.Unreliable: + SendUnreliable(data); + break; + } + } + + // ping goes through kcp to keep it from timing out, so it goes over the + // reliable channel. + void SendPing() => SendReliable(KcpHeader.Ping, default); + + // disconnect info needs to be delivered, so it goes over reliable + void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default); + + protected virtual void Dispose() {} + + // disconnect this connection + public void Disconnect() + { + // only if not disconnected yet + if (state == KcpState.Disconnected) + return; + + // send a disconnect message + if (socket.Connected) + { + try + { + SendDisconnect(); + kcp.Flush(); + } + catch (SocketException) + { + // this is ok, the connection was already closed + } + catch (ObjectDisposedException) + { + // this is normal when we stop the server + // the socket is stopped so we can't send anything anymore + // to the clients + + // the clients will eventually timeout and realize they + // were disconnected + } + } + + // set as Disconnected, call event + Log.Info("KCP Connection: Disconnected."); + state = KcpState.Disconnected; + OnDisconnected?.Invoke(); + } + + // get remote endpoint + public EndPoint GetRemoteEndPoint() => remoteEndPoint; + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta new file mode 100644 index 0000000..fa5dcff --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3915c7c62b72d4dc2a9e4e76c94fc484 +timeCreated: 1602600432 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs new file mode 100644 index 0000000..bc4b047 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs @@ -0,0 +1,19 @@ +namespace kcp2k +{ + // header for messages processed by kcp. + // this is NOT for the raw receive messages(!) because handshake/disconnect + // need to be sent reliably. it's not enough to have those in rawreceive + // because those messages might get lost without being resent! + public enum KcpHeader : byte + { + // don't react on 0x00. might help to filter out random noise. + Handshake = 0x01, + // ping goes over reliable & KcpHeader for now. could go over reliable + // too. there is no real difference except that this is easier because + // we already have a KcpHeader for reliable messages. + // ping is only used to keep it alive, so latency doesn't matter. + Ping = 0x02, + Data = 0x03, + Disconnect = 0x04 + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta new file mode 100644 index 0000000..9e81c94 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 91b5edac31224a49bd76f960ae018942 +timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs new file mode 100644 index 0000000..5e48688 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs @@ -0,0 +1,375 @@ +// kcp server logic abstracted into a class. +// for use in Mirror, DOTSNET, testing, etc. +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + public class KcpServer + { + // events + public Action OnConnected; + public Action, KcpChannel> OnData; + public Action OnDisconnected; + // error callback instead of logging. + // allows libraries to show popups etc. + // (string instead of Exception for ease of use and to avoid user panic) + public Action OnError; + + // socket configuration + // DualMode uses both IPv6 and IPv4. not all platforms support it. + // (Nintendo Switch, etc.) + public bool DualMode; + // too small send/receive buffers might cause connection drops under + // heavy load. using the OS max size can make a difference already. + public bool MaximizeSendReceiveBuffersToOSLimit; + + // kcp configuration + // NoDelay is recommended to reduce latency. This also scales better + // without buffers getting full. + public bool NoDelay; + // 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; + // KCP fastresend parameter. Faster resend for the cost of higher + // bandwidth. + public int FastResend; + // KCP 'NoCongestionWindow' is false by default. here we negate it for + // ease of use. This can be disabled for high scale games if connections + // choke regularly. + public bool CongestionWindow; + // KCP window size can be modified to support higher loads. + // for example, Mirror Benchmark requires: + // 128, 128 for 4k monsters + // 512, 512 for 10k monsters + // 8192, 8192 for 20k monsters + public uint SendWindowSize; + public uint ReceiveWindowSize; + // timeout in milliseconds + public int Timeout; + // maximum retransmission attempts until dead_link + public uint MaxRetransmits; + + // state + protected Socket socket; + EndPoint newClientEP; + + // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even + // if MaxMessageSize is larger. kcp always sends in MTU + // segments and having a buffer smaller than MTU would + // silently drop excess data. + // => we need the mtu to fit channel + message! + readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; + + // connections where connectionId is EndPoint.GetHashCode + public Dictionary connections = new Dictionary(); + + public KcpServer(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + bool DualMode, + bool NoDelay, + uint Interval, + int FastResend = 0, + bool CongestionWindow = true, + uint SendWindowSize = Kcp.WND_SND, + uint ReceiveWindowSize = Kcp.WND_RCV, + int Timeout = KcpConnection.DEFAULT_TIMEOUT, + uint MaxRetransmits = Kcp.DEADLINK, + bool MaximizeSendReceiveBuffersToOSLimit = false) + { + this.OnConnected = OnConnected; + this.OnData = OnData; + this.OnDisconnected = OnDisconnected; + this.OnError = OnError; + this.DualMode = DualMode; + this.NoDelay = NoDelay; + this.Interval = Interval; + this.FastResend = FastResend; + this.CongestionWindow = CongestionWindow; + this.SendWindowSize = SendWindowSize; + this.ReceiveWindowSize = ReceiveWindowSize; + this.Timeout = Timeout; + this.MaxRetransmits = MaxRetransmits; + this.MaximizeSendReceiveBuffersToOSLimit = MaximizeSendReceiveBuffersToOSLimit; + + // create newClientEP either IPv4 or IPv6 + newClientEP = DualMode + ? new IPEndPoint(IPAddress.IPv6Any, 0) + : new IPEndPoint(IPAddress.Any, 0); + } + + public bool IsActive() => socket != null; + + // if connections drop under heavy load, increase to OS limit. + // if still not enough, increase the OS limit. + void ConfigureSocketBufferSizes() + { + if (MaximizeSendReceiveBuffersToOSLimit) + { + // log initial size for comparison. + // remember initial size for log comparison + int initialReceive = socket.ReceiveBufferSize; + int initialSend = socket.SendBufferSize; + + socket.SetReceiveBufferToOSLimit(); + socket.SetSendBufferToOSLimit(); + Log.Info($"KcpServer: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x) increased to OS limits!"); + } + // otherwise still log the defaults for info. + else Log.Info($"KcpServer: RecvBuf = {socket.ReceiveBufferSize} SendBuf = {socket.SendBufferSize}. If connections drop under heavy load, enable {nameof(MaximizeSendReceiveBuffersToOSLimit)} to increase it to OS limit. If they still drop, increase the OS limit."); + } + + public void Start(ushort port) + { + // only start once + if (socket != null) + { + Log.Warning("KCP: server already started!"); + } + + // listen + if (DualMode) + { + // IPv6 socket with DualMode + socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + socket.DualMode = true; + socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); + } + else + { + // IPv4 socket + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); + } + + // configure socket buffer size. + ConfigureSocketBufferSizes(); + } + + public void Send(int connectionId, ArraySegment segment, KcpChannel channel) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + connection.SendData(segment, channel); + } + } + + public void Disconnect(int connectionId) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + connection.Disconnect(); + } + } + + // expose the whole IPEndPoint, not just the IP address. some need it. + public IPEndPoint GetClientEndPoint(int connectionId) + { + if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + return (connection.GetRemoteEndPoint() as IPEndPoint); + } + return null; + } + + // EndPoint & Receive functions can be overwritten for where-allocation: + // https://github.com/vis2k/where-allocation + protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash) + { + // NOTE: ReceiveFrom allocates. + // we pass our IPEndPoint to ReceiveFrom. + // receive from calls newClientEP.Create(socketAddr). + // IPEndPoint.Create always returns a new IPEndPoint. + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 + int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP); + + // calculate connectionHash from endpoint + // NOTE: IPEndPoint.GetHashCode() allocates. + // it calls m_Address.GetHashCode(). + // m_Address is an IPAddress. + // GetHashCode() allocates for IPv6: + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 + // + // => using only newClientEP.Port wouldn't work, because + // different connections can have the same port. + connectionHash = newClientEP.GetHashCode(); + return read; + } + + protected virtual KcpServerConnection CreateConnection() => + new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmits); + + // process incoming messages. should be called before updating the world. + HashSet connectionsToRemove = new HashSet(); + public void TickIncoming() + { + while (socket != null && socket.Poll(0, SelectMode.SelectRead)) + { + try + { + // receive + int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId); + //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); + + // IMPORTANT: detect if buffer was too small for the received + // msgLength. otherwise the excess data would be + // silently lost. + // (see ReceiveFrom documentation) + if (msgLength <= rawReceiveBuffer.Length) + { + // is this a new connection? + if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + // create a new KcpConnection based on last received + // EndPoint. can be overwritten for where-allocation. + connection = CreateConnection(); + + // DO NOT add to connections yet. only if the first message + // is actually the kcp handshake. otherwise it's either: + // * random data from the internet + // * or from a client connection that we just disconnected + // but that hasn't realized it yet, still sending data + // from last session that we should absolutely ignore. + // + // + // TODO this allocates a new KcpConnection for each new + // internet connection. not ideal, but C# UDP Receive + // already allocated anyway. + // + // expecting a MAGIC byte[] would work, but sending the raw + // UDP message without kcp's reliability will have low + // probability of being received. + // + // for now, this is fine. + + // setup authenticated event that also adds to connections + connection.OnAuthenticated = () => + { + // only send handshake to client AFTER we received his + // handshake in OnAuthenticated. + // we don't want to reply to random internet messages + // with handshakes each time. + connection.SendHandshake(); + + // add to connections dict after being authenticated. + connections.Add(connectionId, connection); + Log.Info($"KCP: server added connection({connectionId})"); + + // setup Data + Disconnected events only AFTER the + // handshake. we don't want to fire OnServerDisconnected + // every time we receive invalid random data from the + // internet. + + // setup data event + connection.OnData = (message, channel) => + { + // call mirror event + //Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})"); + OnData.Invoke(connectionId, message, channel); + }; + + // setup disconnected event + connection.OnDisconnected = () => + { + // flag for removal + // (can't remove directly because connection is updated + // and event is called while iterating all connections) + connectionsToRemove.Add(connectionId); + + // call mirror event + Log.Info($"KCP: OnServerDisconnected({connectionId})"); + OnDisconnected(connectionId); + }; + + // setup error event + connection.OnError = (error, reason) => + { + OnError(connectionId, error, reason); + }; + + // finally, call mirror OnConnected event + Log.Info($"KCP: OnServerConnected({connectionId})"); + OnConnected(connectionId); + }; + + // now input the message & process received ones + // connected event was set up. + // tick will process the first message and adds the + // connection if it was the handshake. + connection.RawInput(rawReceiveBuffer, msgLength); + connection.TickIncoming(); + + // again, do not add to connections. + // if the first message wasn't the kcp handshake then + // connection will simply be garbage collected. + } + // existing connection: simply input the message into kcp + else + { + connection.RawInput(rawReceiveBuffer, msgLength); + } + } + else + { + Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}."); + Disconnect(connectionId); + } + } + // this is fine, the socket might have been closed in the other end + catch (SocketException ex) + { + // the other end closing the connection is not an 'error'. + // but connections should never just end silently. + // at least log a message for easier debugging. + Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}"); + } + } + + // process inputs for all server connections + // (even if we didn't receive anything. need to tick ping etc.) + foreach (KcpServerConnection connection in connections.Values) + { + connection.TickIncoming(); + } + + // remove disconnected connections + // (can't do it in connection.OnDisconnected because Tick is called + // while iterating connections) + foreach (int connectionId in connectionsToRemove) + { + connections.Remove(connectionId); + } + connectionsToRemove.Clear(); + } + + // process outgoing messages. should be called after updating the world. + public void TickOutgoing() + { + // flush all server connections + foreach (KcpServerConnection connection in connections.Values) + { + connection.TickOutgoing(); + } + } + + // process incoming and outgoing for convenience. + // => ideally call ProcessIncoming() before updating the world and + // ProcessOutgoing() after updating the world for minimum latency + public void Tick() + { + TickIncoming(); + TickOutgoing(); + } + + public void Stop() + { + socket?.Close(); + socket = null; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta new file mode 100644 index 0000000..ef720d4 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9759159c6589494a9037f5e130a867ed +timeCreated: 1603787747 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs new file mode 100644 index 0000000..a902865 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs @@ -0,0 +1,22 @@ +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + public class KcpServerConnection : KcpConnection + { + // Constructor & Send functions can be overwritten for where-allocation: + // https://github.com/vis2k/where-allocation + public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) + { + this.socket = socket; + this.remoteEndPoint = remoteEndPoint; + SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits); + } + + protected override void RawSend(byte[] data, int length) + { + socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta new file mode 100644 index 0000000..10d9803 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 80a9b1ce9a6f14abeb32bfa9921d097b +timeCreated: 1602601483 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs new file mode 100644 index 0000000..939dae7 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs @@ -0,0 +1,14 @@ +// A simple logger class that uses Console.WriteLine by default. +// Can also do Logger.LogMethod = Debug.Log for Unity etc. +// (this way we don't have to depend on UnityEngine) +using System; + +namespace kcp2k +{ + public static class Log + { + public static Action Info = Console.WriteLine; + public static Action Warning = Console.WriteLine; + public static Action Error = Console.Error.WriteLine; + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs.meta new file mode 100644 index 0000000..333bee5 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b5e1de98d6d84c3793a61cf7d8da9a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta new file mode 100644 index 0000000..4cbc909 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0b320ff06046474eae7bce7240ea478c +timeCreated: 1626430641 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs new file mode 100644 index 0000000..b3e1b27 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs @@ -0,0 +1,24 @@ +// where-allocation version of KcpClientConnection. +// may not be wanted on all platforms, so it's an extra optional class. +using System.Net; +using WhereAllocation; + +namespace kcp2k +{ + public class KcpClientConnectionNonAlloc : KcpClientConnection + { + IPEndPointNonAlloc reusableEP; + + protected override void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) + { + // create reusableEP with same address family as remoteEndPoint. + // otherwise ReceiveFrom_NonAlloc couldn't use it. + reusableEP = new IPEndPointNonAlloc(addresses[0], port); + base.CreateRemoteEndPoint(addresses, port); + } + + // where-allocation nonalloc recv + protected override int ReceiveFrom(byte[] buffer) => + socket.ReceiveFrom_NonAlloc(buffer, reusableEP); + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta new file mode 100644 index 0000000..9d4a42e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4c1b235bbe054706bef6d092f361006e +timeCreated: 1626430539 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs new file mode 100644 index 0000000..2417408 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs @@ -0,0 +1,20 @@ +// where-allocation version of KcpClientConnectionNonAlloc. +// may not be wanted on all platforms, so it's an extra optional class. +using System; + +namespace kcp2k +{ + public class KcpClientNonAlloc : KcpClient + { + public KcpClientNonAlloc(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError) + : base(OnConnected, OnData, OnDisconnected, OnError) + { + } + + protected override KcpClientConnection CreateConnection() => + new KcpClientConnectionNonAlloc(); + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta new file mode 100644 index 0000000..266dafb --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2cf0ccf7d551480bb5af08fcbe169f84 +timeCreated: 1626435264 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs new file mode 100644 index 0000000..7986bea --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs @@ -0,0 +1,25 @@ +// where-allocation version of KcpServerConnection. +// may not be wanted on all platforms, so it's an extra optional class. +using System.Net; +using System.Net.Sockets; +using WhereAllocation; + +namespace kcp2k +{ + public class KcpServerConnectionNonAlloc : KcpServerConnection + { + IPEndPointNonAlloc reusableSendEndPoint; + + public KcpServerConnectionNonAlloc(Socket socket, EndPoint remoteEndpoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) + : base(socket, remoteEndpoint, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits) + { + this.reusableSendEndPoint = reusableSendEndPoint; + } + + protected override void RawSend(byte[] data, int length) + { + // where-allocation nonalloc send + socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta new file mode 100644 index 0000000..383fe02 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4e1b74cc224b4c83a0f6c8d8da9090ab +timeCreated: 1626430608 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs new file mode 100644 index 0000000..001a64b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs @@ -0,0 +1,77 @@ +// where-allocation version of KcpServer. +// may not be wanted on all platforms, so it's an extra optional class. +using System; +using System.Net; +using System.Net.Sockets; +using WhereAllocation; + +namespace kcp2k +{ + public class KcpServerNonAlloc : KcpServer + { + IPEndPointNonAlloc reusableClientEP; + + public KcpServerNonAlloc(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + bool DualMode, + bool NoDelay, + uint Interval, + int FastResend = 0, + bool CongestionWindow = true, + uint SendWindowSize = Kcp.WND_SND, + uint ReceiveWindowSize = Kcp.WND_RCV, + int Timeout = KcpConnection.DEFAULT_TIMEOUT, + uint MaxRetransmits = Kcp.DEADLINK, + bool MaximizeSendReceiveBuffersToOSLimit = false) + : base(OnConnected, + OnData, + OnDisconnected, + OnError, + DualMode, + NoDelay, + Interval, + FastResend, + CongestionWindow, + SendWindowSize, + ReceiveWindowSize, + Timeout, + MaxRetransmits, + MaximizeSendReceiveBuffersToOSLimit) + { + // create reusableClientEP either IPv4 or IPv6 + reusableClientEP = DualMode + ? new IPEndPointNonAlloc(IPAddress.IPv6Any, 0) + : new IPEndPointNonAlloc(IPAddress.Any, 0); + } + + protected override int ReceiveFrom(byte[] buffer, out int connectionHash) + { + // where-allocation nonalloc ReceiveFrom. + int read = socket.ReceiveFrom_NonAlloc(buffer, 0, buffer.Length, SocketFlags.None, reusableClientEP); + SocketAddress remoteAddress = reusableClientEP.temp; + + // where-allocation nonalloc GetHashCode + connectionHash = remoteAddress.GetHashCode(); + return read; + } + + protected override KcpServerConnection CreateConnection() + { + // IPEndPointNonAlloc is reused all the time. + // we can't store that as the connection's endpoint. + // we need a new copy! + IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint(); + + // for allocation free sending, we also need another + // IPEndPointNonAlloc... + IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port); + + // create a new KcpConnection NonAlloc version + // -> where-allocation IPEndPointNonAlloc is reused. + // need to create a new one from the temp address. + return new KcpServerConnectionNonAlloc(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmits); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta new file mode 100644 index 0000000..a878cc1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 54b8398dcd544c8a93bcad846214cc40 +timeCreated: 1626432191 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp.meta new file mode 100644 index 0000000..a7d6e11 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5cafb8851a0084f3e94a580c207b3923 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs new file mode 100644 index 0000000..5fe5547 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("kcp2k.Tests")] \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta new file mode 100644 index 0000000..6b442a9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aec6a15ac7bd43129317ea1f01f19782 +timeCreated: 1602665988 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs new file mode 100644 index 0000000..dff49e1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs @@ -0,0 +1,1058 @@ +// Kcp based on https://github.com/skywind3000/kcp +// Kept as close to original as possible. +using System; +using System.Collections.Generic; + +namespace kcp2k +{ + public class Kcp + { + // original Kcp has a define option, which is not defined by default: + // #define FASTACK_CONSERVE + + public const int RTO_NDL = 30; // no delay min rto + public const int RTO_MIN = 100; // normal min rto + public const int RTO_DEF = 200; // default RTO + public const int RTO_MAX = 60000; // maximum RTO + public const int CMD_PUSH = 81; // cmd: push data + public const int CMD_ACK = 82; // cmd: ack + public const int CMD_WASK = 83; // cmd: window probe (ask) + public const int CMD_WINS = 84; // cmd: window size (tell) + public const int ASK_SEND = 1; // need to send CMD_WASK + public const int ASK_TELL = 2; // need to send CMD_WINS + public const int WND_SND = 32; // default send window + public const int WND_RCV = 128; // default receive window. must be >= max fragment size + public const int MTU_DEF = 1200; // default MTU (reduced to 1200 to fit all cases: https://en.wikipedia.org/wiki/Maximum_transmission_unit ; steam uses 1200 too!) + public const int ACK_FAST = 3; + public const int INTERVAL = 100; + public const int OVERHEAD = 24; + public const int FRG_MAX = byte.MaxValue; // kcp encodes 'frg' as byte. so we can only ever send up to 255 fragments. + public const int DEADLINK = 20; // default maximum amount of 'xmit' retransmissions until a segment is considered lost + public const int THRESH_INIT = 2; + public const int THRESH_MIN = 2; + public const int PROBE_INIT = 7000; // 7 secs to probe window size + public const int PROBE_LIMIT = 120000; // up to 120 secs to probe window + public const int FASTACK_LIMIT = 5; // max times to trigger fastack + + internal struct AckItem + { + internal uint serialNumber; + internal uint timestamp; + } + + // kcp members. + internal int state; + readonly uint conv; // conversation + internal uint mtu; + internal uint mss; // maximum segment size := MTU - OVERHEAD + internal uint snd_una; // unacknowledged. e.g. snd_una is 9 it means 8 has been confirmed, 9 and 10 have been sent + internal uint snd_nxt; + internal uint rcv_nxt; + internal uint ssthresh; // slow start threshold + internal int rx_rttval; // average deviation of rtt, used to measure the jitter of rtt + internal int rx_srtt; // smoothed round trip time (a weighted average of rtt) + internal int rx_rto; + internal int rx_minrto; + internal uint snd_wnd; // send window + internal uint rcv_wnd; // receive window + internal uint rmt_wnd; // remote window + internal uint cwnd; // congestion window + internal uint probe; + internal uint interval; + internal uint ts_flush; + internal uint xmit; + internal uint nodelay; // not a bool. original Kcp has '<2 else' check. + internal bool updated; + internal uint ts_probe; // timestamp probe + internal uint probe_wait; + internal uint dead_link; // maximum amount of 'xmit' retransmissions until a segment is considered lost + internal uint incr; + internal uint current; // current time (milliseconds). set by Update. + + internal int fastresend; + internal int fastlimit; + internal bool nocwnd; // no congestion window + internal readonly Queue snd_queue = new Queue(16); // send queue + internal readonly Queue rcv_queue = new Queue(16); // receive queue + // snd_buffer needs index removals. + // C# LinkedList allocates for each entry, so let's keep List for now. + internal readonly List snd_buf = new List(16); // send buffer + // rcv_buffer needs index insertions and backwards iteration. + // C# LinkedList allocates for each entry, so let's keep List for now. + internal readonly List rcv_buf = new List(16); // receive buffer + internal readonly List acklist = new List(16); + + internal byte[] buffer; + readonly Action output; // buffer, size + + // get how many packet is waiting to be sent + public int WaitSnd => snd_buf.Count + snd_queue.Count; + + // segment pool to avoid allocations in C#. + // this is not part of the original C code. + readonly Pool SegmentPool = new Pool( + // create new segment + () => new Segment(), + // reset segment before reuse + (segment) => segment.Reset(), + // initial capacity + 32 + ); + + // ikcp_create + // create a new kcp control object, 'conv' must equal in two endpoint + // from the same connection. + public Kcp(uint conv, Action output) + { + this.conv = conv; + this.output = output; + snd_wnd = WND_SND; + rcv_wnd = WND_RCV; + rmt_wnd = WND_RCV; + mtu = MTU_DEF; + mss = mtu - OVERHEAD; + rx_rto = RTO_DEF; + rx_minrto = RTO_MIN; + interval = INTERVAL; + ts_flush = INTERVAL; + ssthresh = THRESH_INIT; + fastlimit = FASTACK_LIMIT; + dead_link = DEADLINK; + buffer = new byte[(mtu + OVERHEAD) * 3]; + } + + // ikcp_segment_new + // we keep the original function and add our pooling to it. + // this way we'll never miss it anywhere. + Segment SegmentNew() => SegmentPool.Take(); + + // ikcp_segment_delete + // we keep the original function and add our pooling to it. + // this way we'll never miss it anywhere. + void SegmentDelete(Segment seg) => SegmentPool.Return(seg); + + // ikcp_recv + // receive data from kcp state machine + // returns number of bytes read. + // returns negative on error. + // note: pass negative length to peek. + public int Receive(byte[] buffer, int len) + { + // kcp's ispeek feature is not supported. + // this makes 'merge fragment' code significantly easier because + // we can iterate while queue.Count > 0 and dequeue each time. + // if we had to consider ispeek then count would always be > 0 and + // we would have to remove only after the loop. + // + //bool ispeek = len < 0; + if (len < 0) + throw new NotSupportedException("Receive ispeek for negative len is not supported!"); + + if (rcv_queue.Count == 0) + return -1; + + if (len < 0) len = -len; + + int peeksize = PeekSize(); + + if (peeksize < 0) + return -2; + + if (peeksize > len) + return -3; + + bool recover = rcv_queue.Count >= rcv_wnd; + + // merge fragment. + int offset = 0; + len = 0; + // original KCP iterates rcv_queue and deletes if !ispeek. + // removing from a c# queue while iterating is not possible, but + // we can change to 'while Count > 0' and remove every time. + // (we can remove every time because we removed ispeek support!) + while (rcv_queue.Count > 0) + { + // unlike original kcp, we dequeue instead of just getting the + // entry. this is fine because we remove it in ANY case. + Segment seg = rcv_queue.Dequeue(); + + Buffer.BlockCopy(seg.data.GetBuffer(), 0, buffer, offset, (int)seg.data.Position); + offset += (int)seg.data.Position; + + len += (int)seg.data.Position; + uint fragment = seg.frg; + + // note: ispeek is not supported in order to simplify this loop + + // unlike original kcp, we don't need to remove seg from queue + // because we already dequeued it. + // simply delete it + SegmentDelete(seg); + + if (fragment == 0) + break; + } + + // move available data from rcv_buf -> rcv_queue + int removed = 0; + foreach (Segment seg in rcv_buf) + { + if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd) + { + // can't remove while iterating. remember how many to remove + // and do it after the loop. + // note: don't return segment. we only add it to rcv_queue + ++removed; + // add + rcv_queue.Enqueue(seg); + rcv_nxt++; + } + else + { + break; + } + } + rcv_buf.RemoveRange(0, removed); + + // fast recover + if (rcv_queue.Count < rcv_wnd && recover) + { + // ready to send back CMD_WINS in flush + // tell remote my window size + probe |= ASK_TELL; + } + + return len; + } + + // ikcp_peeksize + // check the size of next message in the recv queue + public int PeekSize() + { + int length = 0; + + if (rcv_queue.Count == 0) return -1; + + Segment seq = rcv_queue.Peek(); + if (seq.frg == 0) return (int)seq.data.Position; + + if (rcv_queue.Count < seq.frg + 1) return -1; + + foreach (Segment seg in rcv_queue) + { + length += (int)seg.data.Position; + if (seg.frg == 0) break; + } + + return length; + } + + // ikcp_send + // sends byte[] to the other end. + public int Send(byte[] buffer, int offset, int len) + { + // fragment count + int count; + + if (len < 0) return -1; + + // streaming mode: removed. we never want to send 'hello' and + // receive 'he' 'll' 'o'. we want to always receive 'hello'. + + // calculate amount of fragments necessary for 'len' + if (len <= mss) count = 1; + else count = (int)((len + mss - 1) / mss); + + // IMPORTANT kcp encodes 'frg' as 1 byte. + // so we can only support up to 255 fragments. + // (which limits max message size to around 288 KB) + // this is really nasty to debug. let's make this 100% obvious. + if (count > FRG_MAX) + throw new Exception($"Send len={len} requires {count} fragments, but kcp can only handle up to {FRG_MAX} fragments."); + + // original kcp uses WND_RCV const instead of rcv_wnd runtime: + // https://github.com/skywind3000/kcp/pull/291/files + // which always limits max message size to 144 KB: + //if (count >= WND_RCV) return -2; + // using configured rcv_wnd uncorks max message size to 'any': + if (count >= rcv_wnd) return -2; + + if (count == 0) count = 1; + + // fragment + for (int i = 0; i < count; i++) + { + int size = len > (int)mss ? (int)mss : len; + Segment seg = SegmentNew(); + + if (len > 0) + { + seg.data.Write(buffer, offset, size); + } + // seg.len = size: WriteBytes sets segment.Position! + seg.frg = (byte)(count - i - 1); + snd_queue.Enqueue(seg); + offset += size; + len -= size; + } + + return 0; + } + + // ikcp_update_ack + void UpdateAck(int rtt) // round trip time + { + // https://tools.ietf.org/html/rfc6298 + if (rx_srtt == 0) + { + rx_srtt = rtt; + rx_rttval = rtt / 2; + } + else + { + int delta = rtt - rx_srtt; + if (delta < 0) delta = -delta; + rx_rttval = (3 * rx_rttval + delta) / 4; + rx_srtt = (7 * rx_srtt + rtt) / 8; + if (rx_srtt < 1) rx_srtt = 1; + } + int rto = rx_srtt + Math.Max((int)interval, 4 * rx_rttval); + rx_rto = Utils.Clamp(rto, rx_minrto, RTO_MAX); + } + + // ikcp_shrink_buf + internal void ShrinkBuf() + { + if (snd_buf.Count > 0) + { + Segment seg = snd_buf[0]; + snd_una = seg.sn; + } + else + { + snd_una = snd_nxt; + } + } + + // ikcp_parse_ack + // removes the segment with 'sn' from send buffer + internal void ParseAck(uint sn) + { + if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0) + return; + + // for-int so we can erase while iterating + for (int i = 0; i < snd_buf.Count; ++i) + { + Segment seg = snd_buf[i]; + if (sn == seg.sn) + { + snd_buf.RemoveAt(i); + SegmentDelete(seg); + break; + } + if (Utils.TimeDiff(sn, seg.sn) < 0) + { + break; + } + } + } + + // ikcp_parse_una + void ParseUna(uint una) + { + int removed = 0; + foreach (Segment seg in snd_buf) + { + if (Utils.TimeDiff(una, seg.sn) > 0) + { + // can't remove while iterating. remember how many to remove + // and do it after the loop. + ++removed; + SegmentDelete(seg); + } + else + { + break; + } + } + snd_buf.RemoveRange(0, removed); + } + + // ikcp_parse_fastack + void ParseFastack(uint sn, uint ts) + { + if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0) + return; + + foreach (Segment seg in snd_buf) + { + if (Utils.TimeDiff(sn, seg.sn) < 0) + { + break; + } + else if (sn != seg.sn) + { +#if !FASTACK_CONSERVE + seg.fastack++; +#else + if (Utils.TimeDiff(ts, seg.ts) >= 0) + seg.fastack++; +#endif + } + } + } + + // ikcp_ack_push + // appends an ack. + void AckPush(uint sn, uint ts) + { + acklist.Add(new AckItem{ serialNumber = sn, timestamp = ts }); + } + + // ikcp_parse_data + void ParseData(Segment newseg) + { + uint sn = newseg.sn; + + if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) >= 0 || + Utils.TimeDiff(sn, rcv_nxt) < 0) + { + SegmentDelete(newseg); + return; + } + + InsertSegmentInReceiveBuffer(newseg); + MoveReceiveBufferDataToReceiveQueue(); + } + + // inserts the segment into rcv_buf, ordered by seg.sn. + // drops the segment if one with the same seg.sn already exists. + // goes through receive buffer in reverse order for performance. + // + // note: see KcpTests.InsertSegmentInReceiveBuffer test! + // note: 'insert or delete' can be done in different ways, but let's + // keep consistency with original C kcp. + internal void InsertSegmentInReceiveBuffer(Segment newseg) + { + bool repeat = false; // 'duplicate' + + // original C iterates backwards, so we need to do that as well. + int i; + for (i = rcv_buf.Count - 1; i >= 0; i--) + { + Segment seg = rcv_buf[i]; + if (seg.sn == newseg.sn) + { + // duplicate segment found. nothing will be added. + repeat = true; + break; + } + if (Utils.TimeDiff(newseg.sn, seg.sn) > 0) + { + // this entry's sn is < newseg.sn, so let's stop + break; + } + } + + // no duplicate? then insert. + if (!repeat) + { + rcv_buf.Insert(i + 1, newseg); + } + // duplicate. just delete it. + else + { + SegmentDelete(newseg); + } + } + + // move available data from rcv_buf -> rcv_queue + void MoveReceiveBufferDataToReceiveQueue() + { + int removed = 0; + foreach (Segment seg in rcv_buf) + { + if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd) + { + // can't remove while iterating. remember how many to remove + // and do it after the loop. + ++removed; + rcv_queue.Enqueue(seg); + rcv_nxt++; + } + else + { + break; + } + } + rcv_buf.RemoveRange(0, removed); + } + + // ikcp_input + // used when you receive a low level packet (e.g. UDP packet) + // => original kcp uses offset=0, we made it a parameter so that high + // level can skip the channel byte more easily + public int Input(byte[] data, int offset, int size) + { + uint prev_una = snd_una; + uint maxack = 0; + uint latest_ts = 0; + int flag = 0; + + if (data == null || size < OVERHEAD) return -1; + + while (true) + { + uint ts = 0; + uint sn = 0; + uint len = 0; + uint una = 0; + uint conv_ = 0; + ushort wnd = 0; + byte cmd = 0; + byte frg = 0; + + // enough data left to decode segment (aka OVERHEAD bytes)? + if (size < OVERHEAD) break; + + // decode segment + offset += Utils.Decode32U(data, offset, ref conv_); + if (conv_ != conv) return -1; + + offset += Utils.Decode8u(data, offset, ref cmd); + // IMPORTANT kcp encodes 'frg' as 1 byte. + // so we can only support up to 255 fragments. + // (which limits max message size to around 288 KB) + offset += Utils.Decode8u(data, offset, ref frg); + offset += Utils.Decode16U(data, offset, ref wnd); + offset += Utils.Decode32U(data, offset, ref ts); + offset += Utils.Decode32U(data, offset, ref sn); + offset += Utils.Decode32U(data, offset, ref una); + offset += Utils.Decode32U(data, offset, ref len); + + // subtract the segment bytes from size + size -= OVERHEAD; + + // enough remaining to read 'len' bytes of the actual payload? + // note: original kcp casts uint len to int for <0 check. + if (size < len || (int)len < 0) return -2; + + if (cmd != CMD_PUSH && cmd != CMD_ACK && + cmd != CMD_WASK && cmd != CMD_WINS) + return -3; + + rmt_wnd = wnd; + ParseUna(una); + ShrinkBuf(); + + if (cmd == CMD_ACK) + { + if (Utils.TimeDiff(current, ts) >= 0) + { + UpdateAck(Utils.TimeDiff(current, ts)); + } + ParseAck(sn); + ShrinkBuf(); + if (flag == 0) + { + flag = 1; + maxack = sn; + latest_ts = ts; + } + else + { + if (Utils.TimeDiff(sn, maxack) > 0) + { +#if !FASTACK_CONSERVE + maxack = sn; + latest_ts = ts; +#else + if (Utils.TimeDiff(ts, latest_ts) > 0) + { + maxack = sn; + latest_ts = ts; + } +#endif + } + } + } + else if (cmd == CMD_PUSH) + { + if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) < 0) + { + AckPush(sn, ts); + if (Utils.TimeDiff(sn, rcv_nxt) >= 0) + { + Segment seg = SegmentNew(); + seg.conv = conv_; + seg.cmd = cmd; + seg.frg = frg; + seg.wnd = wnd; + seg.ts = ts; + seg.sn = sn; + seg.una = una; + if (len > 0) + { + seg.data.Write(data, offset, (int)len); + } + ParseData(seg); + } + } + } + else if (cmd == CMD_WASK) + { + // ready to send back CMD_WINS in flush + // tell remote my window size + probe |= ASK_TELL; + } + else if (cmd == CMD_WINS) + { + // do nothing + } + else + { + return -3; + } + + offset += (int)len; + size -= (int)len; + } + + if (flag != 0) + { + ParseFastack(maxack, latest_ts); + } + + // cwnd update when packet arrived + if (Utils.TimeDiff(snd_una, prev_una) > 0) + { + if (cwnd < rmt_wnd) + { + if (cwnd < ssthresh) + { + cwnd++; + incr += mss; + } + else + { + if (incr < mss) incr = mss; + incr += (mss * mss) / incr + (mss / 16); + if ((cwnd + 1) * mss <= incr) + { + cwnd = (incr + mss - 1) / ((mss > 0) ? mss : 1); + } + } + if (cwnd > rmt_wnd) + { + cwnd = rmt_wnd; + incr = rmt_wnd * mss; + } + } + } + + return 0; + } + + // ikcp_wnd_unused + uint WndUnused() + { + if (rcv_queue.Count < rcv_wnd) + return rcv_wnd - (uint)rcv_queue.Count; + return 0; + } + + // ikcp_flush + // flush remain ack segments + public void Flush() + { + int offset = 0; // buffer ptr in original C + bool lost = false; // lost segments + + // helper functions + void MakeSpace(int space) + { + if (offset + space > mtu) + { + output(buffer, offset); + offset = 0; + } + } + + void FlushBuffer() + { + if (offset > 0) + { + output(buffer, offset); + } + } + + // 'ikcp_update' haven't been called. + if (!updated) return; + + // kcp only stack allocates a segment here for performance, leaving + // its data buffer null because this segment's data buffer is never + // used. that's fine in C, but in C# our segment is class so we need + // to allocate and most importantly, not forget to deallocate it + // before returning. + Segment seg = SegmentNew(); + seg.conv = conv; + seg.cmd = CMD_ACK; + seg.wnd = WndUnused(); + seg.una = rcv_nxt; + + // flush acknowledges + foreach (AckItem ack in acklist) + { + MakeSpace(OVERHEAD); + // ikcp_ack_get assigns ack[i] to seg.sn, seg.ts + seg.sn = ack.serialNumber; + seg.ts = ack.timestamp; + offset += seg.Encode(buffer, offset); + } + + acklist.Clear(); + + // probe window size (if remote window size equals zero) + if (rmt_wnd == 0) + { + if (probe_wait == 0) + { + probe_wait = PROBE_INIT; + ts_probe = current + probe_wait; + } + else + { + if (Utils.TimeDiff(current, ts_probe) >= 0) + { + if (probe_wait < PROBE_INIT) + probe_wait = PROBE_INIT; + probe_wait += probe_wait / 2; + if (probe_wait > PROBE_LIMIT) + probe_wait = PROBE_LIMIT; + ts_probe = current + probe_wait; + probe |= ASK_SEND; + } + } + } + else + { + ts_probe = 0; + probe_wait = 0; + } + + // flush window probing commands + if ((probe & ASK_SEND) != 0) + { + seg.cmd = CMD_WASK; + MakeSpace(OVERHEAD); + offset += seg.Encode(buffer, offset); + } + + // flush window probing commands + if ((probe & ASK_TELL) != 0) + { + seg.cmd = CMD_WINS; + MakeSpace(OVERHEAD); + offset += seg.Encode(buffer, offset); + } + + probe = 0; + + // calculate window size + uint cwnd_ = Math.Min(snd_wnd, rmt_wnd); + // if congestion window: + if (!nocwnd) cwnd_ = Math.Min(cwnd, cwnd_); + + // move data from snd_queue to snd_buf + // sliding window, controlled by snd_nxt && sna_una+cwnd + // + // ELI5: 'snd_nxt' is what we want to send. + // 'snd_una' is what hasn't been acked yet. + // copy up to 'cwnd_' difference between them (sliding window) + while (Utils.TimeDiff(snd_nxt, snd_una + cwnd_) < 0) + { + if (snd_queue.Count == 0) break; + + Segment newseg = snd_queue.Dequeue(); + + newseg.conv = conv; + newseg.cmd = CMD_PUSH; + newseg.wnd = seg.wnd; + newseg.ts = current; + newseg.sn = snd_nxt++; + newseg.una = rcv_nxt; + newseg.resendts = current; + newseg.rto = rx_rto; + newseg.fastack = 0; + newseg.xmit = 0; + snd_buf.Add(newseg); + } + + // calculate resent + uint resent = fastresend > 0 ? (uint)fastresend : 0xffffffff; + uint rtomin = nodelay == 0 ? (uint)rx_rto >> 3 : 0; + + // flush data segments + int change = 0; + foreach (Segment segment in snd_buf) + { + bool needsend = false; + // initial transmit + if (segment.xmit == 0) + { + needsend = true; + segment.xmit++; + segment.rto = rx_rto; + segment.resendts = current + (uint)segment.rto + rtomin; + } + // RTO + else if (Utils.TimeDiff(current, segment.resendts) >= 0) + { + needsend = true; + segment.xmit++; + xmit++; + if (nodelay == 0) + { + segment.rto += Math.Max(segment.rto, rx_rto); + } + else + { + int step = (nodelay < 2) ? segment.rto : rx_rto; + segment.rto += step / 2; + } + segment.resendts = current + (uint)segment.rto; + lost = true; + } + // fast retransmit + else if (segment.fastack >= resent) + { + if (segment.xmit <= fastlimit || fastlimit <= 0) + { + needsend = true; + segment.xmit++; + segment.fastack = 0; + segment.resendts = current + (uint)segment.rto; + change++; + } + } + + if (needsend) + { + segment.ts = current; + segment.wnd = seg.wnd; + segment.una = rcv_nxt; + + int need = OVERHEAD + (int)segment.data.Position; + MakeSpace(need); + + offset += segment.Encode(buffer, offset); + + if (segment.data.Position > 0) + { + Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, offset, (int)segment.data.Position); + offset += (int)segment.data.Position; + } + + // dead link happens if a message was resent N times, but an + // ack was still not received. + if (segment.xmit >= dead_link) + { + state = -1; + } + } + } + + // kcp stackallocs 'seg'. our C# segment is a class though, so we + // need to properly delete and return it to the pool now that we are + // done with it. + SegmentDelete(seg); + + // flash remain segments + FlushBuffer(); + + // update ssthresh + // rate halving, https://tools.ietf.org/html/rfc6937 + if (change > 0) + { + uint inflight = snd_nxt - snd_una; + ssthresh = inflight / 2; + if (ssthresh < THRESH_MIN) + ssthresh = THRESH_MIN; + cwnd = ssthresh + resent; + incr = cwnd * mss; + } + + // congestion control, https://tools.ietf.org/html/rfc5681 + if (lost) + { + // original C uses 'cwnd', not kcp->cwnd! + ssthresh = cwnd_ / 2; + if (ssthresh < THRESH_MIN) + ssthresh = THRESH_MIN; + cwnd = 1; + incr = mss; + } + + if (cwnd < 1) + { + cwnd = 1; + incr = mss; + } + } + + // ikcp_update + // update state (call it repeatedly, every 10ms-100ms), or you can ask + // Check() when to call it again (without Input/Send calling). + // + // 'current' - current timestamp in millisec. pass it to Kcp so that + // Kcp doesn't have to do any stopwatch/deltaTime/etc. code + public void Update(uint currentTimeMilliSeconds) + { + current = currentTimeMilliSeconds; + + if (!updated) + { + updated = true; + ts_flush = current; + } + + int slap = Utils.TimeDiff(current, ts_flush); + + if (slap >= 10000 || slap < -10000) + { + ts_flush = current; + slap = 0; + } + + if (slap >= 0) + { + ts_flush += interval; + if (Utils.TimeDiff(current, ts_flush) >= 0) + { + ts_flush = current + interval; + } + Flush(); + } + } + + // ikcp_check + // Determine when should you invoke update + // Returns when you should invoke update in millisec, if there is no + // input/send calling. you can call update in that time, instead of + // call update repeatly. + // + // Important to reduce unnecessary update invoking. use it to schedule + // update (e.g. implementing an epoll-like mechanism, or optimize update + // when handling massive kcp connections). + public uint Check(uint current_) + { + uint ts_flush_ = ts_flush; + int tm_flush = 0x7fffffff; + int tm_packet = 0x7fffffff; + + if (!updated) + { + return current_; + } + + if (Utils.TimeDiff(current_, ts_flush_) >= 10000 || + Utils.TimeDiff(current_, ts_flush_) < -10000) + { + ts_flush_ = current_; + } + + if (Utils.TimeDiff(current_, ts_flush_) >= 0) + { + return current_; + } + + tm_flush = Utils.TimeDiff(ts_flush_, current_); + + foreach (Segment seg in snd_buf) + { + int diff = Utils.TimeDiff(seg.resendts, current_); + if (diff <= 0) + { + return current_; + } + if (diff < tm_packet) tm_packet = diff; + } + + uint minimal = (uint)(tm_packet < tm_flush ? tm_packet : tm_flush); + if (minimal >= interval) minimal = interval; + + return current_ + minimal; + } + + // ikcp_setmtu + // Change MTU (Maximum Transmission Unit) size. + public void SetMtu(uint mtu) + { + if (mtu < 50 || mtu < OVERHEAD) + throw new ArgumentException("MTU must be higher than 50 and higher than OVERHEAD"); + + buffer = new byte[(mtu + OVERHEAD) * 3]; + this.mtu = mtu; + mss = mtu - OVERHEAD; + } + + // ikcp_interval + public void SetInterval(uint interval) + { + if (interval > 5000) interval = 5000; + else if (interval < 10) interval = 10; + this.interval = interval; + } + + // ikcp_nodelay + // configuration: https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration + // nodelay : Whether nodelay mode is enabled, 0 is not enabled; 1 enabled. + // interval :Protocol internal work interval, in milliseconds, such as 10 ms or 20 ms. + // resend :Fast retransmission mode, 0 represents off by default, 2 can be set (2 ACK spans will result in direct retransmission) + // nc :Whether to turn off flow control, 0 represents “Do not turn off” by default, 1 represents “Turn off”. + // Normal Mode: ikcp_nodelay(kcp, 0, 40, 0, 0); + // Turbo Mode: ikcp_nodelay(kcp, 1, 10, 2, 1); + public void SetNoDelay(uint nodelay, uint interval = INTERVAL, int resend = 0, bool nocwnd = false) + { + this.nodelay = nodelay; + if (nodelay != 0) + { + rx_minrto = RTO_NDL; + } + else + { + rx_minrto = RTO_MIN; + } + + if (interval >= 0) + { + if (interval > 5000) interval = 5000; + else if (interval < 10) interval = 10; + this.interval = interval; + } + + if (resend >= 0) + { + fastresend = resend; + } + + this.nocwnd = nocwnd; + } + + // ikcp_wndsize + public void SetWindowSize(uint sendWindow, uint receiveWindow) + { + if (sendWindow > 0) + { + snd_wnd = sendWindow; + } + + if (receiveWindow > 0) + { + // must >= max fragment size + rcv_wnd = Math.Max(receiveWindow, WND_RCV); + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta new file mode 100644 index 0000000..935b423 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a59b1cae10a334faf807432ab472f212 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs new file mode 100644 index 0000000..81b5289 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs @@ -0,0 +1,46 @@ +// Pool to avoid allocations (from libuv2k & Mirror) +using System; +using System.Collections.Generic; + +namespace kcp2k +{ + public class Pool + { + // Mirror is single threaded, no need for concurrent collections + readonly Stack objects = new Stack(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + // some types might need additional cleanup for returned objects + readonly Action objectResetter; + + public Pool(Func objectGenerator, Action objectResetter, int initialCapacity) + { + this.objectGenerator = objectGenerator; + this.objectResetter = objectResetter; + + // allocate an initial pool so we have fewer (if any) + // allocations in the first few frames (or seconds). + for (int i = 0; i < initialCapacity; ++i) + objects.Push(objectGenerator()); + } + + // take an element from the pool, or create a new one if empty + public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator(); + + // return an element to the pool + public void Return(T item) + { + objectResetter(item); + objects.Push(item); + } + + // clear the pool + public void Clear() => objects.Clear(); + + // count to see how many objects are in the pool. useful for tests. + public int Count => objects.Count; + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs.meta new file mode 100644 index 0000000..5eba0e0 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35c07818fc4784bb4ba472c8e5029002 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs new file mode 100644 index 0000000..b82935a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs @@ -0,0 +1,65 @@ +using System.IO; + +namespace kcp2k +{ + // KCP Segment Definition + internal class Segment + { + internal uint conv; // conversation + internal uint cmd; // command, e.g. Kcp.CMD_ACK etc. + internal uint frg; // fragment (sent as 1 byte) + internal uint wnd; // window size that the receive can currently receive + internal uint ts; // timestamp + internal uint sn; // serial number + internal uint una; + internal uint resendts; // resend timestamp + internal int rto; + internal uint fastack; + internal uint xmit; // retransmit count + + // we need an auto scaling byte[] with a WriteBytes function. + // MemoryStream does that perfectly, no need to reinvent the wheel. + // note: no need to pool it, because Segment is already pooled. + // -> MTU as initial capacity to avoid most runtime resizing/allocations + internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF); + + // ikcp_encode_seg + // encode a segment into buffer + internal int Encode(byte[] ptr, int offset) + { + int offset_ = offset; + offset += Utils.Encode32U(ptr, offset, conv); + offset += Utils.Encode8u(ptr, offset, (byte)cmd); + // IMPORTANT kcp encodes 'frg' as 1 byte. + // so we can only support up to 255 fragments. + // (which limits max message size to around 288 KB) + offset += Utils.Encode8u(ptr, offset, (byte)frg); + offset += Utils.Encode16U(ptr, offset, (ushort)wnd); + offset += Utils.Encode32U(ptr, offset, ts); + offset += Utils.Encode32U(ptr, offset, sn); + offset += Utils.Encode32U(ptr, offset, una); + offset += Utils.Encode32U(ptr, offset, (uint)data.Position); + + return offset - offset_; + } + + // reset to return a fresh segment to the pool + internal void Reset() + { + conv = 0; + cmd = 0; + frg = 0; + wnd = 0; + ts = 0; + sn = 0; + una = 0; + rto = 0; + xmit = 0; + resendts = 0; + fastack = 0; + + // keep buffer for next pool usage, but reset length (= bytes written) + data.SetLength(0); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs.meta new file mode 100644 index 0000000..d14dc1a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc58706a05dd3442c8fde858d5266855 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs new file mode 100644 index 0000000..45dc1a6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs @@ -0,0 +1,76 @@ +using System.Runtime.CompilerServices; + +namespace kcp2k +{ + public static partial class Utils + { + // Clamp so we don't have to depend on UnityEngine + public static int Clamp(int value, int min, int max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + // encode 8 bits unsigned int + public static int Encode8u(byte[] p, int offset, byte c) + { + p[0 + offset] = c; + return 1; + } + + // decode 8 bits unsigned int + public static int Decode8u(byte[] p, int offset, ref byte c) + { + c = p[0 + offset]; + return 1; + } + + // encode 16 bits unsigned int (lsb) + public static int Encode16U(byte[] p, int offset, ushort w) + { + p[0 + offset] = (byte)(w >> 0); + p[1 + offset] = (byte)(w >> 8); + return 2; + } + + // decode 16 bits unsigned int (lsb) + public static int Decode16U(byte[] p, int offset, ref ushort c) + { + ushort result = 0; + result |= p[0 + offset]; + result |= (ushort)(p[1 + offset] << 8); + c = result; + return 2; + } + + // encode 32 bits unsigned int (lsb) + public static int Encode32U(byte[] p, int offset, uint l) + { + p[0 + offset] = (byte)(l >> 0); + p[1 + offset] = (byte)(l >> 8); + p[2 + offset] = (byte)(l >> 16); + p[3 + offset] = (byte)(l >> 24); + return 4; + } + + // decode 32 bits unsigned int (lsb) + public static int Decode32U(byte[] p, int offset, ref uint c) + { + uint result = 0; + result |= p[0 + offset]; + result |= (uint)(p[1 + offset] << 8); + result |= (uint)(p[2 + offset] << 16); + result |= (uint)(p[3 + offset] << 24); + c = result; + return 4; + } + + // timediff was a macro in original Kcp. let's inline it if possible. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int TimeDiff(uint later, uint earlier) + { + return (int)(later - earlier); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs.meta new file mode 100644 index 0000000..86118bc --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef959eb716205bd48b050f010a9a35ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta new file mode 100644 index 0000000..5c72cf0 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e9de45e025f26411bbb52d1aefc8d5a5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE new file mode 100644 index 0000000..0330370 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mirror Networking (vis2k, FakeByte) + +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. diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta new file mode 100644 index 0000000..4fadbdf --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a857d4e863bbf4a7dba70bc2cd1b5949 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta new file mode 100644 index 0000000..6878ad8 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b7f3f8e8fa16475bbe48a8e9fbe800b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs new file mode 100644 index 0000000..246a5d1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("where-allocations.Tests")] \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta new file mode 100644 index 0000000..1edb254 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 158a96a7489b450485a8b06a13328871 +timeCreated: 1622356221 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs new file mode 100644 index 0000000..fcf18f6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Net.Sockets; + +namespace WhereAllocation +{ + public static class Extensions + { + // always pass the same IPEndPointNonAlloc instead of allocating a new + // one each time. + // + // use IPEndPointNonAlloc.temp to get the latest SocketAdddress written + // by ReceiveFrom_Internal! + // + // IMPORTANT: .temp will be overwritten in next call! + // hash or manually copy it if you need to store it, e.g. + // when adding a new connection. + public static int ReceiveFrom_NonAlloc( + this Socket socket, + byte[] buffer, + int offset, + int size, + SocketFlags socketFlags, + IPEndPointNonAlloc remoteEndPoint) + { + // call ReceiveFrom with IPEndPointNonAlloc. + // need to wrap this in ReceiveFrom_NonAlloc because it's not + // obvious that IPEndPointNonAlloc.Create does NOT create a new + // IPEndPoint. it saves the result in IPEndPointNonAlloc.temp! + EndPoint casted = remoteEndPoint; + return socket.ReceiveFrom(buffer, offset, size, socketFlags, ref casted); + } + + // same as above, different parameters + public static int ReceiveFrom_NonAlloc(this Socket socket, byte[] buffer, IPEndPointNonAlloc remoteEndPoint) + { + EndPoint casted = remoteEndPoint; + return socket.ReceiveFrom(buffer, ref casted); + } + + // SendTo allocates too: + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L2240 + // -> the allocation is in EndPoint.Serialize() + // NOTE: technically this function isn't necessary. + // could just pass IPEndPointNonAlloc. + // still good for strong typing. + public static int SendTo_NonAlloc( + this Socket socket, + byte[] buffer, + int offset, + int size, + SocketFlags socketFlags, + IPEndPointNonAlloc remoteEndPoint) + { + EndPoint casted = remoteEndPoint; + return socket.SendTo(buffer, offset, size, socketFlags, casted); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta new file mode 100644 index 0000000..c4fa54d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e801942544d44d65808fb250623fe25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs new file mode 100644 index 0000000..65eb453 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs @@ -0,0 +1,208 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace WhereAllocation +{ + public class IPEndPointNonAlloc : IPEndPoint + { + // Two steps to remove allocations in ReceiveFrom_Internal: + // + // 1.) remoteEndPoint.Serialize(): + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1733 + // -> creates an EndPoint for ReceiveFrom_Internal to write into + // -> it's never read from: + // ReceiveFrom_Internal passes it to native: + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1885 + // native recv populates 'sockaddr* from' with the remote address: + // https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom + // -> can NOT be null. bricks both Unity and Unity Hub otherwise. + // -> it seems as if Serialize() is only called to avoid allocating + // a 'new SocketAddress' in ReceiveFrom. it's up to the EndPoint. + // + // 2.) EndPoint.Create(SocketAddress): + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 + // -> SocketAddress is the remote's address that we want to return + // -> to avoid 'new EndPoint(SocketAddress), it seems up to the user + // to decide how to create a new EndPoint via .Create + // -> SocketAddress is the object that was returned by Serialize() + // + // in other words, all we need is an extra SocketAddress field that we + // can pass to ReceiveFrom_Internal to write the result into. + // => callers can then get the result from the extra field! + // => no allocations + // + // IMPORTANT: remember that IPEndPointNonAlloc is always the same object + // and never changes. only the helper field is changed. + public SocketAddress temp; + + // constructors simply create the field once by calling the base method. + // (our overwritten method would create anything new) + public IPEndPointNonAlloc(long address, int port) : base(address, port) + { + temp = base.Serialize(); + } + public IPEndPointNonAlloc(IPAddress address, int port) : base(address, port) + { + temp = base.Serialize(); + } + + // Serialize simply returns it + public override SocketAddress Serialize() => temp; + + // Create doesn't need to create anything. + // SocketAddress object is already the one we returned in Serialize(). + // ReceiveFrom_Internal simply wrote into it. + public override EndPoint Create(SocketAddress socketAddress) + { + // original IPEndPoint.Create validates: + if (socketAddress.Family != AddressFamily) + throw new ArgumentException($"Unsupported socketAddress.AddressFamily: {socketAddress.Family}. Expected: {AddressFamily}"); + if (socketAddress.Size < 8) + throw new ArgumentException($"Unsupported socketAddress.Size: {socketAddress.Size}. Expected: <8"); + + // double check to guarantee that ReceiveFrom actually did write + // into our 'temp' field. just in case that's ever changed. + if (socketAddress != temp) + { + // well this is fun. + // in the latest mono from the above github links, + // the result of Serialize() is passed as 'ref' so ReceiveFrom + // does in fact write into it. + // + // in Unity 2019 LTS's mono version, it does create a new one + // each time. this is from ILSpy Receive_From: + // + // SocketPal.CheckDualModeReceiveSupport(this); + // ValidateBlockingMode(); + // if (NetEventSource.IsEnabled) + // { + // NetEventSource.Info(this, $"SRC{LocalEndPoint} size:{size} remoteEP:{remoteEP}", "ReceiveFrom"); + // } + // EndPoint remoteEP2 = remoteEP; + // System.Net.Internals.SocketAddress socketAddress = SnapshotAndSerialize(ref remoteEP2); + // System.Net.Internals.SocketAddress socketAddress2 = IPEndPointExtensions.Serialize(remoteEP2); + // int bytesTransferred; + // SocketError socketError = SocketPal.ReceiveFrom(_handle, buffer, offset, size, socketFlags, socketAddress.Buffer, ref socketAddress.InternalSize, out bytesTransferred); + // SocketException ex = null; + // if (socketError != 0) + // { + // ex = new SocketException((int)socketError); + // UpdateStatusAfterSocketError(ex); + // if (NetEventSource.IsEnabled) + // { + // NetEventSource.Error(this, ex, "ReceiveFrom"); + // } + // if (ex.SocketErrorCode != SocketError.MessageSize) + // { + // throw ex; + // } + // } + // if (!socketAddress2.Equals(socketAddress)) + // { + // try + // { + // remoteEP = remoteEP2.Create(socketAddress); + // } + // catch + // { + // } + // if (_rightEndPoint == null) + // { + // _rightEndPoint = remoteEP2; + // } + // } + // if (ex != null) + // { + // throw ex; + // } + // if (NetEventSource.IsEnabled) + // { + // NetEventSource.DumpBuffer(this, buffer, offset, size, "ReceiveFrom"); + // NetEventSource.Exit(this, bytesTransferred, "ReceiveFrom"); + // } + // return bytesTransferred; + // + + // so until they upgrade their mono version, we are stuck with + // some allocations. + // + // for now, let's pass the newly created on to our temp so at + // least we reuse it next time. + temp = socketAddress; + + // SocketAddress.GetHashCode() depends on SocketAddress.m_changed. + // ReceiveFrom only sets the buffer, it does not seem to set m_changed. + // we need to reset m_changed for two reasons: + // * if m_changed is false, GetHashCode() returns the cahced m_hash + // which is '0'. that would be a problem. + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L262 + // * if we have a cached m_hash, but ReceiveFrom modified the buffer + // then the GetHashCode() should change too. so we need to reset + // either way. + // + // the only way to do that is by _actually_ modifying the buffer: + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L99 + // so let's do that. + // -> unchecked in case it's byte.Max + unchecked + { + temp[0] += 1; + temp[0] -= 1; + } + + // make sure this worked. + // at least throw an Exception to make it obvious if the trick does + // not work anymore, in case ReceiveFrom is ever changed. + if (temp.GetHashCode() == 0) + throw new Exception($"SocketAddress GetHashCode() is 0 after ReceiveFrom. Does the m_changed trick not work anymore?"); + + // in the future, enable this again: + //throw new Exception($"Socket.ReceiveFrom(): passed SocketAddress={socketAddress} but expected {temp}. This should never happen. Did ReceiveFrom() change?"); + } + + // ReceiveFrom sets seed_endpoint to the result of Create(): + // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1764 + // so let's return ourselves at least. + // (seed_endpoint only seems to matter for BeginSend etc.) + return this; + } + + // we need to overwrite GetHashCode() for two reasons. + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPEndPoint.cs#L160 + // * it uses m_Address. but our true SocketAddress is in m_temp. + // m_Address might not be set at all. + // * m_Address.GetHashCode() allocates: + // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 + public override int GetHashCode() => temp.GetHashCode(); + + // helper function to create an ACTUAL new IPEndPoint from this. + // server needs it to store new connections as unique IPEndPoints. + public IPEndPoint DeepCopyIPEndPoint() + { + // we need to create a new IPEndPoint from 'temp' SocketAddress. + // there is no 'new IPEndPoint(SocketAddress) constructor. + // so we need to be a bit creative... + + // allocate a placeholder IPAddress to copy + // our SocketAddress into. + // -> needs to be the same address family. + IPAddress ipAddress; + if (temp.Family == AddressFamily.InterNetworkV6) + ipAddress = IPAddress.IPv6Any; + else if (temp.Family == AddressFamily.InterNetwork) + ipAddress = IPAddress.Any; + else + throw new Exception($"Unexpected SocketAddress family: {temp.Family}"); + + // allocate a placeholder IPEndPoint + // with the needed size form IPAddress. + // (the real class. not NonAlloc) + IPEndPoint placeholder = new IPEndPoint(ipAddress, 0); + + // the real IPEndPoint's .Create function can create a new IPEndPoint + // copy from a SocketAddress. + return (IPEndPoint)placeholder.Create(temp); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta new file mode 100644 index 0000000..ef424ba --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af0279d15e39b484792394f1d3cad4d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef new file mode 100644 index 0000000..a185c2b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef @@ -0,0 +1,13 @@ +{ + "name": "where-allocations", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta new file mode 100644 index 0000000..ce96c63 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 63c380d6dae6946209ed0832388a657c +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION new file mode 100644 index 0000000..8341d28 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION @@ -0,0 +1,2 @@ +V0.1 [2021-06-01] +- initial release \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta new file mode 100644 index 0000000..67ab688 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f1256cadc037546ccb66071784fce137 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/LatencySimulation.cs b/Assets/Mirror/Runtime/Transports/LatencySimulation.cs new file mode 100644 index 0000000..2feb073 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/LatencySimulation.cs @@ -0,0 +1,284 @@ +// wraps around a transport and adds latency/loss/scramble simulation. +// +// reliable: latency +// unreliable: latency, loss, scramble (unreliable isn't ordered so we scramble) +// +// IMPORTANT: use Time.unscaledTime instead of Time.time. +// some games might have Time.timeScale modified. +// see also: https://github.com/vis2k/Mirror/issues/2907 +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + struct QueuedMessage + { + public int connectionId; + public byte[] bytes; + public float time; + } + + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/latency-simulaton-transport")] + [DisallowMultipleComponent] + public class LatencySimulation : Transport + { + public Transport wrap; + + [Header("Common")] + [Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")] + [Range(0, 1)] public float latencySpikeMultiplier; + [Tooltip("Spike latency via perlin(Time * speedMultiplier) * spikeMultiplier")] + public float latencySpikeSpeedMultiplier = 1; + + [Header("Reliable Messages")] + [Tooltip("Reliable latency in seconds")] + public float reliableLatency; + // note: packet loss over reliable manifests itself in latency. + // don't need (and can't add) a loss option here. + // note: reliable is ordered by definition. no need to scramble. + + [Header("Unreliable Messages")] + [Tooltip("Packet loss in %")] + [Range(0, 1)] public float unreliableLoss; + [Tooltip("Unreliable latency in seconds")] + public float unreliableLatency; + [Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")] + [Range(0, 1)] public float unreliableScramble; + + // message queues + // list so we can insert randomly (scramble) + List reliableClientToServer = new List(); + List reliableServerToClient = new List(); + List unreliableClientToServer = new List(); + List unreliableServerToClient = new List(); + + // random + // UnityEngine.Random.value is [0, 1] with both upper and lower bounds inclusive + // but we need the upper bound to be exclusive, so using System.Random instead. + // => NextDouble() is NEVER < 0 so loss=0 never drops! + // => NextDouble() is ALWAYS < 1 so loss=1 always drops! + System.Random random = new System.Random(); + + public void Awake() + { + if (wrap == null) + throw new Exception("PressureDrop requires an underlying transport to wrap around."); + } + + // forward enable/disable to the wrapped transport + void OnEnable() { wrap.enabled = true; } + void OnDisable() { wrap.enabled = false; } + + // noise function can be replaced if needed + protected virtual float Noise(float time) => Mathf.PerlinNoise(time, time); + + // helper function to simulate latency + float SimulateLatency(int channeldId) + { + // spike over perlin noise. + // no spikes isn't realistic. + // sin is too predictable / no realistic. + // perlin is still deterministic and random enough. + float spike = Noise(Time.unscaledTime * latencySpikeSpeedMultiplier) * latencySpikeMultiplier; + + // base latency + switch (channeldId) + { + case Channels.Reliable: + return reliableLatency + spike; + case Channels.Unreliable: + return unreliableLatency + spike; + default: + return 0; + } + } + + // helper function to simulate a send with latency/loss/scramble + void SimulateSend(int connectionId, ArraySegment segment, int channelId, float latency, List reliableQueue, List unreliableQueue) + { + // segment is only valid after returning. copy it. + // (allocates for now. it's only for testing anyway.) + byte[] bytes = new byte[segment.Count]; + Buffer.BlockCopy(segment.Array, segment.Offset, bytes, 0, segment.Count); + + // enqueue message. send after latency interval. + QueuedMessage message = new QueuedMessage + { + connectionId = connectionId, + bytes = bytes, + time = Time.unscaledTime + latency + }; + + switch (channelId) + { + case Channels.Reliable: + // simulate latency + reliableQueue.Add(message); + break; + case Channels.Unreliable: + // simulate packet loss + bool drop = random.NextDouble() < unreliableLoss; + if (!drop) + { + // simulate scramble (Random.Next is < max, so +1) + bool scramble = random.NextDouble() < unreliableScramble; + int last = unreliableQueue.Count; + int index = scramble ? random.Next(0, last + 1) : last; + + // simulate latency + unreliableQueue.Insert(index, message); + } + break; + default: + Debug.LogError($"{nameof(LatencySimulation)} unexpected channelId: {channelId}"); + break; + } + } + + public override bool Available() => wrap.Available(); + + public override void ClientConnect(string address) + { + wrap.OnClientConnected = OnClientConnected; + wrap.OnClientDataReceived = OnClientDataReceived; + wrap.OnClientError = OnClientError; + wrap.OnClientDisconnected = OnClientDisconnected; + wrap.ClientConnect(address); + } + + public override void ClientConnect(Uri uri) + { + wrap.OnClientConnected = OnClientConnected; + wrap.OnClientDataReceived = OnClientDataReceived; + wrap.OnClientError = OnClientError; + wrap.OnClientDisconnected = OnClientDisconnected; + wrap.ClientConnect(uri); + } + + public override bool ClientConnected() => wrap.ClientConnected(); + + public override void ClientDisconnect() + { + wrap.ClientDisconnect(); + reliableClientToServer.Clear(); + unreliableClientToServer.Clear(); + } + + public override void ClientSend(ArraySegment segment, int channelId) + { + float latency = SimulateLatency(channelId); + SimulateSend(0, segment, channelId, latency, reliableClientToServer, unreliableClientToServer); + } + + public override Uri ServerUri() => wrap.ServerUri(); + + public override bool ServerActive() => wrap.ServerActive(); + + public override string ServerGetClientAddress(int connectionId) => wrap.ServerGetClientAddress(connectionId); + + public override void ServerDisconnect(int connectionId) => wrap.ServerDisconnect(connectionId); + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + float latency = SimulateLatency(channelId); + SimulateSend(connectionId, segment, channelId, latency, reliableServerToClient, unreliableServerToClient); + } + + public override void ServerStart() + { + wrap.OnServerConnected = OnServerConnected; + wrap.OnServerDataReceived = OnServerDataReceived; + wrap.OnServerError = OnServerError; + wrap.OnServerDisconnected = OnServerDisconnected; + wrap.ServerStart(); + } + + public override void ServerStop() + { + wrap.ServerStop(); + reliableServerToClient.Clear(); + unreliableServerToClient.Clear(); + } + + public override void ClientEarlyUpdate() => wrap.ClientEarlyUpdate(); + public override void ServerEarlyUpdate() => wrap.ServerEarlyUpdate(); + public override void ClientLateUpdate() + { + // flush reliable messages after latency + while (reliableClientToServer.Count > 0) + { + // check the first message time + QueuedMessage message = reliableClientToServer[0]; + if (message.time <= Time.unscaledTime) + { + // send and eat + wrap.ClientSend(new ArraySegment(message.bytes), Channels.Reliable); + reliableClientToServer.RemoveAt(0); + } + // not enough time elapsed yet + break; + } + + // flush unreliable messages after latency + while (unreliableClientToServer.Count > 0) + { + // check the first message time + QueuedMessage message = unreliableClientToServer[0]; + if (message.time <= Time.unscaledTime) + { + // send and eat + wrap.ClientSend(new ArraySegment(message.bytes), Channels.Unreliable); + unreliableClientToServer.RemoveAt(0); + } + // not enough time elapsed yet + break; + } + + // update wrapped transport too + wrap.ClientLateUpdate(); + } + public override void ServerLateUpdate() + { + // flush reliable messages after latency + while (reliableServerToClient.Count > 0) + { + // check the first message time + QueuedMessage message = reliableServerToClient[0]; + if (message.time <= Time.unscaledTime) + { + // send and eat + wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Reliable); + reliableServerToClient.RemoveAt(0); + } + // not enough time elapsed yet + break; + } + + // flush unreliable messages after latency + while (unreliableServerToClient.Count > 0) + { + // check the first message time + QueuedMessage message = unreliableServerToClient[0]; + if (message.time <= Time.unscaledTime) + { + // send and eat + wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), Channels.Unreliable); + unreliableServerToClient.RemoveAt(0); + } + // not enough time elapsed yet + break; + } + + // update wrapped transport too + wrap.ServerLateUpdate(); + } + + public override int GetBatchThreshold(int channelId) => wrap.GetBatchThreshold(channelId); + public override int GetMaxPacketSize(int channelId = 0) => wrap.GetMaxPacketSize(channelId); + + public override void Shutdown() => wrap.Shutdown(); + + public override string ToString() => $"{nameof(LatencySimulation)} {wrap}"; + } +} diff --git a/Assets/Mirror/Runtime/Transports/LatencySimulation.cs.meta b/Assets/Mirror/Runtime/Transports/LatencySimulation.cs.meta new file mode 100644 index 0000000..eabbe4a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/LatencySimulation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96b149f511061407fb54895c057b7736 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs b/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs new file mode 100644 index 0000000..7dd934a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs @@ -0,0 +1,61 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// Allows Middleware to override some of the transport methods or let the inner transport handle them. + /// + [DisallowMultipleComponent] + public abstract class MiddlewareTransport : Transport + { + /// + /// Transport to call to after middleware + /// + public Transport inner; + + public override bool Available() => inner.Available(); + public override int GetMaxPacketSize(int channelId = 0) => inner.GetMaxPacketSize(channelId); + public override int GetBatchThreshold(int channelId = Channels.Reliable) => inner.GetBatchThreshold(channelId); + public override void Shutdown() => inner.Shutdown(); + + #region Client + public override void ClientConnect(string address) + { + inner.OnClientConnected = OnClientConnected; + inner.OnClientDataReceived = OnClientDataReceived; + inner.OnClientDisconnected = OnClientDisconnected; + inner.OnClientError = OnClientError; + inner.ClientConnect(address); + } + + public override bool ClientConnected() => inner.ClientConnected(); + public override void ClientDisconnect() => inner.ClientDisconnect(); + public override void ClientSend(ArraySegment segment, int channelId) => inner.ClientSend(segment, channelId); + + public override void ClientEarlyUpdate() => inner.ClientEarlyUpdate(); + public override void ClientLateUpdate() => inner.ClientLateUpdate(); + #endregion + + #region Server + public override bool ServerActive() => inner.ServerActive(); + public override void ServerStart() + { + inner.OnServerConnected = OnServerConnected; + inner.OnServerDataReceived = OnServerDataReceived; + inner.OnServerDisconnected = OnServerDisconnected; + inner.OnServerError = OnServerError; + inner.ServerStart(); + } + + public override void ServerStop() => inner.ServerStop(); + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) => inner.ServerSend(connectionId, segment, channelId); + public override void ServerDisconnect(int connectionId) => inner.ServerDisconnect(connectionId); + public override string ServerGetClientAddress(int connectionId) => inner.ServerGetClientAddress(connectionId); + public override Uri ServerUri() => inner.ServerUri(); + + public override void ServerEarlyUpdate() => inner.ServerEarlyUpdate(); + public override void ServerLateUpdate() => inner.ServerLateUpdate(); + #endregion + } +} diff --git a/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs.meta b/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs.meta new file mode 100644 index 0000000..dce8378 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46f20ede74658e147a1af57172710de2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs b/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs new file mode 100644 index 0000000..0d0503d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs @@ -0,0 +1,306 @@ +using System; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // a transport that can listen to multiple underlying transport at the same time + [DisallowMultipleComponent] + public class MultiplexTransport : Transport + { + public Transport[] transports; + + Transport available; + + public void Awake() + { + if (transports == null || transports.Length == 0) + { + Debug.LogError("Multiplex transport requires at least 1 underlying transport"); + } + } + + public override void ClientEarlyUpdate() + { + foreach (Transport transport in transports) + { + transport.ClientEarlyUpdate(); + } + } + + public override void ServerEarlyUpdate() + { + foreach (Transport transport in transports) + { + transport.ServerEarlyUpdate(); + } + } + + public override void ClientLateUpdate() + { + foreach (Transport transport in transports) + { + transport.ClientLateUpdate(); + } + } + + public override void ServerLateUpdate() + { + foreach (Transport transport in transports) + { + transport.ServerLateUpdate(); + } + } + + void OnEnable() + { + foreach (Transport transport in transports) + { + transport.enabled = true; + } + } + + void OnDisable() + { + foreach (Transport transport in transports) + { + transport.enabled = false; + } + } + + public override bool Available() + { + // available if any of the transports is available + foreach (Transport transport in transports) + { + if (transport.Available()) + { + return true; + } + } + return false; + } + + #region Client + + public override void ClientConnect(string address) + { + foreach (Transport transport in transports) + { + if (transport.Available()) + { + available = transport; + transport.OnClientConnected = OnClientConnected; + transport.OnClientDataReceived = OnClientDataReceived; + transport.OnClientError = OnClientError; + transport.OnClientDisconnected = OnClientDisconnected; + transport.ClientConnect(address); + return; + } + } + throw new ArgumentException("No transport suitable for this platform"); + } + + public override void ClientConnect(Uri uri) + { + foreach (Transport transport in transports) + { + if (transport.Available()) + { + try + { + available = transport; + transport.OnClientConnected = OnClientConnected; + transport.OnClientDataReceived = OnClientDataReceived; + transport.OnClientError = OnClientError; + transport.OnClientDisconnected = OnClientDisconnected; + transport.ClientConnect(uri); + return; + } + catch (ArgumentException) + { + // transport does not support the schema, just move on to the next one + } + } + } + throw new ArgumentException("No transport suitable for this platform"); + } + + public override bool ClientConnected() + { + return (object)available != null && available.ClientConnected(); + } + + public override void ClientDisconnect() + { + if ((object)available != null) + available.ClientDisconnect(); + } + + public override void ClientSend(ArraySegment segment, int channelId) + { + available.ClientSend(segment, channelId); + } + + #endregion + + #region Server + // connection ids get mapped to base transports + // if we have 3 transports, then + // transport 0 will produce connection ids [0, 3, 6, 9, ...] + // transport 1 will produce connection ids [1, 4, 7, 10, ...] + // transport 2 will produce connection ids [2, 5, 8, 11, ...] + int FromBaseId(int transportId, int connectionId) + { + return connectionId * transports.Length + transportId; + } + + int ToBaseId(int connectionId) + { + return connectionId / transports.Length; + } + + int ToTransportId(int connectionId) + { + return connectionId % transports.Length; + } + + void AddServerCallbacks() + { + // wire all the base transports to my events + for (int i = 0; i < transports.Length; i++) + { + // this is required for the handlers, if I use i directly + // then all the handlers will use the last i + int locali = i; + Transport transport = transports[i]; + + transport.OnServerConnected = (baseConnectionId => + { + OnServerConnected.Invoke(FromBaseId(locali, baseConnectionId)); + }); + + transport.OnServerDataReceived = (baseConnectionId, data, channel) => + { + OnServerDataReceived.Invoke(FromBaseId(locali, baseConnectionId), data, channel); + }; + + transport.OnServerError = (baseConnectionId, error) => + { + OnServerError.Invoke(FromBaseId(locali, baseConnectionId), error); + }; + transport.OnServerDisconnected = baseConnectionId => + { + OnServerDisconnected.Invoke(FromBaseId(locali, baseConnectionId)); + }; + } + } + + // for now returns the first uri, + // should we return all available uris? + public override Uri ServerUri() + { + return transports[0].ServerUri(); + } + + + public override bool ServerActive() + { + // avoid Linq.All allocations + foreach (Transport transport in transports) + { + if (!transport.ServerActive()) + { + return false; + } + } + return true; + } + + public override string ServerGetClientAddress(int connectionId) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + return transports[transportId].ServerGetClientAddress(baseConnectionId); + } + + public override void ServerDisconnect(int connectionId) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + transports[transportId].ServerDisconnect(baseConnectionId); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + + for (int i = 0; i < transports.Length; ++i) + { + if (i == transportId) + { + transports[i].ServerSend(baseConnectionId, segment, channelId); + } + } + } + + public override void ServerStart() + { + foreach (Transport transport in transports) + { + AddServerCallbacks(); + transport.ServerStart(); + } + } + + public override void ServerStop() + { + foreach (Transport transport in transports) + { + transport.ServerStop(); + } + } + #endregion + + public override int GetMaxPacketSize(int channelId = 0) + { + // finding the max packet size in a multiplex environment has to be + // done very carefully: + // * servers run multiple transports at the same time + // * different clients run different transports + // * there should only ever be ONE true max packet size for everyone, + // otherwise a spawn message might be sent to all tcp sockets, but + // be too big for some udp sockets. that would be a debugging + // nightmare and allow for possible exploits and players on + // different platforms seeing a different game state. + // => the safest solution is to use the smallest max size for all + // transports. that will never fail. + int mininumAllowedSize = int.MaxValue; + foreach (Transport transport in transports) + { + int size = transport.GetMaxPacketSize(channelId); + mininumAllowedSize = Mathf.Min(size, mininumAllowedSize); + } + return mininumAllowedSize; + } + + public override void Shutdown() + { + foreach (Transport transport in transports) + { + transport.Shutdown(); + } + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + foreach (Transport transport in transports) + { + builder.AppendLine(transport.ToString()); + } + return builder.ToString().Trim(); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs.meta b/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs.meta new file mode 100644 index 0000000..6e97b28 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 929e3234c7db540b899f00183fc2b1fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport.meta new file mode 100644 index 0000000..5baa80f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3ba68af305d809418d6c6a804939290 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/.cert.example.Json b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/.cert.example.Json new file mode 100644 index 0000000..e303974 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/.cert.example.Json @@ -0,0 +1,8 @@ +{ + "_readme_1": "Make a copy of this file and update the fields below. (readme fields should be deleted)", + "_readme_2": "Include the json file and cert with your server build ONLY (put them outside of asset folder)", + "_readme_path": "path is relative from cwd not this json file", + "_readme_password": "password can be empty or left out", + "path": "./certs/MirrorLocal.pfx", + "password": "" +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs new file mode 100644 index 0000000..7bc5c17 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyVersion("1.3.0")] + +[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")] +[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")] diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs.meta new file mode 100644 index 0000000..028a307 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee9e76201f7665244bd6ab8ea343a83f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md new file mode 100644 index 0000000..d98f014 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md @@ -0,0 +1,48 @@ +# [1.3.0](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.7...v1.3.0) (2022-02-12) + + +### Features + +* Allowing max message size to be increase to int32.max ([#2](https://github.com/James-Frowen/SimpleWebTransport/issues/2)) ([4cc60fd](https://github.com/James-Frowen/SimpleWebTransport/commit/4cc60fd67f3c65d90ced0e6f9f97d15d0368076d)) + +## [1.2.7](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.6...v1.2.7) (2022-02-12) + + +### Bug Fixes + +* fixing ObjectDisposedException in toString ([426de52](https://github.com/James-Frowen/SimpleWebTransport/commit/426de52ee4e98ac6212713b2b2272e3affb8fc99)) + +## [1.2.6](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.5...v1.2.6) (2022-02-02) + + +### Bug Fixes + +* fixing Runtime is not defined for unity 2021 ([945b50d](https://github.com/James-Frowen/SimpleWebTransport/commit/945b50dbad5b71c43e2bdaa4033f87d3f62c5572)) + +## [1.2.5](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.4...v1.2.5) (2022-02-02) + + +### Bug Fixes + +* updating Pointer_stringify to UTF8ToString ([2f5a74b](https://github.com/James-Frowen/SimpleWebTransport/commit/2f5a74ba10865e934be8d3b54ebfdeb14ca491f6)) + +## [1.2.4](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.3...v1.2.4) (2021-12-16) + + +### Bug Fixes + +* adding meta file for changelog ([ba5b164](https://github.com/James-Frowen/SimpleWebTransport/commit/ba5b1647aa5cc69ca80f5b52c542a9b5ee749c7f)) + +## [1.2.3](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.2...v1.2.3) (2021-12-16) + + +### Bug Fixes + +* fixing compile error in assemblyInfo ([7ee8380](https://github.com/James-Frowen/SimpleWebTransport/commit/7ee8380b4daf34d4e12017de55d8be481690046f)) + +## [1.2.2](https://github.com/James-Frowen/SimpleWebTransport/compare/v1.2.1...v1.2.2) (2021-12-16) + + +### Bug Fixes + +* fixing release with empty commit ([068af74](https://github.com/James-Frowen/SimpleWebTransport/commit/068af74f7399354081f25181f90fb060b0fa1524)) diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta new file mode 100644 index 0000000..bc43099 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b0ef23ac1c6a62546bbad5529b3bfdad +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client.meta new file mode 100644 index 0000000..e6e2943 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5faa957b8d9fc314ab7596ccf14750d9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs new file mode 100644 index 0000000..3569af3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using UnityEngine; + +namespace Mirror.SimpleWeb +{ + public enum ClientState + { + NotConnected = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + } + /// + /// Client used to control websockets + /// Base class used by WebSocketClientWebGl and WebSocketClientStandAlone + /// + public abstract class SimpleWebClient + { + public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) + { +#if UNITY_WEBGL && !UNITY_EDITOR + return new WebSocketClientWebGl(maxMessageSize, maxMessagesPerTick); +#else + return new WebSocketClientStandAlone(maxMessageSize, maxMessagesPerTick, tcpConfig); +#endif + } + + readonly int maxMessagesPerTick; + protected readonly int maxMessageSize; + public readonly ConcurrentQueue receiveQueue = new ConcurrentQueue(); + protected readonly BufferPool bufferPool; + + protected ClientState state; + + protected SimpleWebClient(int maxMessageSize, int maxMessagesPerTick) + { + this.maxMessageSize = maxMessageSize; + this.maxMessagesPerTick = maxMessagesPerTick; + bufferPool = new BufferPool(5, 20, maxMessageSize); + } + + public ClientState ConnectionState => state; + + public event Action onConnect; + public event Action onDisconnect; + public event Action> onData; + public event Action onError; + + /// + /// Processes all new messages + /// + public void ProcessMessageQueue() + { + ProcessMessageQueue(null); + } + + /// + /// Processes all messages while is enabled + /// + /// + public void ProcessMessageQueue(MonoBehaviour behaviour) + { + int processedCount = 0; + bool skipEnabled = behaviour == null; + // check enabled every time in case behaviour was disabled after data + while ( + (skipEnabled || behaviour.enabled) && + processedCount < maxMessagesPerTick && + // Dequeue last + receiveQueue.TryDequeue(out Message next) + ) + { + processedCount++; + + switch (next.type) + { + case EventType.Connected: + onConnect?.Invoke(); + break; + case EventType.Data: + onData?.Invoke(next.data.ToSegment()); + next.data.Release(); + break; + case EventType.Disconnected: + onDisconnect?.Invoke(); + break; + case EventType.Error: + onError?.Invoke(next.exception); + break; + } + } + } + + public abstract void Connect(Uri serverAddress); + public abstract void Disconnect(); + public abstract void Send(ArraySegment segment); + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta new file mode 100644 index 0000000..90c361b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13131761a0bf5a64dadeccd700fe26e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone.meta new file mode 100644 index 0000000..bf320c6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9c19d05220a87c4cbbe4d1e422da0aa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs new file mode 100644 index 0000000..e5fccf9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Mirror.SimpleWeb +{ + /// + /// Handles Handshake to the server when it first connects + /// The client handshake does not need buffers to reduce allocations since it only happens once + /// + internal class ClientHandshake + { + public bool TryHandshake(Connection conn, Uri uri) + { + try + { + Stream stream = conn.stream; + + byte[] keyBuffer = new byte[16]; + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(keyBuffer); + } + + string key = Convert.ToBase64String(keyBuffer); + string keySum = key + Constants.HandshakeGUID; + byte[] keySumBytes = Encoding.ASCII.GetBytes(keySum); + Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keySumBytes)}"); + + byte[] keySumHash = SHA1.Create().ComputeHash(keySumBytes); + + string expectedResponse = Convert.ToBase64String(keySumHash); + string handshake = + $"GET {uri.PathAndQuery} HTTP/1.1\r\n" + + $"Host: {uri.Host}:{uri.Port}\r\n" + + $"Upgrade: websocket\r\n" + + $"Connection: Upgrade\r\n" + + $"Sec-WebSocket-Key: {key}\r\n" + + $"Sec-WebSocket-Version: 13\r\n" + + "\r\n"; + byte[] encoded = Encoding.ASCII.GetBytes(handshake); + stream.Write(encoded, 0, encoded.Length); + + byte[] responseBuffer = new byte[1000]; + + int? lengthOrNull = ReadHelper.SafeReadTillMatch(stream, responseBuffer, 0, responseBuffer.Length, Constants.endOfHandshake); + + if (!lengthOrNull.HasValue) + { + Log.Error("Connected closed before handshake"); + return false; + } + + string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value); + + string acceptHeader = "Sec-WebSocket-Accept: "; + int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase) + acceptHeader.Length; + int endIndex = responseString.IndexOf("\r\n", startIndex); + string responseKey = responseString.Substring(startIndex, endIndex - startIndex); + + if (responseKey != expectedResponse) + { + Log.Error($"Response key incorrect, Response:{responseKey} Expected:{expectedResponse}"); + return false; + } + + return true; + } + catch (Exception e) + { + Log.Exception(e); + return false; + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs.meta new file mode 100644 index 0000000..ad3d40d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3ffdcabc9e28f764a94fc4efc82d3e8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs new file mode 100644 index 0000000..be93f6c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; + +namespace Mirror.SimpleWeb +{ + internal class ClientSslHelper + { + internal bool TryCreateStream(Connection conn, Uri uri) + { + NetworkStream stream = conn.client.GetStream(); + if (uri.Scheme != "wss") + { + conn.stream = stream; + return true; + } + + try + { + conn.stream = CreateStream(stream, uri); + return true; + } + catch (Exception e) + { + Log.Error($"Create SSLStream Failed: {e}", false); + return false; + } + } + + Stream CreateStream(NetworkStream stream, Uri uri) + { + SslStream sslStream = new SslStream(stream, true, ValidateServerCertificate); + sslStream.AuthenticateAsClient(uri.Host); + return sslStream; + } + + static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // Do not allow this client to communicate with unauthenticated servers. + + // only accept if no errors + return sslPolicyErrors == SslPolicyErrors.None; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs.meta new file mode 100644 index 0000000..d6be2bb --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46055a75559a79849a750f39a766db61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs new file mode 100644 index 0000000..3414afb --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs @@ -0,0 +1,143 @@ +using System; +using System.Net.Sockets; +using System.Threading; + +namespace Mirror.SimpleWeb +{ + public class WebSocketClientStandAlone : SimpleWebClient + { + readonly ClientSslHelper sslHelper; + readonly ClientHandshake handshake; + readonly TcpConfig tcpConfig; + Connection conn; + + internal WebSocketClientStandAlone(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) : base(maxMessageSize, maxMessagesPerTick) + { +#if UNITY_WEBGL && !UNITY_EDITOR + throw new NotSupportedException(); +#else + sslHelper = new ClientSslHelper(); + handshake = new ClientHandshake(); + this.tcpConfig = tcpConfig; +#endif + } + + public override void Connect(Uri serverAddress) + { + state = ClientState.Connecting; + + // create connection here before thread so that send queue exist before connected + TcpClient client = new TcpClient(); + tcpConfig.ApplyTo(client); + + // create connection object here so dispose correctly disconnects on failed connect + conn = new Connection(client, AfterConnectionDisposed); + + Thread receiveThread = new Thread(() => ConnectAndReceiveLoop(serverAddress)); + receiveThread.IsBackground = true; + receiveThread.Start(); + } + + void ConnectAndReceiveLoop(Uri serverAddress) + { + try + { + // connection created above + TcpClient client = conn.client; + conn.receiveThread = Thread.CurrentThread; + + try + { + client.Connect(serverAddress.Host, serverAddress.Port); + } + catch (SocketException) + { + client.Dispose(); + throw; + } + + + bool success = sslHelper.TryCreateStream(conn, serverAddress); + if (!success) + { + Log.Warn("Failed to create Stream"); + conn.Dispose(); + return; + } + + success = handshake.TryHandshake(conn, serverAddress); + if (!success) + { + Log.Warn("Failed Handshake"); + conn.Dispose(); + return; + } + + Log.Info("HandShake Successful"); + + state = ClientState.Connected; + + receiveQueue.Enqueue(new Message(EventType.Connected)); + + Thread sendThread = new Thread(() => + { + SendLoop.Config sendConfig = new SendLoop.Config( + conn, + bufferSize: Constants.HeaderSize + Constants.MaskSize + maxMessageSize, + setMask: true); + + SendLoop.Loop(sendConfig); + }); + + conn.sendThread = sendThread; + sendThread.IsBackground = true; + sendThread.Start(); + + ReceiveLoop.Config config = new ReceiveLoop.Config(conn, + maxMessageSize, + false, + receiveQueue, + bufferPool); + ReceiveLoop.Loop(config); + } + catch (ThreadInterruptedException e) { Log.InfoException(e); } + catch (ThreadAbortException e) { Log.InfoException(e); } + catch (Exception e) { Log.Exception(e); } + finally + { + // close here in case connect fails + conn?.Dispose(); + } + } + + void AfterConnectionDisposed(Connection conn) + { + state = ClientState.NotConnected; + // make sure Disconnected event is only called once + receiveQueue.Enqueue(new Message(EventType.Disconnected)); + } + + public override void Disconnect() + { + state = ClientState.Disconnecting; + Log.Info("Disconnect Called"); + if (conn == null) + { + state = ClientState.NotConnected; + } + else + { + conn?.Dispose(); + } + } + + public override void Send(ArraySegment segment) + { + ArrayBuffer buffer = bufferPool.Take(segment.Count); + buffer.CopyFrom(segment); + + conn.sendQueue.Enqueue(buffer); + conn.sendPending.Set(); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs.meta new file mode 100644 index 0000000..37229d3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05a9c87dea309e241a9185e5aa0d72ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl.meta new file mode 100644 index 0000000..2d81f7f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7142349d566213c4abc763afaf4d91a1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs new file mode 100644 index 0000000..6af4671 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs @@ -0,0 +1,34 @@ +using System; +#if UNITY_WEBGL +using System.Runtime.InteropServices; +#endif + +namespace Mirror.SimpleWeb +{ + internal static class SimpleWebJSLib + { +#if UNITY_WEBGL + [DllImport("__Internal")] + internal static extern bool IsConnected(int index); + +#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments + [DllImport("__Internal")] +#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments + internal static extern int Connect(string address, Action openCallback, Action closeCallBack, Action messageCallback, Action errorCallback); + + [DllImport("__Internal")] + internal static extern void Disconnect(int index); + + [DllImport("__Internal")] + internal static extern bool Send(int index, byte[] array, int offset, int length); +#else + internal static bool IsConnected(int index) => throw new NotSupportedException(); + + internal static int Connect(string address, Action openCallback, Action closeCallBack, Action messageCallback, Action errorCallback) => throw new NotSupportedException(); + + internal static void Disconnect(int index) => throw new NotSupportedException(); + + internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException(); +#endif + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs.meta new file mode 100644 index 0000000..9dfa12e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97b96a0b65c104443977473323c2ff35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs new file mode 100644 index 0000000..ece94d6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AOT; + +namespace Mirror.SimpleWeb +{ + public class WebSocketClientWebGl : SimpleWebClient + { + static readonly Dictionary instances = new Dictionary(); + + /// + /// key for instances sent between c# and js + /// + int index; + + /// + /// Queue for messages sent by high level while still connecting, they will be sent after onOpen is called. + /// + /// This is a workaround for anything that calls Send immediately after Connect. + /// Without this the JS websocket will give errors. + /// + /// + Queue ConnectingSendQueue; + + internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick) + { +#if !UNITY_WEBGL || UNITY_EDITOR + throw new NotSupportedException(); +#endif + } + + public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index); + + public override void Connect(Uri serverAddress) + { + index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback); + instances.Add(index, this); + state = ClientState.Connecting; + } + + public override void Disconnect() + { + state = ClientState.Disconnecting; + // disconnect should cause closeCallback and OnDisconnect to be called + SimpleWebJSLib.Disconnect(index); + } + + public override void Send(ArraySegment segment) + { + if (segment.Count > maxMessageSize) + { + Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}"); + return; + } + + if (state == ClientState.Connected) + { + SimpleWebJSLib.Send(index, segment.Array, segment.Offset, segment.Count); + } + else + { + if (ConnectingSendQueue == null) + ConnectingSendQueue = new Queue(); + ConnectingSendQueue.Enqueue(segment.ToArray()); + } + } + + void onOpen() + { + receiveQueue.Enqueue(new Message(EventType.Connected)); + state = ClientState.Connected; + + if (ConnectingSendQueue != null) + { + while (ConnectingSendQueue.Count > 0) + { + byte[] next = ConnectingSendQueue.Dequeue(); + SimpleWebJSLib.Send(index, next, 0, next.Length); + } + ConnectingSendQueue = null; + } + } + + void onClose() + { + // this code should be last in this class + + receiveQueue.Enqueue(new Message(EventType.Disconnected)); + state = ClientState.NotConnected; + instances.Remove(index); + } + + void onMessage(IntPtr bufferPtr, int count) + { + try + { + ArrayBuffer buffer = bufferPool.Take(count); + buffer.CopyFrom(bufferPtr, count); + + receiveQueue.Enqueue(new Message(buffer)); + } + catch (Exception e) + { + Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}"); + receiveQueue.Enqueue(new Message(e)); + } + } + + void onErr() + { + receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error"))); + Disconnect(); + } + + [MonoPInvokeCallback(typeof(Action))] + static void OpenCallback(int index) => instances[index].onOpen(); + + [MonoPInvokeCallback(typeof(Action))] + static void CloseCallBack(int index) => instances[index].onClose(); + + [MonoPInvokeCallback(typeof(Action))] + static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count); + + [MonoPInvokeCallback(typeof(Action))] + static void ErrorCallback(int index) => instances[index].onErr(); + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs.meta new file mode 100644 index 0000000..3827d3a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 015c5b1915fd1a64cbe36444d16b2f7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin.meta new file mode 100644 index 0000000..b516a8f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1999985791b91b9458059e88404885a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib new file mode 100644 index 0000000..02e6b93 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib @@ -0,0 +1,114 @@ +// this will create a global object +const SimpleWeb = { + webSockets: [], + next: 1, + GetWebSocket: function (index) { + return SimpleWeb.webSockets[index] + }, + AddNextSocket: function (webSocket) { + var index = SimpleWeb.next; + SimpleWeb.next++; + SimpleWeb.webSockets[index] = webSocket; + return index; + }, + RemoveSocket: function (index) { + SimpleWeb.webSockets[index] = undefined; + }, +}; + +function IsConnected(index) { + var webSocket = SimpleWeb.GetWebSocket(index); + if (webSocket) { + return webSocket.readyState === webSocket.OPEN; + } + else { + return false; + } +} + +function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) { + // fix for unity 2021 because unity bug in .jslib + if (typeof Runtime === "undefined") { + // if unity doesn't create Runtime, then make it here + // dont ask why this works, just be happy that it does + Runtime = { + dynCall: dynCall + } + } + + const address = UTF8ToString(addressPtr); + console.log("Connecting to " + address); + // Create webSocket connection. + webSocket = new WebSocket(address); + webSocket.binaryType = 'arraybuffer'; + const index = SimpleWeb.AddNextSocket(webSocket); + + // Connection opened + webSocket.addEventListener('open', function (event) { + console.log("Connected to " + address); + Runtime.dynCall('vi', openCallbackPtr, [index]); + }); + webSocket.addEventListener('close', function (event) { + console.log("Disconnected from " + address); + Runtime.dynCall('vi', closeCallBackPtr, [index]); + }); + + // Listen for messages + webSocket.addEventListener('message', function (event) { + if (event.data instanceof ArrayBuffer) { + // TODO dont alloc each time + var array = new Uint8Array(event.data); + var arrayLength = array.length; + + var bufferPtr = _malloc(arrayLength); + var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength); + dataBuffer.set(array); + + Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]); + _free(bufferPtr); + } + else { + console.error("message type not supported") + } + }); + + webSocket.addEventListener('error', function (event) { + console.error('Socket Error', event); + + Runtime.dynCall('vi', errorCallbackPtr, [index]); + }); + + return index; +} + +function Disconnect(index) { + var webSocket = SimpleWeb.GetWebSocket(index); + if (webSocket) { + webSocket.close(1000, "Disconnect Called by Mirror"); + } + + SimpleWeb.RemoveSocket(index); +} + +function Send(index, arrayPtr, offset, length) { + var webSocket = SimpleWeb.GetWebSocket(index); + if (webSocket) { + const start = arrayPtr + offset; + const end = start + length; + const data = HEAPU8.buffer.slice(start, end); + webSocket.send(data); + return true; + } + return false; +} + + +const SimpleWebLib = { + $SimpleWeb: SimpleWeb, + IsConnected, + Connect, + Disconnect, + Send +}; +autoAddDeps(SimpleWebLib, '$SimpleWeb'); +mergeInto(LibraryManager.library, SimpleWebLib); diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib.meta new file mode 100644 index 0000000..cc1319e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib.meta @@ -0,0 +1,37 @@ +fileFormatVersion: 2 +guid: 54452a8c6d2ca9b49a8c79f81b50305c +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Facebook: WebGL + second: + enabled: 1 + settings: {} + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common.meta new file mode 100644 index 0000000..078faaa --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 564d2cd3eee5b21419553c0528739d1b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs new file mode 100644 index 0000000..4262feb --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Mirror.SimpleWeb +{ + public interface IBufferOwner + { + void Return(ArrayBuffer buffer); + } + + public sealed class ArrayBuffer : IDisposable + { + readonly IBufferOwner owner; + + public readonly byte[] array; + + /// + /// number of bytes written to buffer + /// + public int count { get; internal set; } + + /// + /// How many times release needs to be called before buffer is returned to pool + /// This allows the buffer to be used in multiple places at the same time + /// + public void SetReleasesRequired(int required) + { + releasesRequired = required; + } + + /// + /// How many times release needs to be called before buffer is returned to pool + /// This allows the buffer to be used in multiple places at the same time + /// + /// + /// This value is normally 0, but can be changed to require release to be called multiple times + /// + int releasesRequired; + + public ArrayBuffer(IBufferOwner owner, int size) + { + this.owner = owner; + array = new byte[size]; + } + + public void Release() + { + int newValue = Interlocked.Decrement(ref releasesRequired); + if (newValue <= 0) + { + count = 0; + owner?.Return(this); + } + } + public void Dispose() + { + Release(); + } + + + public void CopyTo(byte[] target, int offset) + { + if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target)); + + Buffer.BlockCopy(array, 0, target, offset, count); + } + + public void CopyFrom(ArraySegment segment) + { + CopyFrom(segment.Array, segment.Offset, segment.Count); + } + + public void CopyFrom(byte[] source, int offset, int length) + { + if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length)); + + count = length; + Buffer.BlockCopy(source, offset, array, 0, length); + } + + public void CopyFrom(IntPtr bufferPtr, int length) + { + if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length)); + + count = length; + Marshal.Copy(bufferPtr, array, 0, length); + } + + public ArraySegment ToSegment() + { + return new ArraySegment(array, 0, count); + } + + [Conditional("UNITY_ASSERTIONS")] + internal void Validate(int arraySize) + { + if (array.Length != arraySize) + { + Log.Error("Buffer that was returned had an array of the wrong size"); + } + } + } + + internal class BufferBucket : IBufferOwner + { + public readonly int arraySize; + readonly ConcurrentQueue buffers; + + /// + /// keeps track of how many arrays are taken vs returned + /// + internal int _current = 0; + + public BufferBucket(int arraySize) + { + this.arraySize = arraySize; + buffers = new ConcurrentQueue(); + } + + public ArrayBuffer Take() + { + IncrementCreated(); + if (buffers.TryDequeue(out ArrayBuffer buffer)) + { + return buffer; + } + else + { + Log.Verbose($"BufferBucket({arraySize}) create new"); + return new ArrayBuffer(this, arraySize); + } + } + + public void Return(ArrayBuffer buffer) + { + DecrementCreated(); + buffer.Validate(arraySize); + buffers.Enqueue(buffer); + } + + [Conditional("DEBUG")] + void IncrementCreated() + { + int next = Interlocked.Increment(ref _current); + Log.Verbose($"BufferBucket({arraySize}) count:{next}"); + } + [Conditional("DEBUG")] + void DecrementCreated() + { + int next = Interlocked.Decrement(ref _current); + Log.Verbose($"BufferBucket({arraySize}) count:{next}"); + } + } + + /// + /// Collection of different sized buffers + /// + /// + /// + /// Problem:
+ /// * Need to cached byte[] so that new ones aren't created each time
+ /// * Arrays sent are multiple different sizes
+ /// * Some message might be big so need buffers to cover that size
+ /// * Most messages will be small compared to max message size
+ ///
+ ///
+ /// + /// Solution:
+ /// * Create multiple groups of buffers covering the range of allowed sizes
+ /// * Split range exponentially (using math.log) so that there are more groups for small buffers
+ ///
+ ///
+ public class BufferPool + { + internal readonly BufferBucket[] buckets; + readonly int bucketCount; + readonly int smallest; + readonly int largest; + + public BufferPool(int bucketCount, int smallest, int largest) + { + if (bucketCount < 2) throw new ArgumentException("Count must be at least 2"); + if (smallest < 1) throw new ArgumentException("Smallest must be at least 1"); + if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest"); + + + this.bucketCount = bucketCount; + this.smallest = smallest; + this.largest = largest; + + + // split range over log scale (more buckets for smaller sizes) + + double minLog = Math.Log(this.smallest); + double maxLog = Math.Log(this.largest); + + double range = maxLog - minLog; + double each = range / (bucketCount - 1); + + buckets = new BufferBucket[bucketCount]; + + for (int i = 0; i < bucketCount; i++) + { + double size = smallest * Math.Pow(Math.E, each * i); + buckets[i] = new BufferBucket((int)Math.Ceiling(size)); + } + + + Validate(); + + // Example + // 5 count + // 20 smallest + // 16400 largest + + // 3.0 log 20 + // 9.7 log 16400 + + // 6.7 range 9.7 - 3 + // 1.675 each 6.7 / (5-1) + + // 20 e^ (3 + 1.675 * 0) + // 107 e^ (3 + 1.675 * 1) + // 572 e^ (3 + 1.675 * 2) + // 3056 e^ (3 + 1.675 * 3) + // 16,317 e^ (3 + 1.675 * 4) + + // precision wont be lose when using doubles + } + + [Conditional("UNITY_ASSERTIONS")] + void Validate() + { + if (buckets[0].arraySize != smallest) + { + Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}"); + } + + int largestBucket = buckets[bucketCount - 1].arraySize; + // rounded using Ceiling, so allowed to be 1 more that largest + if (largestBucket != largest && largestBucket != largest + 1) + { + Log.Error($"BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}"); + } + } + + public ArrayBuffer Take(int size) + { + if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); } + + for (int i = 0; i < bucketCount; i++) + { + if (size <= buckets[i].arraySize) + { + return buckets[i].Take(); + } + } + + throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs.meta new file mode 100644 index 0000000..0b1070f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94ae50f3ec35667469b861b12cd72f92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs new file mode 100644 index 0000000..adc52db --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net.Sockets; +using System.Threading; + +namespace Mirror.SimpleWeb +{ + internal sealed class Connection : IDisposable + { + public const int IdNotSet = -1; + + readonly object disposedLock = new object(); + + public TcpClient client; + + public int connId = IdNotSet; + public Stream stream; + public Thread receiveThread; + public Thread sendThread; + + public ManualResetEventSlim sendPending = new ManualResetEventSlim(false); + public ConcurrentQueue sendQueue = new ConcurrentQueue(); + + public Action onDispose; + + volatile bool hasDisposed; + + public Connection(TcpClient client, Action onDispose) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.onDispose = onDispose; + } + + /// + /// disposes client and stops threads + /// + public void Dispose() + { + Log.Verbose($"Dispose {ToString()}"); + + // check hasDisposed first to stop ThreadInterruptedException on lock + if (hasDisposed) { return; } + + Log.Info($"Connection Close: {ToString()}"); + + + lock (disposedLock) + { + // check hasDisposed again inside lock to make sure no other object has called this + if (hasDisposed) { return; } + hasDisposed = true; + + // stop threads first so they don't try to use disposed objects + receiveThread.Interrupt(); + sendThread?.Interrupt(); + + try + { + // stream + stream?.Dispose(); + stream = null; + client.Dispose(); + client = null; + } + catch (Exception e) + { + Log.Exception(e); + } + + sendPending.Dispose(); + + // release all buffers in send queue + while (sendQueue.TryDequeue(out ArrayBuffer buffer)) + { + buffer.Release(); + } + + onDispose.Invoke(this); + } + } + + public override string ToString() + { + if (hasDisposed) + { + return $"[Conn:{connId}, Disposed]"; + } + else + { + System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint; + return $"[Conn:{connId}, endPoint:{endpoint}]"; + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs.meta new file mode 100644 index 0000000..d48a835 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a13073c2b49d39943888df45174851bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs new file mode 100644 index 0000000..3aa16c3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs @@ -0,0 +1,77 @@ +using System.Text; + +namespace Mirror.SimpleWeb +{ + /// + /// Constant values that should never change + /// + /// Some values are from https://tools.ietf.org/html/rfc6455 + /// + /// + internal static class Constants + { + /// + /// Header is at most 4 bytes + /// + /// If message is less than 125 then header is 2 bytes, else header is 4 bytes + /// + /// + public const int HeaderSize = 4; + + /// + /// Smallest size of header + /// + /// If message is less than 125 then header is 2 bytes, else header is 4 bytes + /// + /// + public const int HeaderMinSize = 2; + + /// + /// bytes for short length + /// + public const int ShortLength = 2; + + /// + /// bytes for long length + /// + public const int LongLength = 8; + + /// + /// Message mask is always 4 bytes + /// + public const int MaskSize = 4; + + /// + /// Max size of a message for length to be 1 byte long + /// + /// payload length between 0-125 + /// + /// + public const int BytePayloadLength = 125; + + /// + /// if payload length is 126 when next 2 bytes will be the length + /// + public const int UshortPayloadLength = 126; + + /// + /// if payload length is 127 when next 8 bytes will be the length + /// + public const int UlongPayloadLength = 127; + + + /// + /// Guid used for WebSocket Protocol + /// + public const string HandshakeGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + public static readonly int HandshakeGUIDLength = HandshakeGUID.Length; + + public static readonly byte[] HandshakeGUIDBytes = Encoding.ASCII.GetBytes(HandshakeGUID); + + /// + /// Handshake messages will end with \r\n\r\n + /// + public static readonly byte[] endOfHandshake = new byte[4] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }; + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs.meta new file mode 100644 index 0000000..ece602e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85d110a089d6ad348abf2d073ebce7cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs new file mode 100644 index 0000000..3a9d185 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs @@ -0,0 +1,10 @@ +namespace Mirror.SimpleWeb +{ + public enum EventType + { + Connected, + Data, + Disconnected, + Error + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs.meta new file mode 100644 index 0000000..a91403a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d9cd7d2b5229ab42a12e82ae17d0347 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs new file mode 100644 index 0000000..4b7bce5 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs @@ -0,0 +1,116 @@ +using System; +using UnityEngine; +using Conditional = System.Diagnostics.ConditionalAttribute; + +namespace Mirror.SimpleWeb +{ + public static class Log + { + // used for Conditional + const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED); + const string DEBUG = nameof(DEBUG); + + public enum Levels + { + none = 0, + error = 1, + warn = 2, + info = 3, + verbose = 4, + } + + public static ILogger logger = Debug.unityLogger; + public static Levels level = Levels.none; + + public static string BufferToString(byte[] buffer, int offset = 0, int? length = null) + { + return BitConverter.ToString(buffer, offset, length ?? buffer.Length); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED)] + public static void DumpBuffer(string label, byte[] buffer, int offset, int length) + { + if (level < Levels.verbose) + return; + + logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(buffer, offset, length)}
"); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED)] + public static void DumpBuffer(string label, ArrayBuffer arrayBuffer) + { + if (level < Levels.verbose) + return; + + logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}
"); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED)] + public static void Verbose(string msg, bool showColor = true) + { + if (level < Levels.verbose) + return; + + if (showColor) + logger.Log(LogType.Log, $"VERBOSE: {msg}"); + else + logger.Log(LogType.Log, $"VERBOSE: {msg}"); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED)] + public static void Info(string msg, bool showColor = true) + { + if (level < Levels.info) + return; + + if (showColor) + logger.Log(LogType.Log, $"INFO: {msg}"); + else + logger.Log(LogType.Log, $"INFO: {msg}"); + } + + /// + /// An expected Exception was caught, useful for debugging but not important + /// + /// + /// + [Conditional(SIMPLEWEB_LOG_ENABLED)] + public static void InfoException(Exception e) + { + if (level < Levels.info) + return; + + logger.Log(LogType.Log, $"INFO_EXCEPTION: {e.GetType().Name} Message: {e.Message}\n{e.StackTrace}\n\n"); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] + public static void Warn(string msg, bool showColor = true) + { + if (level < Levels.warn) + return; + + if (showColor) + logger.Log(LogType.Warning, $"WARN: {msg}"); + else + logger.Log(LogType.Warning, $"WARN: {msg}"); + } + + [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] + public static void Error(string msg, bool showColor = true) + { + if (level < Levels.error) + return; + + if (showColor) + logger.Log(LogType.Error, $"ERROR: {msg}"); + else + logger.Log(LogType.Error, $"ERROR: {msg}"); + } + + public static void Exception(Exception e) + { + // always log Exceptions + logger.Log(LogType.Error, $"EXCEPTION: {e.GetType().Name} Message: {e.Message}\n{e.StackTrace}\n\n"); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs.meta new file mode 100644 index 0000000..beb2883 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cf1521098e04f74fbea0fe2aa0439f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs new file mode 100644 index 0000000..29b4849 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs @@ -0,0 +1,49 @@ +using System; + +namespace Mirror.SimpleWeb +{ + public struct Message + { + public readonly int connId; + public readonly EventType type; + public readonly ArrayBuffer data; + public readonly Exception exception; + + public Message(EventType type) : this() + { + this.type = type; + } + + public Message(ArrayBuffer data) : this() + { + type = EventType.Data; + this.data = data; + } + + public Message(Exception exception) : this() + { + type = EventType.Error; + this.exception = exception; + } + + public Message(int connId, EventType type) : this() + { + this.connId = connId; + this.type = type; + } + + public Message(int connId, ArrayBuffer data) : this() + { + this.connId = connId; + type = EventType.Data; + this.data = data; + } + + public Message(int connId, Exception exception) : this() + { + this.connId = connId; + type = EventType.Error; + this.exception = exception; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs.meta new file mode 100644 index 0000000..3286a2c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5d05d71b09d2714b96ffe80bc3d2a77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs new file mode 100644 index 0000000..59c9326 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs @@ -0,0 +1,193 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Mirror.SimpleWeb +{ + public static class MessageProcessor + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static byte FirstLengthByte(byte[] buffer) => (byte)(buffer[1] & 0b0111_1111); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NeedToReadShortLength(byte[] buffer) + { + byte lenByte = FirstLengthByte(buffer); + + return lenByte == Constants.UshortPayloadLength; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool NeedToReadLongLength(byte[] buffer) + { + byte lenByte = FirstLengthByte(buffer); + + return lenByte == Constants.UlongPayloadLength; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetOpcode(byte[] buffer) + { + return buffer[0] & 0b0000_1111; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetPayloadLength(byte[] buffer) + { + byte lenByte = FirstLengthByte(buffer); + return GetMessageLength(buffer, 0, lenByte); + } + + /// + /// Has full message been sent + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Finished(byte[] buffer) + { + return (buffer[0] & 0b1000_0000) != 0; + } + + public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask, bool opCodeContinuation = false) + { + bool finished = Finished(buffer); + bool hasMask = (buffer[1] & 0b1000_0000) != 0; // true from clients, false from server, "All messages from the client to the server have this bit set" + + int opcode = buffer[0] & 0b0000_1111; // expecting 1 - text message + byte lenByte = FirstLengthByte(buffer); + + ThrowIfMaskNotExpected(hasMask, expectMask); + ThrowIfBadOpCode(opcode, finished, opCodeContinuation); + + int msglen = GetMessageLength(buffer, 0, lenByte); + + ThrowIfLengthZero(msglen); + ThrowIfMsgLengthTooLong(msglen, maxLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToggleMask(byte[] src, int sourceOffset, int messageLength, byte[] maskBuffer, int maskOffset) + { + ToggleMask(src, sourceOffset, src, sourceOffset, messageLength, maskBuffer, maskOffset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToggleMask(byte[] src, int sourceOffset, ArrayBuffer dst, int messageLength, byte[] maskBuffer, int maskOffset) + { + ToggleMask(src, sourceOffset, dst.array, 0, messageLength, maskBuffer, maskOffset); + dst.count = messageLength; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToggleMask(byte[] src, int srcOffset, byte[] dst, int dstOffset, int messageLength, byte[] maskBuffer, int maskOffset) + { + for (int i = 0; i < messageLength; i++) + { + byte maskByte = maskBuffer[maskOffset + i % Constants.MaskSize]; + dst[dstOffset + i] = (byte)(src[srcOffset + i] ^ maskByte); + } + } + + /// + static int GetMessageLength(byte[] buffer, int offset, byte lenByte) + { + if (lenByte == Constants.UshortPayloadLength) + { + // header is 2 bytes + ushort value = 0; + value |= (ushort)(buffer[offset + 2] << 8); + value |= buffer[offset + 3]; + + return value; + } + else if (lenByte == Constants.UlongPayloadLength) + { + // header is 8 bytes + ulong value = 0; + value |= ((ulong)buffer[offset + 2] << 56); + value |= ((ulong)buffer[offset + 3] << 48); + value |= ((ulong)buffer[offset + 4] << 40); + value |= ((ulong)buffer[offset + 5] << 32); + value |= ((ulong)buffer[offset + 6] << 24); + value |= ((ulong)buffer[offset + 7] << 16); + value |= ((ulong)buffer[offset + 8] << 8); + value |= ((ulong)buffer[offset + 9] << 0); + + if (value > int.MaxValue) + { + throw new NotSupportedException($"Can't receive payloads larger that int.max: {int.MaxValue}"); + } + return (int)value; + } + else // is less than 126 + { + // header is 2 bytes long + return lenByte; + } + } + + /// + static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask) + { + if (hasMask != expectMask) + { + throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}"); + } + } + + /// + static void ThrowIfBadOpCode(int opcode, bool finished, bool opCodeContinuation) + { + // 0 = continuation + // 2 = binary + // 8 = close + + // do we expect Continuation? + if (opCodeContinuation) + { + // good it was Continuation + if (opcode == 0) + return; + + // bad, wasn't Continuation + throw new InvalidDataException("Expected opcode to be Continuation"); + } + else if (!finished) + { + // fragmented message, should be binary + if (opcode == 2) + return; + + throw new InvalidDataException("Expected opcode to be binary"); + } + else + { + // normal message, should be binary or close + if (opcode == 2 || opcode == 8) + return; + + throw new InvalidDataException("Expected opcode to be binary or close"); + } + } + + /// + static void ThrowIfLengthZero(int msglen) + { + if (msglen == 0) + { + throw new InvalidDataException("Message length was zero"); + } + } + + /// + /// need to check this so that data from previous buffer isn't used + /// + public static void ThrowIfMsgLengthTooLong(int msglen, int maxLength) + { + if (msglen > maxLength) + { + throw new InvalidDataException("Message length is greater than max length"); + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs.meta new file mode 100644 index 0000000..7e3a7c4 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c1f218a2b16ca846aaf23260078e549 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs new file mode 100644 index 0000000..74cbf2d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace Mirror.SimpleWeb +{ + public static class ReadHelper + { + /// + /// Reads exactly length from stream + /// + /// outOffset + length + /// + public static int Read(Stream stream, byte[] outBuffer, int outOffset, int length) + { + int received = 0; + try + { + while (received < length) + { + int read = stream.Read(outBuffer, outOffset + received, length - received); + if (read == 0) + { + throw new ReadHelperException("returned 0"); + } + received += read; + } + } + catch (AggregateException ae) + { + // if interrupt is called we don't care about Exceptions + Utils.CheckForInterupt(); + + // rethrow + ae.Handle(e => false); + } + + if (received != length) + { + throw new ReadHelperException("returned not equal to length"); + } + + return outOffset + received; + } + + /// + /// Reads and returns results. This should never throw an exception + /// + public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int length) + { + try + { + Read(stream, outBuffer, outOffset, length); + return true; + } + catch (ReadHelperException) + { + return false; + } + catch (IOException) + { + return false; + } + catch (Exception e) + { + Log.Exception(e); + return false; + } + } + + public static int? SafeReadTillMatch(Stream stream, byte[] outBuffer, int outOffset, int maxLength, byte[] endOfHeader) + { + try + { + int read = 0; + int endIndex = 0; + int endLength = endOfHeader.Length; + while (true) + { + int next = stream.ReadByte(); + if (next == -1) // closed + return null; + + if (read >= maxLength) + { + Log.Error("SafeReadTillMatch exceeded maxLength"); + return null; + } + + outBuffer[outOffset + read] = (byte)next; + read++; + + // if n is match, check n+1 next + if (endOfHeader[endIndex] == next) + { + endIndex++; + // when all is match return with read length + if (endIndex >= endLength) + { + return read; + } + } + // if n not match reset to 0 + else + { + endIndex = 0; + } + } + } + catch (IOException e) + { + Log.InfoException(e); + return null; + } + catch (Exception e) + { + Log.Exception(e); + return null; + } + } + } + + [Serializable] + public class ReadHelperException : Exception + { + public ReadHelperException(string message) : base(message) { } + + protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs.meta new file mode 100644 index 0000000..77d09c1 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f4fa5d324e708c46a55810a97de75bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs new file mode 100644 index 0000000..952592c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using UnityEngine.Profiling; + +namespace Mirror.SimpleWeb +{ + internal static class ReceiveLoop + { + public struct Config + { + public readonly Connection conn; + public readonly int maxMessageSize; + public readonly bool expectMask; + public readonly ConcurrentQueue queue; + public readonly BufferPool bufferPool; + + public Config(Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue queue, BufferPool bufferPool) + { + this.conn = conn ?? throw new ArgumentNullException(nameof(conn)); + this.maxMessageSize = maxMessageSize; + this.expectMask = expectMask; + this.queue = queue ?? throw new ArgumentNullException(nameof(queue)); + this.bufferPool = bufferPool ?? throw new ArgumentNullException(nameof(bufferPool)); + } + + public void Deconstruct(out Connection conn, out int maxMessageSize, out bool expectMask, out ConcurrentQueue queue, out BufferPool bufferPool) + { + conn = this.conn; + maxMessageSize = this.maxMessageSize; + expectMask = this.expectMask; + queue = this.queue; + bufferPool = this.bufferPool; + } + } + + struct Header + { + public int payloadLength; + public int offset; + public int opcode; + public bool finished; + } + + public static void Loop(Config config) + { + (Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue queue, BufferPool _) = config; + + Profiler.BeginThreadProfiling("SimpleWeb", $"ReceiveLoop {conn.connId}"); + + byte[] readBuffer = new byte[Constants.HeaderSize + (expectMask ? Constants.MaskSize : 0) + maxMessageSize]; + try + { + try + { + TcpClient client = conn.client; + + while (client.Connected) + { + ReadOneMessage(config, readBuffer); + } + + Log.Info($"{conn} Not Connected"); + } + catch (Exception) + { + // if interrupted we don't care about other exceptions + Utils.CheckForInterupt(); + throw; + } + } + catch (ThreadInterruptedException e) { Log.InfoException(e); } + catch (ThreadAbortException e) { Log.InfoException(e); } + catch (ObjectDisposedException e) { Log.InfoException(e); } + catch (ReadHelperException e) + { + Log.InfoException(e); + } + catch (SocketException e) + { + // this could happen if wss client closes stream + Log.Warn($"ReceiveLoop SocketException\n{e.Message}", false); + queue.Enqueue(new Message(conn.connId, e)); + } + catch (IOException e) + { + // this could happen if client disconnects + Log.Warn($"ReceiveLoop IOException\n{e.Message}", false); + queue.Enqueue(new Message(conn.connId, e)); + } + catch (InvalidDataException e) + { + Log.Error($"Invalid data from {conn}: {e.Message}"); + queue.Enqueue(new Message(conn.connId, e)); + } + catch (Exception e) + { + Log.Exception(e); + queue.Enqueue(new Message(conn.connId, e)); + } + finally + { + Profiler.EndThreadProfiling(); + + conn.Dispose(); + } + } + + static void ReadOneMessage(Config config, byte[] buffer) + { + (Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue queue, BufferPool bufferPool) = config; + Stream stream = conn.stream; + + Header header = ReadHeader(config, buffer); + + int msgOffset = header.offset; + header.offset = ReadHelper.Read(stream, buffer, header.offset, header.payloadLength); + + if (header.finished) + { + switch (header.opcode) + { + case 2: + HandleArrayMessage(config, buffer, msgOffset, header.payloadLength); + break; + case 8: + HandleCloseMessage(config, buffer, msgOffset, header.payloadLength); + break; + } + } + else + { + // todo cache this to avoid allocations + Queue fragments = new Queue(); + fragments.Enqueue(CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, header.payloadLength)); + int totalSize = header.payloadLength; + + while (!header.finished) + { + header = ReadHeader(config, buffer, opCodeContinuation: true); + + msgOffset = header.offset; + header.offset = ReadHelper.Read(stream, buffer, header.offset, header.payloadLength); + fragments.Enqueue(CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, header.payloadLength)); + + totalSize += header.payloadLength; + MessageProcessor.ThrowIfMsgLengthTooLong(totalSize, maxMessageSize); + } + + + ArrayBuffer msg = bufferPool.Take(totalSize); + msg.count = 0; + while (fragments.Count > 0) + { + ArrayBuffer part = fragments.Dequeue(); + + part.CopyTo(msg.array, msg.count); + msg.count += part.count; + + part.Release(); + } + + // dump after mask off + Log.DumpBuffer($"Message", msg); + + queue.Enqueue(new Message(conn.connId, msg)); + } + } + + static Header ReadHeader(Config config, byte[] buffer, bool opCodeContinuation = false) + { + (Connection conn, int maxMessageSize, bool expectMask, ConcurrentQueue queue, BufferPool bufferPool) = config; + Stream stream = conn.stream; + Header header = new Header(); + + // read 2 + header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.HeaderMinSize); + // log after first blocking call + Log.Verbose($"Message From {conn}"); + + if (MessageProcessor.NeedToReadShortLength(buffer)) + { + header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.ShortLength); + } + if (MessageProcessor.NeedToReadLongLength(buffer)) + { + header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.LongLength); + } + + Log.DumpBuffer($"Raw Header", buffer, 0, header.offset); + + MessageProcessor.ValidateHeader(buffer, maxMessageSize, expectMask, opCodeContinuation); + + if (expectMask) + { + header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.MaskSize); + } + + header.opcode = MessageProcessor.GetOpcode(buffer); + header.payloadLength = MessageProcessor.GetPayloadLength(buffer); + header.finished = MessageProcessor.Finished(buffer); + + Log.Verbose($"Header ln:{header.payloadLength} op:{header.opcode} mask:{expectMask}"); + + return header; + } + + static void HandleArrayMessage(Config config, byte[] buffer, int msgOffset, int payloadLength) + { + (Connection conn, int _, bool expectMask, ConcurrentQueue queue, BufferPool bufferPool) = config; + + ArrayBuffer arrayBuffer = CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, payloadLength); + + // dump after mask off + Log.DumpBuffer($"Message", arrayBuffer); + + queue.Enqueue(new Message(conn.connId, arrayBuffer)); + } + + static ArrayBuffer CopyMessageToBuffer(BufferPool bufferPool, bool expectMask, byte[] buffer, int msgOffset, int payloadLength) + { + ArrayBuffer arrayBuffer = bufferPool.Take(payloadLength); + + if (expectMask) + { + int maskOffset = msgOffset - Constants.MaskSize; + // write the result of toggle directly into arrayBuffer to avoid 2nd copy call + MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset); + } + else + { + arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength); + } + + return arrayBuffer; + } + + static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int payloadLength) + { + (Connection conn, int _, bool expectMask, ConcurrentQueue _, BufferPool _) = config; + + if (expectMask) + { + int maskOffset = msgOffset - Constants.MaskSize; + MessageProcessor.ToggleMask(buffer, msgOffset, payloadLength, buffer, maskOffset); + } + + // dump after mask off + Log.DumpBuffer($"Message", buffer, msgOffset, payloadLength); + + Log.Info($"Close: {GetCloseCode(buffer, msgOffset)} message:{GetCloseMessage(buffer, msgOffset, payloadLength)}"); + + conn.Dispose(); + } + + static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength) + { + return Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2); + } + + static int GetCloseCode(byte[] buffer, int msgOffset) + { + return buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1]; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs.meta new file mode 100644 index 0000000..47c6ff5 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a26c2815f58431c4a98c158c8b655ffd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs new file mode 100644 index 0000000..6dc1b1b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs @@ -0,0 +1,228 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Threading; +using UnityEngine.Profiling; + +namespace Mirror.SimpleWeb +{ + public static class SendLoopConfig + { + public static volatile bool batchSend = false; + public static volatile bool sleepBeforeSend = false; + } + internal static class SendLoop + { + public struct Config + { + public readonly Connection conn; + public readonly int bufferSize; + public readonly bool setMask; + + public Config(Connection conn, int bufferSize, bool setMask) + { + this.conn = conn ?? throw new ArgumentNullException(nameof(conn)); + this.bufferSize = bufferSize; + this.setMask = setMask; + } + + public void Deconstruct(out Connection conn, out int bufferSize, out bool setMask) + { + conn = this.conn; + bufferSize = this.bufferSize; + setMask = this.setMask; + } + } + + public static void Loop(Config config) + { + (Connection conn, int bufferSize, bool setMask) = config; + + Profiler.BeginThreadProfiling("SimpleWeb", $"SendLoop {conn.connId}"); + + // create write buffer for this thread + byte[] writeBuffer = new byte[bufferSize]; + MaskHelper maskHelper = setMask ? new MaskHelper() : null; + try + { + TcpClient client = conn.client; + Stream stream = conn.stream; + + // null check in case disconnect while send thread is starting + if (client == null) + return; + + while (client.Connected) + { + // wait for message + conn.sendPending.Wait(); + // wait for 1ms for mirror to send other messages + if (SendLoopConfig.sleepBeforeSend) + { + Thread.Sleep(1); + } + conn.sendPending.Reset(); + + if (SendLoopConfig.batchSend) + { + int offset = 0; + while (conn.sendQueue.TryDequeue(out ArrayBuffer msg)) + { + // check if connected before sending message + if (!client.Connected) + { + Log.Info($"SendLoop {conn} not connected"); + msg.Release(); + return; + } + + int maxLength = msg.count + Constants.HeaderSize + Constants.MaskSize; + + // if next writer could overflow, write to stream and clear buffer + if (offset + maxLength > bufferSize) + { + stream.Write(writeBuffer, 0, offset); + offset = 0; + } + + offset = SendMessage(writeBuffer, offset, msg, setMask, maskHelper); + msg.Release(); + } + + // after no message in queue, send remaining messages + // don't need to check offset > 0 because last message in queue will always be sent here + + stream.Write(writeBuffer, 0, offset); + } + else + { + while (conn.sendQueue.TryDequeue(out ArrayBuffer msg)) + { + // check if connected before sending message + if (!client.Connected) + { + Log.Info($"SendLoop {conn} not connected"); + msg.Release(); + return; + } + + int length = SendMessage(writeBuffer, 0, msg, setMask, maskHelper); + stream.Write(writeBuffer, 0, length); + msg.Release(); + } + } + } + + Log.Info($"{conn} Not Connected"); + } + catch (ThreadInterruptedException e) { Log.InfoException(e); } + catch (ThreadAbortException e) { Log.InfoException(e); } + catch (Exception e) + { + Log.Exception(e); + } + finally + { + Profiler.EndThreadProfiling(); + conn.Dispose(); + maskHelper?.Dispose(); + } + } + + /// new offset in buffer + static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool setMask, MaskHelper maskHelper) + { + int msgLength = msg.count; + int offset = WriteHeader(buffer, startOffset, msgLength, setMask); + + if (setMask) + { + offset = maskHelper.WriteMask(buffer, offset); + } + + msg.CopyTo(buffer, offset); + offset += msgLength; + + // dump before mask on + Log.DumpBuffer("Send", buffer, startOffset, offset); + + if (setMask) + { + int messageOffset = offset - msgLength; + MessageProcessor.ToggleMask(buffer, messageOffset, msgLength, buffer, messageOffset - Constants.MaskSize); + } + + return offset; + } + + public static int WriteHeader(byte[] buffer, int startOffset, int msgLength, bool setMask) + { + int sendLength = 0; + const byte finished = 128; + const byte byteOpCode = 2; + + buffer[startOffset + 0] = finished | byteOpCode; + sendLength++; + + if (msgLength <= Constants.BytePayloadLength) + { + buffer[startOffset + 1] = (byte)msgLength; + sendLength++; + } + else if (msgLength <= ushort.MaxValue) + { + buffer[startOffset + 1] = 126; + buffer[startOffset + 2] = (byte)(msgLength >> 8); + buffer[startOffset + 3] = (byte)msgLength; + sendLength += 3; + } + else + { + buffer[startOffset + 1] = 127; + // must be 64 bytes, but we only have 32 bit length, so first 4 bits are 0 + buffer[startOffset + 2] = 0; + buffer[startOffset + 3] = 0; + buffer[startOffset + 4] = 0; + buffer[startOffset + 5] = 0; + buffer[startOffset + 6] = (byte)(msgLength >> 24); + buffer[startOffset + 7] = (byte)(msgLength >> 16); + buffer[startOffset + 8] = (byte)(msgLength >> 8); + buffer[startOffset + 9] = (byte)msgLength; + + sendLength += 9; + } + + if (setMask) + { + buffer[startOffset + 1] |= 0b1000_0000; + } + + return sendLength + startOffset; + } + + } + sealed class MaskHelper : IDisposable + { + readonly byte[] maskBuffer; + readonly RNGCryptoServiceProvider random; + + public MaskHelper() + { + maskBuffer = new byte[4]; + random = new RNGCryptoServiceProvider(); + } + public void Dispose() + { + random.Dispose(); + } + + public int WriteMask(byte[] buffer, int offset) + { + random.GetBytes(maskBuffer); + Buffer.BlockCopy(maskBuffer, 0, buffer, offset, 4); + + return offset + 4; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs.meta new file mode 100644 index 0000000..09dfd1e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f87dd81736d9c824db67f808ac71841d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs new file mode 100644 index 0000000..230cd7a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs @@ -0,0 +1,26 @@ +using System.Net.Sockets; + +namespace Mirror.SimpleWeb +{ + [System.Serializable] + public struct TcpConfig + { + public readonly bool noDelay; + public readonly int sendTimeout; + public readonly int receiveTimeout; + + public TcpConfig(bool noDelay, int sendTimeout, int receiveTimeout) + { + this.noDelay = noDelay; + this.sendTimeout = sendTimeout; + this.receiveTimeout = receiveTimeout; + } + + public void ApplyTo(TcpClient client) + { + client.SendTimeout = sendTimeout; + client.ReceiveTimeout = receiveTimeout; + client.NoDelay = noDelay; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs.meta new file mode 100644 index 0000000..62ba232 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81ac8d35f28fab14b9edda5cd9d4fc86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs new file mode 100644 index 0000000..b8a860c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs @@ -0,0 +1,13 @@ +using System.Threading; + +namespace Mirror.SimpleWeb +{ + internal static class Utils + { + public static void CheckForInterupt() + { + // sleep in order to check for ThreadInterruptedException + Thread.Sleep(1); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs.meta new file mode 100644 index 0000000..79a1583 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4643ffb4cb0562847b1ae925d07e15b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE new file mode 100644 index 0000000..d2b4728 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 James Frowen + +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. diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta new file mode 100644 index 0000000..8ece59e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0a0cf751b4a201242ac60b4adbc54657 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt new file mode 100644 index 0000000..fd59b3d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt @@ -0,0 +1,19 @@ +SimpleWebTransport is a Transport that implements websocket for Webgl +Can be used in High level networking solution like Mirror or Mirage +This transport can also work on standalone builds and has support for +encryption with websocket secure. + +Requirements: + Unity 2019.4 LTS + +Documentation: + https://mirror-networking.gitbook.io/docs/ + https://github.com/James-Frowen/SimpleWebTransport/blob/master/README.md + +Support: + Discord: https://discord.gg/BZTQcftBkE + Bug Reports: https://github.com/James-Frowen/SimpleWebTransport/issues + + +**To get most recent updates and fixes download from github** +https://github.com/James-Frowen/SimpleWebTransport/releases diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta new file mode 100644 index 0000000..b63fe39 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0e3971d5783109f4d9ce93c7a689d701 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server.meta new file mode 100644 index 0000000..31f317f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0e599e92544d43344a9a9060052add28 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs new file mode 100644 index 0000000..b138201 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Mirror.SimpleWeb +{ + /// + /// Handles Handshakes from new clients on the server + /// The server handshake has buffers to reduce allocations when clients connect + /// + internal class ServerHandshake + { + const int GetSize = 3; + const int ResponseLength = 129; + const int KeyLength = 24; + const int MergedKeyLength = 60; + const string KeyHeaderString = "Sec-WebSocket-Key: "; + // this isn't an official max, just a reasonable size for a websocket handshake + readonly int maxHttpHeaderSize = 3000; + + readonly SHA1 sha1 = SHA1.Create(); + readonly BufferPool bufferPool; + + public ServerHandshake(BufferPool bufferPool, int handshakeMaxSize) + { + this.bufferPool = bufferPool; + maxHttpHeaderSize = handshakeMaxSize; + } + + ~ServerHandshake() + { + sha1.Dispose(); + } + + public bool TryHandshake(Connection conn) + { + Stream stream = conn.stream; + + using (ArrayBuffer getHeader = bufferPool.Take(GetSize)) + { + if (!ReadHelper.TryRead(stream, getHeader.array, 0, GetSize)) + return false; + getHeader.count = GetSize; + + + if (!IsGet(getHeader.array)) + { + Log.Warn($"First bytes from client was not 'GET' for handshake, instead was {Log.BufferToString(getHeader.array, 0, GetSize)}"); + return false; + } + } + + + string msg = ReadToEndForHandshake(stream); + + if (string.IsNullOrEmpty(msg)) + return false; + + try + { + AcceptHandshake(stream, msg); + return true; + } + catch (ArgumentException e) + { + Log.InfoException(e); + return false; + } + } + + string ReadToEndForHandshake(Stream stream) + { + using (ArrayBuffer readBuffer = bufferPool.Take(maxHttpHeaderSize)) + { + int? readCountOrFail = ReadHelper.SafeReadTillMatch(stream, readBuffer.array, 0, maxHttpHeaderSize, Constants.endOfHandshake); + if (!readCountOrFail.HasValue) + return null; + + int readCount = readCountOrFail.Value; + + string msg = Encoding.ASCII.GetString(readBuffer.array, 0, readCount); + Log.Verbose(msg); + + return msg; + } + } + + static bool IsGet(byte[] getHeader) + { + // just check bytes here instead of using Encoding.ASCII + return getHeader[0] == 71 && // G + getHeader[1] == 69 && // E + getHeader[2] == 84; // T + } + + void AcceptHandshake(Stream stream, string msg) + { + using ( + ArrayBuffer keyBuffer = bufferPool.Take(KeyLength + Constants.HandshakeGUIDLength), + responseBuffer = bufferPool.Take(ResponseLength)) + { + GetKey(msg, keyBuffer.array); + AppendGuid(keyBuffer.array); + byte[] keyHash = CreateHash(keyBuffer.array); + CreateResponse(keyHash, responseBuffer.array); + + stream.Write(responseBuffer.array, 0, ResponseLength); + } + } + + + static void GetKey(string msg, byte[] keyBuffer) + { + int start = msg.IndexOf(KeyHeaderString) + KeyHeaderString.Length; + + Log.Verbose($"Handshake Key: {msg.Substring(start, KeyLength)}"); + Encoding.ASCII.GetBytes(msg, start, KeyLength, keyBuffer, 0); + } + + static void AppendGuid(byte[] keyBuffer) + { + Buffer.BlockCopy(Constants.HandshakeGUIDBytes, 0, keyBuffer, KeyLength, Constants.HandshakeGUIDLength); + } + + byte[] CreateHash(byte[] keyBuffer) + { + Log.Verbose($"Handshake Hashing {Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength)}"); + + return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength); + } + + static void CreateResponse(byte[] keyHash, byte[] responseBuffer) + { + string keyHashString = Convert.ToBase64String(keyHash); + + // compiler should merge these strings into 1 string before format + string message = string.Format( + "HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: {0}\r\n\r\n", + keyHashString); + + Log.Verbose($"Handshake Response length {message.Length}, IsExpected {message.Length == ResponseLength}"); + Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0); + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs.meta new file mode 100644 index 0000000..6fa74da --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6268509ac4fb48141b9944c03295da11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs new file mode 100644 index 0000000..de6c022 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace Mirror.SimpleWeb +{ + public struct SslConfig + { + public readonly bool enabled; + public readonly string certPath; + public readonly string certPassword; + public readonly SslProtocols sslProtocols; + + public SslConfig(bool enabled, string certPath, string certPassword, SslProtocols sslProtocols) + { + this.enabled = enabled; + this.certPath = certPath; + this.certPassword = certPassword; + this.sslProtocols = sslProtocols; + } + } + internal class ServerSslHelper + { + readonly SslConfig config; + readonly X509Certificate2 certificate; + + public ServerSslHelper(SslConfig sslConfig) + { + config = sslConfig; + if (config.enabled) + certificate = new X509Certificate2(config.certPath, config.certPassword); + } + + internal bool TryCreateStream(Connection conn) + { + NetworkStream stream = conn.client.GetStream(); + if (config.enabled) + { + try + { + conn.stream = CreateStream(stream); + return true; + } + catch (Exception e) + { + Log.Error($"Create SSLStream Failed: {e}", false); + return false; + } + } + else + { + conn.stream = stream; + return true; + } + } + + Stream CreateStream(NetworkStream stream) + { + SslStream sslStream = new SslStream(stream, true, acceptClient); + sslStream.AuthenticateAsServer(certificate, false, config.sslProtocols, false); + + return sslStream; + } + + bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // always accept client + return true; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs.meta new file mode 100644 index 0000000..e0d133c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11061fee528ebdd43817a275b1e4a317 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs new file mode 100644 index 0000000..05cf996 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror.SimpleWeb +{ + public class SimpleWebServer + { + readonly int maxMessagesPerTick; + + readonly WebSocketServer server; + readonly BufferPool bufferPool; + + public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig) + { + this.maxMessagesPerTick = maxMessagesPerTick; + // use max because bufferpool is used for both messages and handshake + int max = Math.Max(maxMessageSize, handshakeMaxSize); + bufferPool = new BufferPool(5, 20, max); + + server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool); + } + + public bool Active { get; private set; } + + public event Action onConnect; + public event Action onDisconnect; + public event Action> onData; + public event Action onError; + + public void Start(ushort port) + { + server.Listen(port); + Active = true; + } + + public void Stop() + { + server.Stop(); + Active = false; + } + + public void SendAll(List connectionIds, ArraySegment source) + { + ArrayBuffer buffer = bufferPool.Take(source.Count); + buffer.CopyFrom(source); + buffer.SetReleasesRequired(connectionIds.Count); + + // make copy of array before for each, data sent to each client is the same + foreach (int id in connectionIds) + { + server.Send(id, buffer); + } + } + + public void SendOne(int connectionId, ArraySegment source) + { + ArrayBuffer buffer = bufferPool.Take(source.Count); + buffer.CopyFrom(source); + + server.Send(connectionId, buffer); + } + + public bool KickClient(int connectionId) + { + return server.CloseConnection(connectionId); + } + + public string GetClientAddress(int connectionId) + { + return server.GetClientAddress(connectionId); + } + + /// + /// Processes all new messages + /// + public void ProcessMessageQueue() + { + ProcessMessageQueue(null); + } + + /// + /// Processes all messages while is enabled + /// + /// + public void ProcessMessageQueue(MonoBehaviour behaviour) + { + int processedCount = 0; + bool skipEnabled = behaviour == null; + // check enabled every time in case behaviour was disabled after data + while ( + (skipEnabled || behaviour.enabled) && + processedCount < maxMessagesPerTick && + // Dequeue last + server.receiveQueue.TryDequeue(out Message next) + ) + { + processedCount++; + + switch (next.type) + { + case EventType.Connected: + onConnect?.Invoke(next.connId); + break; + case EventType.Data: + onData?.Invoke(next.connId, next.data.ToSegment()); + next.data.Release(); + break; + case EventType.Disconnected: + onDisconnect?.Invoke(next.connId); + break; + case EventType.Error: + onError?.Invoke(next.connId, next.exception); + break; + } + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs.meta new file mode 100644 index 0000000..c8c6f5a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd51d7896f55a5e48b41a4b526562b0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs new file mode 100644 index 0000000..da4f402 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net.Sockets; +using System.Threading; + +namespace Mirror.SimpleWeb +{ + public class WebSocketServer + { + public readonly ConcurrentQueue receiveQueue = new ConcurrentQueue(); + + readonly TcpConfig tcpConfig; + readonly int maxMessageSize; + + TcpListener listener; + Thread acceptThread; + bool serverStopped; + readonly ServerHandshake handShake; + readonly ServerSslHelper sslHelper; + readonly BufferPool bufferPool; + readonly ConcurrentDictionary connections = new ConcurrentDictionary(); + + + int _idCounter = 0; + + public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool) + { + this.tcpConfig = tcpConfig; + this.maxMessageSize = maxMessageSize; + sslHelper = new ServerSslHelper(sslConfig); + this.bufferPool = bufferPool; + handShake = new ServerHandshake(this.bufferPool, handshakeMaxSize); + } + + public void Listen(int port) + { + listener = TcpListener.Create(port); + listener.Start(); + + Log.Info($"Server has started on port {port}"); + + acceptThread = new Thread(acceptLoop); + acceptThread.IsBackground = true; + acceptThread.Start(); + } + + public void Stop() + { + serverStopped = true; + + // Interrupt then stop so that Exception is handled correctly + acceptThread?.Interrupt(); + listener?.Stop(); + acceptThread = null; + + Log.Info("Server stopped, Closing all connections..."); + // make copy so that foreach doesn't break if values are removed + Connection[] connectionsCopy = connections.Values.ToArray(); + foreach (Connection conn in connectionsCopy) + { + conn.Dispose(); + } + + connections.Clear(); + } + + void acceptLoop() + { + try + { + try + { + while (true) + { + TcpClient client = listener.AcceptTcpClient(); + tcpConfig.ApplyTo(client); + + + // TODO keep track of connections before they are in connections dictionary + // this might not be a problem as HandshakeAndReceiveLoop checks for stop + // and returns/disposes before sending message to queue + Connection conn = new Connection(client, AfterConnectionDisposed); + Log.Info($"A client connected {conn}"); + + // handshake needs its own thread as it needs to wait for message from client + Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn)); + + conn.receiveThread = receiveThread; + + receiveThread.IsBackground = true; + receiveThread.Start(); + } + } + catch (SocketException) + { + // check for Interrupted/Abort + Utils.CheckForInterupt(); + throw; + } + } + catch (ThreadInterruptedException e) { Log.InfoException(e); } + catch (ThreadAbortException e) { Log.InfoException(e); } + catch (Exception e) { Log.Exception(e); } + } + + void HandshakeAndReceiveLoop(Connection conn) + { + try + { + bool success = sslHelper.TryCreateStream(conn); + if (!success) + { + Log.Error($"Failed to create SSL Stream {conn}"); + conn.Dispose(); + return; + } + + success = handShake.TryHandshake(conn); + + if (success) + { + Log.Info($"Sent Handshake {conn}"); + } + else + { + Log.Error($"Handshake Failed {conn}"); + conn.Dispose(); + return; + } + + // check if Stop has been called since accepting this client + if (serverStopped) + { + Log.Info("Server stops after successful handshake"); + return; + } + + conn.connId = Interlocked.Increment(ref _idCounter); + connections.TryAdd(conn.connId, conn); + + receiveQueue.Enqueue(new Message(conn.connId, EventType.Connected)); + + Thread sendThread = new Thread(() => + { + SendLoop.Config sendConfig = new SendLoop.Config( + conn, + bufferSize: Constants.HeaderSize + maxMessageSize, + setMask: false); + + SendLoop.Loop(sendConfig); + }); + + conn.sendThread = sendThread; + sendThread.IsBackground = true; + sendThread.Name = $"SendLoop {conn.connId}"; + sendThread.Start(); + + ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config( + conn, + maxMessageSize, + expectMask: true, + receiveQueue, + bufferPool); + + ReceiveLoop.Loop(receiveConfig); + } + catch (ThreadInterruptedException e) { Log.InfoException(e); } + catch (ThreadAbortException e) { Log.InfoException(e); } + catch (Exception e) { Log.Exception(e); } + finally + { + // close here in case connect fails + conn.Dispose(); + } + } + + void AfterConnectionDisposed(Connection conn) + { + if (conn.connId != Connection.IdNotSet) + { + receiveQueue.Enqueue(new Message(conn.connId, EventType.Disconnected)); + connections.TryRemove(conn.connId, out Connection _); + } + } + + public void Send(int id, ArrayBuffer buffer) + { + if (connections.TryGetValue(id, out Connection conn)) + { + conn.sendQueue.Enqueue(buffer); + conn.sendPending.Set(); + } + else + { + Log.Warn($"Cant send message to {id} because connection was not found in dictionary. Maybe it disconnected."); + } + } + + public bool CloseConnection(int id) + { + if (connections.TryGetValue(id, out Connection conn)) + { + Log.Info($"Kicking connection {id}"); + conn.Dispose(); + return true; + } + else + { + Log.Warn($"Failed to kick {id} because id not found"); + + return false; + } + } + + public string GetClientAddress(int id) + { + if (connections.TryGetValue(id, out Connection conn)) + { + return conn.client.Client.RemoteEndPoint.ToString(); + } + else + { + Log.Error($"Cant get address of connection {id} because connection was not found in dictionary"); + return null; + } + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs.meta new file mode 100644 index 0000000..0a76a9f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c434db044777d2439bae5a57d4e8ee7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef new file mode 100644 index 0000000..3687c5d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef @@ -0,0 +1,14 @@ +{ + "name": "SimpleWebTransport", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta new file mode 100644 index 0000000..99755b6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3b5390adca4e2bb4791cb930316d6f3e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs new file mode 100644 index 0000000..66badc3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs @@ -0,0 +1,293 @@ +using System; +using System.Net; +using System.Security.Authentication; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mirror.SimpleWeb +{ + [DisallowMultipleComponent] + public class SimpleWebTransport : Transport + { + public const string NormalScheme = "ws"; + public const string SecureScheme = "wss"; + + [Tooltip("Port to use for server and client")] + public ushort port = 7778; + + + [Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")] + public int maxMessageSize = 16 * 1024; + + [Tooltip("Max size for http header send as handshake for websockets")] + public int handshakeMaxSize = 3000; + + [Tooltip("disables nagle algorithm. lowers CPU% and latency but increases bandwidth")] + public bool noDelay = true; + + [Tooltip("Send would stall forever if the network is cut off during a send, so we need a timeout (in milliseconds)")] + public int sendTimeout = 5000; + + [Tooltip("How long without a message before disconnecting (in milliseconds)")] + public int receiveTimeout = 20000; + + [Tooltip("Caps the number of messages the server will process per tick. Allows LateUpdate to finish to let the reset of unity continue in case more messages arrive before they are processed")] + public int serverMaxMessagesPerTick = 10000; + + [Tooltip("Caps the number of messages the client will process per tick. Allows LateUpdate to finish to let the reset of unity continue in case more messages arrive before they are processed")] + public int clientMaxMessagesPerTick = 1000; + + [Header("Server settings")] + + [Tooltip("Groups messages in queue before calling Stream.Send")] + public bool batchSend = true; + + [Tooltip("Waits for 1ms before grouping and sending messages. " + + "This gives time for mirror to finish adding message to queue so that less groups need to be made. " + + "If WaitBeforeSend is true then BatchSend Will also be set to true")] + public bool waitBeforeSend = false; + + + [Header("Ssl Settings")] + [Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport, NOTE: if sslEnabled is true clientUseWss is also true")] + public bool clientUseWss; + + public bool sslEnabled; + [Tooltip("Path to json file that contains path to cert and its password\n\nUse Json file so that cert password is not included in client builds\n\nSee cert.example.Json")] + public string sslCertJson = "./cert.json"; + public SslProtocols sslProtocols = SslProtocols.Tls12; + + [Header("Debug")] + [Tooltip("Log functions uses ConditionalAttribute which will effect which log methods are allowed. DEBUG allows warn/error, SIMPLEWEB_LOG_ENABLED allows all")] + [FormerlySerializedAs("logLevels")] + [SerializeField] Log.Levels _logLevels = Log.Levels.none; + + /// + /// Gets _logLevels field + /// Sets _logLevels and Log.level fields + /// + public Log.Levels LogLevels + { + get => _logLevels; + set + { + _logLevels = value; + Log.level = _logLevels; + } + } + + void OnValidate() + { + Log.level = _logLevels; + } + + SimpleWebClient client; + SimpleWebServer server; + + TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout); + + public override bool Available() + { + return true; + } + public override int GetMaxPacketSize(int channelId = 0) + { + return maxMessageSize; + } + + void Awake() + { + Log.level = _logLevels; + } + public override void Shutdown() + { + client?.Disconnect(); + client = null; + server?.Stop(); + server = null; + } + + #region Client + string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme; + string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme; + public override bool ClientConnected() + { + // not null and not NotConnected (we want to return true if connecting or disconnecting) + return client != null && client.ConnectionState != ClientState.NotConnected; + } + + public override void ClientConnect(string hostname) + { + // connecting or connected + if (ClientConnected()) + { + Debug.LogError("Already Connected"); + return; + } + + UriBuilder builder = new UriBuilder + { + Scheme = GetClientScheme(), + Host = hostname, + Port = port + }; + + + client = SimpleWebClient.Create(maxMessageSize, clientMaxMessagesPerTick, TcpConfig); + if (client == null) { return; } + + client.onConnect += OnClientConnected.Invoke; + client.onDisconnect += () => + { + OnClientDisconnected.Invoke(); + // clear client here after disconnect event has been sent + // there should be no more messages after disconnect + client = null; + }; + client.onData += (ArraySegment data) => OnClientDataReceived.Invoke(data, Channels.Reliable); + client.onError += (Exception e) => + { + OnClientError.Invoke(e); + ClientDisconnect(); + }; + + client.Connect(builder.Uri); + } + + public override void ClientDisconnect() + { + // don't set client null here of messages wont be processed + client?.Disconnect(); + } + + public override void ClientSend(ArraySegment segment, int channelId) + { + if (!ClientConnected()) + { + Debug.LogError("Not Connected"); + return; + } + + if (segment.Count > maxMessageSize) + { + Log.Error("Message greater than max size"); + return; + } + + if (segment.Count == 0) + { + Log.Error("Message count was zero"); + return; + } + + client.Send(segment); + + // call event. might be null if no statistics are listening etc. + OnClientDataSent?.Invoke(segment, Channels.Reliable); + } + + // messages should always be processed in early update + public override void ClientEarlyUpdate() + { + client?.ProcessMessageQueue(this); + } + #endregion + + #region Server + public override bool ServerActive() + { + return server != null && server.Active; + } + + public override void ServerStart() + { + if (ServerActive()) + { + Debug.LogError("SimpleWebServer Already Started"); + } + + SslConfig config = SslConfigLoader.Load(sslEnabled, sslCertJson, sslProtocols); + server = new SimpleWebServer(serverMaxMessagesPerTick, TcpConfig, maxMessageSize, handshakeMaxSize, config); + + server.onConnect += OnServerConnected.Invoke; + server.onDisconnect += OnServerDisconnected.Invoke; + server.onData += (int connId, ArraySegment data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable); + server.onError += OnServerError.Invoke; + + SendLoopConfig.batchSend = batchSend || waitBeforeSend; + SendLoopConfig.sleepBeforeSend = waitBeforeSend; + + server.Start(port); + } + + public override void ServerStop() + { + if (!ServerActive()) + { + Debug.LogError("SimpleWebServer Not Active"); + } + + server.Stop(); + server = null; + } + + public override void ServerDisconnect(int connectionId) + { + if (!ServerActive()) + { + Debug.LogError("SimpleWebServer Not Active"); + } + + server.KickClient(connectionId); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + if (!ServerActive()) + { + Debug.LogError("SimpleWebServer Not Active"); + return; + } + + if (segment.Count > maxMessageSize) + { + Log.Error("Message greater than max size"); + return; + } + + if (segment.Count == 0) + { + Log.Error("Message count was zero"); + return; + } + + server.SendOne(connectionId, segment); + + // call event. might be null if no statistics are listening etc. + OnServerDataSent?.Invoke(connectionId, segment, Channels.Reliable); + } + + public override string ServerGetClientAddress(int connectionId) + { + return server.GetClientAddress(connectionId); + } + + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder + { + Scheme = GetServerScheme(), + Host = Dns.GetHostName(), + Port = port + }; + return builder.Uri; + } + + // messages should always be processed in early update + public override void ServerEarlyUpdate() + { + server?.ProcessMessageQueue(this); + } + #endregion + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs.meta new file mode 100644 index 0000000..381a5c7 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0110f245bfcfc7d459681f7bd9ebc590 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs new file mode 100644 index 0000000..4baf8c5 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs @@ -0,0 +1,52 @@ +using System.IO; +using System.Security.Authentication; +using UnityEngine; + +namespace Mirror.SimpleWeb +{ + + public class SslConfigLoader + { + internal struct Cert + { + public string path; + public string password; + } + public static SslConfig Load(bool sslEnabled, string sslCertJson, SslProtocols sslProtocols) + { + // don't need to load anything if ssl is not enabled + if (!sslEnabled) + return default; + + string certJsonPath = sslCertJson; + + Cert cert = LoadCertJson(certJsonPath); + + return new SslConfig( + enabled: sslEnabled, + sslProtocols: sslProtocols, + certPath: cert.path, + certPassword: cert.password + ); + } + + internal static Cert LoadCertJson(string certJsonPath) + { + string json = File.ReadAllText(certJsonPath); + Cert cert = JsonUtility.FromJson(json); + + if (string.IsNullOrWhiteSpace(cert.path)) + { + throw new InvalidDataException("Cert Json didn't not contain \"path\""); + } + // don't use IsNullOrWhiteSpace here because whitespace could be a valid password for a cert + if (string.IsNullOrEmpty(cert.password)) + { + // password can be empty + cert.password = string.Empty; + } + + return cert; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs.meta new file mode 100644 index 0000000..e653532 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dfdb6b97a48a48b498e563e857342da1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy.meta b/Assets/Mirror/Runtime/Transports/Telepathy.meta new file mode 100644 index 0000000..ede2d0e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 552b3d8382916438d81fe7f39e18db72 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy.meta new file mode 100644 index 0000000..345a638 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a1233408bc4b145fb8f6f5a8e95790e0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs new file mode 100644 index 0000000..73e775c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs @@ -0,0 +1,362 @@ +using System; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + // ClientState OBJECT that can be handed to the ReceiveThread safely. + // => allows us to create a NEW OBJECT every time we connect and start a + // receive thread. + // => perfectly protects us against data races. fixes all the flaky tests + // where .Connecting or .client would still be used by a dieing thread + // while attempting to use it for a new connection attempt etc. + // => creating a fresh client state each time is the best solution against + // data races here! + class ClientConnectionState : ConnectionState + { + public Thread receiveThread; + + // TcpClient.Connected doesn't check if socket != null, which + // results in NullReferenceExceptions if connection was closed. + // -> let's check it manually instead + public bool Connected => client != null && + client.Client != null && + client.Client.Connected; + + // TcpClient has no 'connecting' state to check. We need to keep track + // of it manually. + // -> checking 'thread.IsAlive && !Connected' is not enough because the + // thread is alive and connected is false for a short moment after + // disconnecting, so this would cause race conditions. + // -> we use a threadsafe bool wrapper so that ThreadFunction can remain + // static (it needs a common lock) + // => Connecting is true from first Connect() call in here, through the + // thread start, until TcpClient.Connect() returns. Simple and clear. + // => bools are atomic according to + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables + // made volatile so the compiler does not reorder access to it + public volatile bool Connecting; + + // thread safe pipe for received messages + // => inside client connection state so that we can create a new state + // each time we connect + // (unlike server which has one receive pipe for all connections) + public readonly MagnificentReceivePipe receivePipe; + + // constructor always creates new TcpClient for client connection! + public ClientConnectionState(int MaxMessageSize) : base(new TcpClient(), MaxMessageSize) + { + // create receive pipe with max message size for pooling + receivePipe = new MagnificentReceivePipe(MaxMessageSize); + } + + // dispose all the state safely + public void Dispose() + { + // close client + client.Close(); + + // wait until thread finished. this is the only way to guarantee + // that we can call Connect() again immediately after Disconnect + // -> calling .Join would sometimes wait forever, e.g. when + // calling Disconnect while trying to connect to a dead end + receiveThread?.Interrupt(); + + // we interrupted the receive Thread, so we can't guarantee that + // connecting was reset. let's do it manually. + Connecting = false; + + // clear send pipe. no need to hold on to elements. + // (unlike receiveQueue, which is still needed to process the + // latest Disconnected message, etc.) + sendPipe.Clear(); + + // IMPORTANT: DO NOT CLEAR RECEIVE PIPE. + // we still want to process disconnect messages in Tick()! + + // let go of this client completely. the thread ended, no one uses + // it anymore and this way Connected is false again immediately. + client = null; + } + } + + public class Client : Common + { + // events to hook into + // => OnData uses ArraySegment for allocation free receives later + public Action OnConnected; + public Action> OnData; + public Action OnDisconnected; + + // disconnect if send queue gets too big. + // -> avoids ever growing queue memory if network is slower than input + // -> disconnecting is great for load balancing. better to disconnect + // one connection than risking every connection / the whole server + // -> huge queue would introduce multiple seconds of latency anyway + // + // Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size: + // limit = 1,000 means 16 MB of memory/connection + // limit = 10,000 means 160 MB of memory/connection + public int SendQueueLimit = 10000; + public int ReceiveQueueLimit = 10000; + + // all client state wrapped into an object that is passed to ReceiveThread + // => we create a new one each time we connect to avoid data races with + // old dieing threads still using the previous object! + ClientConnectionState state; + + // Connected & Connecting + public bool Connected => state != null && state.Connected; + public bool Connecting => state != null && state.Connecting; + + // pipe count, useful for debugging / benchmarks + public int ReceivePipeCount => state != null ? state.receivePipe.TotalCount : 0; + + // constructor + public Client(int MaxMessageSize) : base(MaxMessageSize) {} + + // the thread function + // STATIC to avoid sharing state! + // => pass ClientState object. a new one is created for each new thread! + // => avoids data races where an old dieing thread might still modify + // the current thread's state :/ + static void ReceiveThreadFunction(ClientConnectionState state, string ip, int port, int MaxMessageSize, bool NoDelay, int SendTimeout, int ReceiveTimeout, int ReceiveQueueLimit) + + { + Thread sendThread = null; + + // absolutely must wrap with try/catch, otherwise thread + // exceptions are silent + try + { + // connect (blocking) + state.client.Connect(ip, port); + state.Connecting = false; // volatile! + + // set socket options after the socket was created in Connect() + // (not after the constructor because we clear the socket there) + state.client.NoDelay = NoDelay; + state.client.SendTimeout = SendTimeout; + state.client.ReceiveTimeout = ReceiveTimeout; + + // start send thread only after connected + // IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS! + sendThread = new Thread(() => { ThreadFunctions.SendLoop(0, state.client, state.sendPipe, state.sendPending); }); + sendThread.IsBackground = true; + sendThread.Start(); + + // run the receive loop + // (receive pipe is shared across all loops) + ThreadFunctions.ReceiveLoop(0, state.client, MaxMessageSize, state.receivePipe, ReceiveQueueLimit); + } + catch (SocketException exception) + { + // this happens if (for example) the ip address is correct + // but there is no server running on that ip/port + Log.Info("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception); + + // add 'Disconnected' event to receive pipe so that the caller + // knows that the Connect failed. otherwise they will never know + state.receivePipe.Enqueue(0, EventType.Disconnected, default); + } + catch (ThreadInterruptedException) + { + // expected if Disconnect() aborts it + } + catch (ThreadAbortException) + { + // expected if Disconnect() aborts it + } + catch (ObjectDisposedException) + { + // expected if Disconnect() aborts it and disposed the client + // while ReceiveThread is in a blocking Connect() call + } + catch (Exception exception) + { + // something went wrong. probably important. + Log.Error("Client Recv Exception: " + exception); + } + + // sendthread might be waiting on ManualResetEvent, + // so let's make sure to end it if the connection + // closed. + // otherwise the send thread would only end if it's + // actually sending data while the connection is + // closed. + sendThread?.Interrupt(); + + // Connect might have failed. thread might have been closed. + // let's reset connecting state no matter what. + state.Connecting = false; + + // if we got here then we are done. ReceiveLoop cleans up already, + // but we may never get there if connect fails. so let's clean up + // here too. + state.client?.Close(); + } + + public void Connect(string ip, int port) + { + // not if already started + if (Connecting || Connected) + { + Log.Warning("Telepathy Client can not create connection because an existing connection is connecting or connected"); + return; + } + + // overwrite old thread's state object. create a new one to avoid + // data races where an old dieing thread might still modify the + // current state! fixes all the flaky tests! + state = new ClientConnectionState(MaxMessageSize); + + // We are connecting from now until Connect succeeds or fails + state.Connecting = true; + + // create a TcpClient with perfect IPv4, IPv6 and hostname resolving + // support. + // + // * TcpClient(hostname, port): works but would connect (and block) + // already + // * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6 + // addresses but only connects to IPv6 servers (e.g. Telepathy). + // does NOT connect to IPv4 servers (e.g. Mirror Booster), even + // with DualMode enabled. + // * TcpClient(): creates IPv4 socket internally, which would force + // Connect() to only use IPv4 sockets. + // + // => the trick is to clear the internal IPv4 socket so that Connect + // resolves the hostname and creates either an IPv4 or an IPv6 + // socket as needed (see TcpClient source) + state.client.Client = null; // clear internal IPv4 socket until Connect() + + // client.Connect(ip, port) is blocking. let's call it in the thread + // and return immediately. + // -> this way the application doesn't hang for 30s if connect takes + // too long, which is especially good in games + // -> this way we don't async client.BeginConnect, which seems to + // fail sometimes if we connect too many clients too fast + state.receiveThread = new Thread(() => { + ReceiveThreadFunction(state, ip, port, MaxMessageSize, NoDelay, SendTimeout, ReceiveTimeout, ReceiveQueueLimit); + }); + state.receiveThread.IsBackground = true; + state.receiveThread.Start(); + } + + public void Disconnect() + { + // only if started + if (Connecting || Connected) + { + // dispose all the state safely + state.Dispose(); + + // IMPORTANT: DO NOT set state = null! + // we still want to process the pipe's disconnect message etc.! + } + } + + // send message to server using socket connection. + // arraysegment for allocation free sends later. + // -> the segment's array is only used until Send() returns! + public bool Send(ArraySegment message) + { + if (Connected) + { + // respect max message size to avoid allocation attacks. + if (message.Count <= MaxMessageSize) + { + // check send pipe limit + if (state.sendPipe.Count < SendQueueLimit) + { + // add to thread safe send pipe and return immediately. + // calling Send here would be blocking (sometimes for long + // times if other side lags or wire was disconnected) + state.sendPipe.Enqueue(message); + state.sendPending.Set(); // interrupt SendThread WaitOne() + return true; + } + // disconnect if send queue gets too big. + // -> avoids ever growing queue memory if network is slower + // than input + // -> avoids ever growing latency as well + // + // note: while SendThread always grabs the WHOLE send queue + // immediately, it's still possible that the sending + // blocks for so long that the send queue just gets + // way too big. have a limit - better safe than sorry. + else + { + // log the reason + Log.Warning($"Client.Send: sendPipe reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting to avoid ever growing memory & latency."); + + // just close it. send thread will take care of the rest. + state.client.Close(); + return false; + } + } + Log.Error("Client.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize); + return false; + } + Log.Warning("Client.Send: not connected!"); + return false; + } + + // tick: processes up to 'limit' messages + // => limit parameter to avoid deadlocks / too long freezes if server or + // client is too slow to process network load + // => Mirror & DOTSNET need to have a process limit anyway. + // might as well do it here and make life easier. + // => returns amount of remaining messages to process, so the caller + // can call tick again as many times as needed (or up to a limit) + // + // Tick() may process multiple messages, but Mirror needs a way to stop + // processing immediately if a scene change messages arrives. Mirror + // can't process any other messages during a scene change. + // (could be useful for others too) + // => make sure to allocate the lambda only once in transports + public int Tick(int processLimit, Func checkEnabled = null) + { + // only if state was created yet (after connect()) + // note: we don't check 'only if connected' because we want to still + // process Disconnect messages afterwards too! + if (state == null) + return 0; + + // process up to 'processLimit' messages + for (int i = 0; i < processLimit; ++i) + { + // check enabled in case a Mirror scene message arrived + if (checkEnabled != null && !checkEnabled()) + break; + + // peek first. allows us to process the first queued entry while + // still keeping the pooled byte[] alive by not removing anything. + if (state.receivePipe.TryPeek(out int _, out EventType eventType, out ArraySegment message)) + { + switch (eventType) + { + case EventType.Connected: + OnConnected?.Invoke(); + break; + case EventType.Data: + OnData?.Invoke(message); + break; + case EventType.Disconnected: + OnDisconnected?.Invoke(); + break; + } + + // IMPORTANT: now dequeue and return it to pool AFTER we are + // done processing the event. + state.receivePipe.TryDequeue(); + } + // no more messages. stop the loop. + else break; + } + + // return what's left to process for next time + return state.receivePipe.TotalCount; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs.meta new file mode 100644 index 0000000..1b6d222 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5b95294cc4ec4b15aacba57531c7985 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs new file mode 100644 index 0000000..15265f9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs @@ -0,0 +1,39 @@ +// common code used by server and client +namespace Telepathy +{ + public abstract class Common + { + // IMPORTANT: DO NOT SHARE STATE ACROSS SEND/RECV LOOPS (DATA RACES) + // (except receive pipe which is used for all threads) + + // NoDelay disables nagle algorithm. lowers CPU% and latency but + // increases bandwidth + public bool NoDelay = true; + + // Prevent allocation attacks. Each packet is prefixed with a length + // header, so an attacker could send a fake packet with length=2GB, + // causing the server to allocate 2GB and run out of memory quickly. + // -> simply increase max packet size if you want to send around bigger + // files! + // -> 16KB per message should be more than enough. + public readonly int MaxMessageSize; + + // Send would stall forever if the network is cut off during a send, so + // we need a timeout (in milliseconds) + public int SendTimeout = 5000; + + // Default TCP receive time out can be huge (minutes). + // That's way too much for games, let's make it configurable. + // we need a timeout (in milliseconds) + // => '0' means disabled + // => disabled by default because some people might use Telepathy + // without Mirror and without sending pings, so timeouts are likely + public int ReceiveTimeout = 0; + + // constructor + protected Common(int MaxMessageSize) + { + this.MaxMessageSize = MaxMessageSize; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs.meta new file mode 100644 index 0000000..5d8ab5b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4d56322cf0e248a89103c002a505dab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs new file mode 100644 index 0000000..cdfe3c0 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs @@ -0,0 +1,35 @@ +// both server and client need a connection state object. +// -> server needs it to keep track of multiple connections +// -> client needs it to safely create a new connection state on every new +// connect in order to avoid data races where a dieing thread might still +// modify the current state. can't happen if we create a new state each time! +// (fixes all the flaky tests) +// +// ... besides, it also allows us to share code! +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public class ConnectionState + { + public TcpClient client; + + // thread safe pipe to send messages from main thread to send thread + public readonly MagnificentSendPipe sendPipe; + + // ManualResetEvent to wake up the send thread. better than Thread.Sleep + // -> call Set() if everything was sent + // -> call Reset() if there is something to send again + // -> call WaitOne() to block until Reset was called + public ManualResetEvent sendPending = new ManualResetEvent(false); + + public ConnectionState(TcpClient client, int MaxMessageSize) + { + this.client = client; + + // create send pipe with max message size for pooling + sendPipe = new MagnificentSendPipe(MaxMessageSize); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs.meta new file mode 100644 index 0000000..3dcceaf --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af95e2b6f6343411aa8bdf871abd7b1b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta new file mode 100644 index 0000000..1bc9652 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 885e89897e3a03241827ab7a14fe5fa0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs new file mode 100644 index 0000000..4f7722a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs @@ -0,0 +1 @@ +// removed 2021-02-04 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta new file mode 100644 index 0000000..304866f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa8d703f0b73f4d6398b76812719b68b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs new file mode 100644 index 0000000..4f7722a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs @@ -0,0 +1 @@ +// removed 2021-02-04 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta new file mode 100644 index 0000000..5937bb9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aedf812e9637b4f92a35db1aedca8c92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs new file mode 100644 index 0000000..7899911 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs @@ -0,0 +1 @@ +// removed 2021-02-04 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta new file mode 100644 index 0000000..f3a9310 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8fc06e2fb29854a0c9e90c0188d36a08 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs new file mode 100644 index 0000000..85dece4 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs @@ -0,0 +1 @@ +// removed 2021-02-04 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta new file mode 100644 index 0000000..77c885d --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64df4eaebe4ff9a43a9fb318c3e8e321 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs new file mode 100644 index 0000000..66bc3b4 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs @@ -0,0 +1,9 @@ +namespace Telepathy +{ + public enum EventType + { + Connected, + Data, + Disconnected + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs.meta new file mode 100644 index 0000000..ac88c1b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49f1a330755814803be5f27f493e1910 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE new file mode 100644 index 0000000..680deef --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018, 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. diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta new file mode 100644 index 0000000..4d7664e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0ba11103b95fd4721bffbb08440d5b8e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs new file mode 100644 index 0000000..2d50aa3 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs @@ -0,0 +1,15 @@ +// A simple logger class that uses Console.WriteLine by default. +// Can also do Logger.LogMethod = Debug.Log for Unity etc. +// (this way we don't have to depend on UnityEngine.DLL and don't need a +// different version for every UnityEngine version here) +using System; + +namespace Telepathy +{ + public static class Log + { + public static Action Info = Console.WriteLine; + public static Action Warning = Console.WriteLine; + public static Action Error = Console.Error.WriteLine; + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta new file mode 100644 index 0000000..8f78650 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a123d054bef34d059057ac2ce936605 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs new file mode 100644 index 0000000..2e10318 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs @@ -0,0 +1,222 @@ +// a magnificent receive pipe to shield us from all of life's complexities. +// safely sends messages from receive thread to main thread. +// -> thread safety built in +// -> byte[] pooling coming in the future +// +// => hides all the complexity from telepathy +// => easy to switch between stack/queue/concurrentqueue/etc. +// => easy to test +using System; +using System.Collections.Generic; + +namespace Telepathy +{ + public class MagnificentReceivePipe + { + // queue entry message. only used in here. + // -> byte arrays are always of 4 + MaxMessageSize + // -> ArraySegment indicates the actual message content + struct Entry + { + public int connectionId; + public EventType eventType; + public ArraySegment data; + public Entry(int connectionId, EventType eventType, ArraySegment data) + { + this.connectionId = connectionId; + this.eventType = eventType; + this.data = data; + } + } + + // message queue + // ConcurrentQueue allocates. lock{} instead. + // + // IMPORTANT: lock{} all usages! + readonly Queue queue = new Queue(); + + // byte[] pool to avoid allocations + // Take & Return is beautifully encapsulated in the pipe. + // the outside does not need to worry about anything. + // and it can be tested easily. + // + // IMPORTANT: lock{} all usages! + Pool pool; + + // unfortunately having one receive pipe per connetionId is way slower + // in CCU tests. right now we have one pipe for all connections. + // => we still need to limit queued messages per connection to avoid one + // spamming connection being able to slow down everyone else since + // the queue would be full of just this connection's messages forever + // => let's use a simpler per-connectionId counter for now + Dictionary queueCounter = new Dictionary(); + + // constructor + public MagnificentReceivePipe(int MaxMessageSize) + { + // initialize pool to create max message sized byte[]s each time + pool = new Pool(() => new byte[MaxMessageSize]); + } + + // return amount of queued messages for this connectionId. + // for statistics. don't call Count and assume that it's the same after + // the call. + public int Count(int connectionId) + { + lock (this) + { + return queueCounter.TryGetValue(connectionId, out int count) + ? count + : 0; + } + } + + // total count + public int TotalCount + { + get { lock (this) { return queue.Count; } } + } + + // pool count for testing + public int PoolCount + { + get { lock (this) { return pool.Count(); } } + } + + // enqueue a message + // -> ArraySegment to avoid allocations later + // -> parameters passed directly so it's more obvious that we don't just + // queue a passed 'Message', instead we copy the ArraySegment into + // a byte[] and store it internally, etc.) + public void Enqueue(int connectionId, EventType eventType, ArraySegment message) + { + // pool & queue usage always needs to be locked + lock (this) + { + // does this message have a data array content? + ArraySegment segment = default; + if (message != default) + { + // ArraySegment is only valid until returning. + // copy it into a byte[] that we can store. + // ArraySegment array is only valid until returning, so copy + // it into a byte[] that we can queue safely. + + // get one from the pool first to avoid allocations + byte[] bytes = pool.Take(); + + // copy into it + Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count); + + // indicate which part is the message + segment = new ArraySegment(bytes, 0, message.Count); + } + + // enqueue it + // IMPORTANT: pass the segment around pool byte[], + // NOT the 'message' that is only valid until returning! + Entry entry = new Entry(connectionId, eventType, segment); + queue.Enqueue(entry); + + // increase counter for this connectionId + int oldCount = Count(connectionId); + queueCounter[connectionId] = oldCount + 1; + } + } + + // peek the next message + // -> allows the caller to process it while pipe still holds on to the + // byte[] + // -> TryDequeue should be called after processing, so that the message + // is actually dequeued and the byte[] is returned to pool! + // => see TryDequeue comments! + // + // IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD! + public bool TryPeek(out int connectionId, out EventType eventType, out ArraySegment data) + { + connectionId = 0; + eventType = EventType.Disconnected; + data = default; + + // pool & queue usage always needs to be locked + lock (this) + { + if (queue.Count > 0) + { + Entry entry = queue.Peek(); + connectionId = entry.connectionId; + eventType = entry.eventType; + data = entry.data; + return true; + } + return false; + } + } + + // dequeue the next message + // -> simply dequeues and returns the byte[] to pool (if any) + // -> use Peek to actually process the first element while the pipe + // still holds on to the byte[] + // -> doesn't return the element because the byte[] needs to be returned + // to the pool in dequeue. caller can't be allowed to work with a + // byte[] that is already returned to pool. + // => Peek & Dequeue is the most simple, clean solution for receive + // pipe pooling to avoid allocations! + // + // IMPORTANT: TryPeek & Dequeue need to be called from the SAME THREAD! + public bool TryDequeue() + { + // pool & queue usage always needs to be locked + lock (this) + { + if (queue.Count > 0) + { + // dequeue from queue + Entry entry = queue.Dequeue(); + + // return byte[] to pool (if any). + // not all message types have byte[] contents. + if (entry.data != default) + { + pool.Return(entry.data.Array); + } + + // decrease counter for this connectionId + queueCounter[entry.connectionId]--; + + // remove if zero. don't want to keep old connectionIds in + // there forever, it would cause slowly growing memory. + if (queueCounter[entry.connectionId] == 0) + queueCounter.Remove(entry.connectionId); + + return true; + } + return false; + } + } + + public void Clear() + { + // pool & queue usage always needs to be locked + lock (this) + { + // clear queue, but via dequeue to return each byte[] to pool + while (queue.Count > 0) + { + // dequeue + Entry entry = queue.Dequeue(); + + // return byte[] to pool (if any). + // not all message types have byte[] contents. + if (entry.data != default) + { + pool.Return(entry.data.Array); + } + } + + // clear counter too + queueCounter.Clear(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta new file mode 100644 index 0000000..614bab6 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 010a208972a9a4e0cb0e7c18a60b4494 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs new file mode 100644 index 0000000..be456a0 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs @@ -0,0 +1,165 @@ +// a magnificent send pipe to shield us from all of life's complexities. +// safely sends messages from main thread to send thread. +// -> thread safety built in +// -> byte[] pooling coming in the future +// +// => hides all the complexity from telepathy +// => easy to switch between stack/queue/concurrentqueue/etc. +// => easy to test + +using System; +using System.Collections.Generic; + +namespace Telepathy +{ + public class MagnificentSendPipe + { + // message queue + // ConcurrentQueue allocates. lock{} instead. + // -> byte arrays are always of MaxMessageSize + // -> ArraySegment indicates the actual message content + // + // IMPORTANT: lock{} all usages! + readonly Queue> queue = new Queue>(); + + // byte[] pool to avoid allocations + // Take & Return is beautifully encapsulated in the pipe. + // the outside does not need to worry about anything. + // and it can be tested easily. + // + // IMPORTANT: lock{} all usages! + Pool pool; + + // constructor + public MagnificentSendPipe(int MaxMessageSize) + { + // initialize pool to create max message sized byte[]s each time + pool = new Pool(() => new byte[MaxMessageSize]); + } + + // for statistics. don't call Count and assume that it's the same after + // the call. + public int Count + { + get { lock (this) { return queue.Count; } } + } + + // pool count for testing + public int PoolCount + { + get { lock (this) { return pool.Count(); } } + } + + // enqueue a message + // arraysegment for allocation free sends later. + // -> the segment's array is only used until Enqueue() returns! + public void Enqueue(ArraySegment message) + { + // pool & queue usage always needs to be locked + lock (this) + { + // ArraySegment array is only valid until returning, so copy + // it into a byte[] that we can queue safely. + + // get one from the pool first to avoid allocations + byte[] bytes = pool.Take(); + + // copy into it + Buffer.BlockCopy(message.Array, message.Offset, bytes, 0, message.Count); + + // indicate which part is the message + ArraySegment segment = new ArraySegment(bytes, 0, message.Count); + + // now enqueue it + queue.Enqueue(segment); + } + } + + // send threads need to dequeue each byte[] and write it into the socket + // -> dequeueing one byte[] after another works, but it's WAY slower + // than dequeueing all immediately (locks only once) + // lock{} & DequeueAll is WAY faster than ConcurrentQueue & dequeue + // one after another: + // + // uMMORPG 450 CCU + // SafeQueue: 900-1440ms latency + // ConcurrentQueue: 2000ms latency + // + // -> the most obvious solution is to just return a list with all byte[] + // (which allocates) and then write each one into the socket + // -> a faster solution is to serialize each one into one payload buffer + // and pass that to the socket only once. fewer socket calls always + // give WAY better CPU performance(!) + // -> to avoid allocating a new list of entries each time, we simply + // serialize all entries into the payload here already + // => having all this complexity built into the pipe makes testing and + // modifying the algorithm super easy! + // + // IMPORTANT: serializing in here will allow us to return the byte[] + // entries back to a pool later to completely avoid + // allocations! + public bool DequeueAndSerializeAll(ref byte[] payload, out int packetSize) + { + // pool & queue usage always needs to be locked + lock (this) + { + // do nothing if empty + packetSize = 0; + if (queue.Count == 0) + return false; + + // we might have multiple pending messages. merge into one + // packet to avoid TCP overheads and improve performance. + // + // IMPORTANT: Mirror & DOTSNET already batch into MaxMessageSize + // chunks, but we STILL pack all pending messages + // into one large payload so we only give it to TCP + // ONCE. This is HUGE for performance so we keep it! + packetSize = 0; + foreach (ArraySegment message in queue) + packetSize += 4 + message.Count; // header + content + + // create payload buffer if not created yet or previous one is + // too small + // IMPORTANT: payload.Length might be > packetSize! don't use it! + if (payload == null || payload.Length < packetSize) + payload = new byte[packetSize]; + + // dequeue all byte[] messages and serialize into the packet + int position = 0; + while (queue.Count > 0) + { + // dequeue + ArraySegment message = queue.Dequeue(); + + // write header (size) into buffer at position + Utils.IntToBytesBigEndianNonAlloc(message.Count, payload, position); + position += 4; + + // copy message into payload at position + Buffer.BlockCopy(message.Array, message.Offset, payload, position, message.Count); + position += message.Count; + + // return to pool so it can be reused (avoids allocations!) + pool.Return(message.Array); + } + + // we did serialize something + return true; + } + } + + public void Clear() + { + // pool & queue usage always needs to be locked + lock (this) + { + // clear queue, but via dequeue to return each byte[] to pool + while (queue.Count > 0) + { + pool.Return(queue.Dequeue().Array); + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta new file mode 100644 index 0000000..cf1415f --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d490021c2e6a64374bc88168cec75c70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs new file mode 100644 index 0000000..7cfd73c --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Net.Sockets; + +namespace Telepathy +{ + public static class NetworkStreamExtensions + { + // .Read returns '0' if remote closed the connection but throws an + // IOException if we voluntarily closed our own connection. + // + // let's add a ReadSafely method that returns '0' in both cases so we don't + // have to worry about exceptions, since a disconnect is a disconnect... + public static int ReadSafely(this NetworkStream stream, byte[] buffer, int offset, int size) + { + try + { + return stream.Read(buffer, offset, size); + } + // IOException happens if we voluntarily closed our own connection. + catch (IOException) + { + return 0; + } + // ObjectDisposedException can be thrown if Client.Disconnect() + // disposes the stream, while we are still trying to read here. + // catching it fixes https://github.com/vis2k/Telepathy/pull/104 + catch (ObjectDisposedException) + { + return 0; + } + } + + // helper function to read EXACTLY 'n' bytes + // -> default .Read reads up to 'n' bytes. this function reads exactly + // 'n' bytes + // -> this is blocking until 'n' bytes were received + // -> immediately returns false in case of disconnects + public static bool ReadExactly(this NetworkStream stream, byte[] buffer, int amount) + { + // there might not be enough bytes in the TCP buffer for .Read to read + // the whole amount at once, so we need to keep trying until we have all + // the bytes (blocking) + // + // note: this just is a faster version of reading one after another: + // for (int i = 0; i < amount; ++i) + // if (stream.Read(buffer, i, 1) == 0) + // return false; + // return true; + int bytesRead = 0; + while (bytesRead < amount) + { + // read up to 'remaining' bytes with the 'safe' read extension + int remaining = amount - bytesRead; + int result = stream.ReadSafely(buffer, bytesRead, remaining); + + // .Read returns 0 if disconnected + if (result == 0) + return false; + + // otherwise add to bytes read + bytesRead += result; + } + return true; + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta new file mode 100644 index 0000000..e7e5744 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a8076c43fa8d4d45831adae232d4d3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs new file mode 100644 index 0000000..4ec4fd2 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs @@ -0,0 +1,34 @@ +// pool to avoid allocations. originally from libuv2k. +using System; +using System.Collections.Generic; + +namespace Telepathy +{ + public class Pool + { + // objects + readonly Stack objects = new Stack(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + // constructor + public Pool(Func objectGenerator) + { + this.objectGenerator = objectGenerator; + } + + // take an element from the pool, or create a new one if empty + public T Take() => objects.Count > 0 ? objects.Pop() : objectGenerator(); + + // return an element to the pool + public void Return(T item) => objects.Push(item); + + // clear the pool with the disposer function applied to each object + public void Clear() => objects.Clear(); + + // count to see how many objects are in the pool. useful for tests. + public int Count() => objects.Count; + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs.meta new file mode 100644 index 0000000..9a7dafc --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d3e530f6872642ec81e9b8b76277c93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs new file mode 100644 index 0000000..0b4ada7 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public class Server : Common + { + // events to hook into + // => OnData uses ArraySegment for allocation free receives later + public Action OnConnected; + public Action> OnData; + public Action OnDisconnected; + + // listener + public TcpListener listener; + Thread listenerThread; + + // disconnect if send queue gets too big. + // -> avoids ever growing queue memory if network is slower than input + // -> disconnecting is great for load balancing. better to disconnect + // one connection than risking every connection / the whole server + // -> huge queue would introduce multiple seconds of latency anyway + // + // Mirror/DOTSNET use MaxMessageSize batching, so for a 16kb max size: + // limit = 1,000 means 16 MB of memory/connection + // limit = 10,000 means 160 MB of memory/connection + public int SendQueueLimit = 10000; + public int ReceiveQueueLimit = 10000; + + // thread safe pipe for received messages + // IMPORTANT: unfortunately using one pipe per connection is way slower + // when testing 150 CCU. we need to use one pipe for all + // connections. this scales beautifully. + protected MagnificentReceivePipe receivePipe; + + // pipe count, useful for debugging / benchmarks + public int ReceivePipeTotalCount => receivePipe.TotalCount; + + // clients with + readonly ConcurrentDictionary clients = new ConcurrentDictionary(); + + // connectionId counter + int counter; + + // public next id function in case someone needs to reserve an id + // (e.g. if hostMode should always have 0 connection and external + // connections should start at 1, etc.) + public int NextConnectionId() + { + int id = Interlocked.Increment(ref counter); + + // it's very unlikely that we reach the uint limit of 2 billion. + // even with 1 new connection per second, this would take 68 years. + // -> but if it happens, then we should throw an exception because + // the caller probably should stop accepting clients. + // -> it's hardly worth using 'bool Next(out id)' for that case + // because it's just so unlikely. + if (id == int.MaxValue) + { + throw new Exception("connection id limit reached: " + id); + } + + return id; + } + + // check if the server is running + public bool Active => listenerThread != null && listenerThread.IsAlive; + + // constructor + public Server(int MaxMessageSize) : base(MaxMessageSize) {} + + // the listener thread's listen function + // note: no maxConnections parameter. high level API should handle that. + // (Transport can't send a 'too full' message anyway) + void Listen(int port) + { + // absolutely must wrap with try/catch, otherwise thread + // exceptions are silent + try + { + // start listener on all IPv4 and IPv6 address via .Create + listener = TcpListener.Create(port); + listener.Server.NoDelay = NoDelay; + // IMPORTANT: do not set send/receive timeouts on listener. + // On linux setting the recv timeout will cause the blocking + // Accept call to timeout with EACCEPT (which mono interprets + // as EWOULDBLOCK). + // https://stackoverflow.com/questions/1917814/eagain-error-for-accept-on-blocking-socket/1918118#1918118 + // => fixes https://github.com/vis2k/Mirror/issues/2695 + // + //listener.Server.SendTimeout = SendTimeout; + //listener.Server.ReceiveTimeout = ReceiveTimeout; + listener.Start(); + Log.Info("Server: listening port=" + port); + + // keep accepting new clients + while (true) + { + // wait and accept new client + // note: 'using' sucks here because it will try to + // dispose after thread was started but we still need it + // in the thread + TcpClient client = listener.AcceptTcpClient(); + + // set socket options + client.NoDelay = NoDelay; + client.SendTimeout = SendTimeout; + client.ReceiveTimeout = ReceiveTimeout; + + // generate the next connection id (thread safely) + int connectionId = NextConnectionId(); + + // add to dict immediately + ConnectionState connection = new ConnectionState(client, MaxMessageSize); + clients[connectionId] = connection; + + // spawn a send thread for each client + Thread sendThread = new Thread(() => + { + // wrap in try-catch, otherwise Thread exceptions + // are silent + try + { + // run the send loop + // IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS! + ThreadFunctions.SendLoop(connectionId, client, connection.sendPipe, connection.sendPending); + } + catch (ThreadAbortException) + { + // happens on stop. don't log anything. + // (we catch it in SendLoop too, but it still gets + // through to here when aborting. don't show an + // error.) + } + catch (Exception exception) + { + Log.Error("Server send thread exception: " + exception); + } + }); + sendThread.IsBackground = true; + sendThread.Start(); + + // spawn a receive thread for each client + Thread receiveThread = new Thread(() => + { + // wrap in try-catch, otherwise Thread exceptions + // are silent + try + { + // run the receive loop + // (receive pipe is shared across all loops) + ThreadFunctions.ReceiveLoop(connectionId, client, MaxMessageSize, receivePipe, ReceiveQueueLimit); + + // IMPORTANT: do NOT remove from clients after the + // thread ends. need to do it in Tick() so that the + // disconnect event in the pipe is still processed. + // (removing client immediately would mean that the + // pipe is lost and the disconnect event is never + // processed) + + // sendthread might be waiting on ManualResetEvent, + // so let's make sure to end it if the connection + // closed. + // otherwise the send thread would only end if it's + // actually sending data while the connection is + // closed. + sendThread.Interrupt(); + } + catch (Exception exception) + { + Log.Error("Server client thread exception: " + exception); + } + }); + receiveThread.IsBackground = true; + receiveThread.Start(); + } + } + catch (ThreadAbortException exception) + { + // UnityEditor causes AbortException if thread is still + // running when we press Play again next time. that's okay. + Log.Info("Server thread aborted. That's okay. " + exception); + } + catch (SocketException exception) + { + // calling StopServer will interrupt this thread with a + // 'SocketException: interrupted'. that's okay. + Log.Info("Server Thread stopped. That's okay. " + exception); + } + catch (Exception exception) + { + // something went wrong. probably important. + Log.Error("Server Exception: " + exception); + } + } + + // start listening for new connections in a background thread and spawn + // a new thread for each one. + public bool Start(int port) + { + // not if already started + if (Active) return false; + + // create receive pipe with max message size for pooling + // => create new pipes every time! + // if an old receive thread is still finishing up, it might still + // be using the old pipes. we don't want to risk any old data for + // our new start here. + receivePipe = new MagnificentReceivePipe(MaxMessageSize); + + // start the listener thread + // (on low priority. if main thread is too busy then there is not + // much value in accepting even more clients) + Log.Info("Server: Start port=" + port); + listenerThread = new Thread(() => { Listen(port); }); + listenerThread.IsBackground = true; + listenerThread.Priority = ThreadPriority.BelowNormal; + listenerThread.Start(); + return true; + } + + public void Stop() + { + // only if started + if (!Active) return; + + Log.Info("Server: stopping..."); + + // stop listening to connections so that no one can connect while we + // close the client connections + // (might be null if we call Stop so quickly after Start that the + // thread was interrupted before even creating the listener) + listener?.Stop(); + + // kill listener thread at all costs. only way to guarantee that + // .Active is immediately false after Stop. + // -> calling .Join would sometimes wait forever + listenerThread?.Interrupt(); + listenerThread = null; + + // close all client connections + foreach (KeyValuePair kvp in clients) + { + TcpClient client = kvp.Value.client; + // close the stream if not closed yet. it may have been closed + // by a disconnect already, so use try/catch + try { client.GetStream().Close(); } catch {} + client.Close(); + } + + // clear clients list + clients.Clear(); + + // reset the counter in case we start up again so + // clients get connection ID's starting from 1 + counter = 0; + } + + // send message to client using socket connection. + // arraysegment for allocation free sends later. + // -> the segment's array is only used until Send() returns! + public bool Send(int connectionId, ArraySegment message) + { + // respect max message size to avoid allocation attacks. + if (message.Count <= MaxMessageSize) + { + // find the connection + if (clients.TryGetValue(connectionId, out ConnectionState connection)) + { + // check send pipe limit + if (connection.sendPipe.Count < SendQueueLimit) + { + // add to thread safe send pipe and return immediately. + // calling Send here would be blocking (sometimes for long + // times if other side lags or wire was disconnected) + connection.sendPipe.Enqueue(message); + connection.sendPending.Set(); // interrupt SendThread WaitOne() + return true; + } + // disconnect if send queue gets too big. + // -> avoids ever growing queue memory if network is slower + // than input + // -> disconnecting is great for load balancing. better to + // disconnect one connection than risking every + // connection / the whole server + // + // note: while SendThread always grabs the WHOLE send queue + // immediately, it's still possible that the sending + // blocks for so long that the send queue just gets + // way too big. have a limit - better safe than sorry. + else + { + // log the reason + Log.Warning($"Server.Send: sendPipe for connection {connectionId} reached limit of {SendQueueLimit}. This can happen if we call send faster than the network can process messages. Disconnecting this connection for load balancing."); + + // just close it. send thread will take care of the rest. + connection.client.Close(); + return false; + } + } + + // sending to an invalid connectionId is expected sometimes. + // for example, if a client disconnects, the server might still + // try to send for one frame before it calls GetNextMessages + // again and realizes that a disconnect happened. + // so let's not spam the console with log messages. + //Logger.Log("Server.Send: invalid connectionId: " + connectionId); + return false; + } + Log.Error("Server.Send: message too big: " + message.Count + ". Limit: " + MaxMessageSize); + return false; + } + + // client's ip is sometimes needed by the server, e.g. for bans + public string GetClientAddress(int connectionId) + { + // find the connection + if (clients.TryGetValue(connectionId, out ConnectionState connection)) + { + return ((IPEndPoint)connection.client.Client.RemoteEndPoint).Address.ToString(); + } + return ""; + } + + // disconnect (kick) a client + public bool Disconnect(int connectionId) + { + // find the connection + if (clients.TryGetValue(connectionId, out ConnectionState connection)) + { + // just close it. send thread will take care of the rest. + connection.client.Close(); + Log.Info("Server.Disconnect connectionId:" + connectionId); + return true; + } + return false; + } + + // tick: processes up to 'limit' messages for each connection + // => limit parameter to avoid deadlocks / too long freezes if server or + // client is too slow to process network load + // => Mirror & DOTSNET need to have a process limit anyway. + // might as well do it here and make life easier. + // => returns amount of remaining messages to process, so the caller + // can call tick again as many times as needed (or up to a limit) + // + // Tick() may process multiple messages, but Mirror needs a way to stop + // processing immediately if a scene change messages arrives. Mirror + // can't process any other messages during a scene change. + // (could be useful for others too) + // => make sure to allocate the lambda only once in transports + public int Tick(int processLimit, Func checkEnabled = null) + { + // only if pipe was created yet (after start()) + if (receivePipe == null) + return 0; + + // process up to 'processLimit' messages for this connection + for (int i = 0; i < processLimit; ++i) + { + // check enabled in case a Mirror scene message arrived + if (checkEnabled != null && !checkEnabled()) + break; + + // peek first. allows us to process the first queued entry while + // still keeping the pooled byte[] alive by not removing anything. + if (receivePipe.TryPeek(out int connectionId, out EventType eventType, out ArraySegment message)) + { + switch (eventType) + { + case EventType.Connected: + OnConnected?.Invoke(connectionId); + break; + case EventType.Data: + OnData?.Invoke(connectionId, message); + break; + case EventType.Disconnected: + OnDisconnected?.Invoke(connectionId); + // remove disconnected connection now that the final + // disconnected message was processed. + clients.TryRemove(connectionId, out ConnectionState _); + break; + } + + // IMPORTANT: now dequeue and return it to pool AFTER we are + // done processing the event. + receivePipe.TryDequeue(); + } + // no more messages. stop the loop. + else break; + } + + // return what's left to process for next time + return receivePipe.TotalCount; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs.meta new file mode 100644 index 0000000..9cee8b7 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb98a16841ccc4338a7e0b4e59136563 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef new file mode 100644 index 0000000..cd8d16a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef @@ -0,0 +1,12 @@ +{ + "name": "Telepathy", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta new file mode 100644 index 0000000..572c127 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 725ee7191c021de4dbf9269590ded755 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs new file mode 100644 index 0000000..6f026c9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs @@ -0,0 +1,244 @@ +// IMPORTANT +// force all thread functions to be STATIC. +// => Common.Send/ReceiveLoop is EXTREMELY DANGEROUS because it's too easy to +// accidentally share Common state between threads. +// => header buffer, payload etc. were accidentally shared once after changing +// the thread functions from static to non static +// => C# does not automatically detect data races. best we can do is move all of +// our thread code into static functions and pass all state into them +// +// let's even keep them in a STATIC CLASS so it's 100% obvious that this should +// NOT EVER be changed to non static! +using System; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public static class ThreadFunctions + { + // send message (via stream) with the message structure + // this function is blocking sometimes! + // (e.g. if someone has high latency or wire was cut off) + // -> payload is of multiple < parts + public static bool SendMessagesBlocking(NetworkStream stream, byte[] payload, int packetSize) + { + // stream.Write throws exceptions if client sends with high + // frequency and the server stops + try + { + // write the whole thing + stream.Write(payload, 0, packetSize); + return true; + } + catch (Exception exception) + { + // log as regular message because servers do shut down sometimes + Log.Info("Send: stream.Write exception: " + exception); + return false; + } + } + // read message (via stream) blocking. + // writes into byte[] and returns bytes written to avoid allocations. + public static bool ReadMessageBlocking(NetworkStream stream, int MaxMessageSize, byte[] headerBuffer, byte[] payloadBuffer, out int size) + { + size = 0; + + // buffer needs to be of Header + MaxMessageSize + if (payloadBuffer.Length != 4 + MaxMessageSize) + { + Log.Error($"ReadMessageBlocking: payloadBuffer needs to be of size 4 + MaxMessageSize = {4 + MaxMessageSize} instead of {payloadBuffer.Length}"); + return false; + } + + // read exactly 4 bytes for header (blocking) + if (!stream.ReadExactly(headerBuffer, 4)) + return false; + + // convert to int + size = Utils.BytesToIntBigEndian(headerBuffer); + + // protect against allocation attacks. an attacker might send + // multiple fake '2GB header' packets in a row, causing the server + // to allocate multiple 2GB byte arrays and run out of memory. + // + // also protect against size <= 0 which would cause issues + if (size > 0 && size <= MaxMessageSize) + { + // read exactly 'size' bytes for content (blocking) + return stream.ReadExactly(payloadBuffer, size); + } + Log.Warning("ReadMessageBlocking: possible header attack with a header of: " + size + " bytes."); + return false; + } + + // thread receive function is the same for client and server's clients + public static void ReceiveLoop(int connectionId, TcpClient client, int MaxMessageSize, MagnificentReceivePipe receivePipe, int QueueLimit) + { + // get NetworkStream from client + NetworkStream stream = client.GetStream(); + + // every receive loop needs it's own receive buffer of + // HeaderSize + MaxMessageSize + // to avoid runtime allocations. + // + // IMPORTANT: DO NOT make this a member, otherwise every connection + // on the server would use the same buffer simulatenously + byte[] receiveBuffer = new byte[4 + MaxMessageSize]; + + // avoid header[4] allocations + // + // IMPORTANT: DO NOT make this a member, otherwise every connection + // on the server would use the same buffer simulatenously + byte[] headerBuffer = new byte[4]; + + // absolutely must wrap with try/catch, otherwise thread exceptions + // are silent + try + { + // add connected event to pipe + receivePipe.Enqueue(connectionId, EventType.Connected, default); + + // let's talk about reading data. + // -> normally we would read as much as possible and then + // extract as many , messages + // as we received this time. this is really complicated + // and expensive to do though + // -> instead we use a trick: + // Read(2) -> size + // Read(size) -> content + // repeat + // Read is blocking, but it doesn't matter since the + // best thing to do until the full message arrives, + // is to wait. + // => this is the most elegant AND fast solution. + // + no resizing + // + no extra allocations, just one for the content + // + no crazy extraction logic + while (true) + { + // read the next message (blocking) or stop if stream closed + if (!ReadMessageBlocking(stream, MaxMessageSize, headerBuffer, receiveBuffer, out int size)) + // break instead of return so stream close still happens! + break; + + // create arraysegment for the read message + ArraySegment message = new ArraySegment(receiveBuffer, 0, size); + + // send to main thread via pipe + // -> it'll copy the message internally so we can reuse the + // receive buffer for next read! + receivePipe.Enqueue(connectionId, EventType.Data, message); + + // disconnect if receive pipe gets too big for this connectionId. + // -> avoids ever growing queue memory if network is slower + // than input + // -> disconnecting is great for load balancing. better to + // disconnect one connection than risking every + // connection / the whole server + if (receivePipe.Count(connectionId) >= QueueLimit) + { + // log the reason + Log.Warning($"receivePipe reached limit of {QueueLimit} for connectionId {connectionId}. This can happen if network messages come in way faster than we manage to process them. Disconnecting this connection for load balancing."); + + // IMPORTANT: do NOT clear the whole queue. we use one + // queue for all connections. + //receivePipe.Clear(); + + // just break. the finally{} will close everything. + break; + } + } + } + catch (Exception exception) + { + // something went wrong. the thread was interrupted or the + // connection closed or we closed our own connection or ... + // -> either way we should stop gracefully + Log.Info("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception); + } + finally + { + // clean up no matter what + stream.Close(); + client.Close(); + + // add 'Disconnected' message after disconnecting properly. + // -> always AFTER closing the streams to avoid a race condition + // where Disconnected -> Reconnect wouldn't work because + // Connected is still true for a short moment before the stream + // would be closed. + receivePipe.Enqueue(connectionId, EventType.Disconnected, default); + } + } + // thread send function + // note: we really do need one per connection, so that if one connection + // blocks, the rest will still continue to get sends + public static void SendLoop(int connectionId, TcpClient client, MagnificentSendPipe sendPipe, ManualResetEvent sendPending) + { + // get NetworkStream from client + NetworkStream stream = client.GetStream(); + + // avoid payload[packetSize] allocations. size increases dynamically as + // needed for batching. + // + // IMPORTANT: DO NOT make this a member, otherwise every connection + // on the server would use the same buffer simulatenously + byte[] payload = null; + + try + { + while (client.Connected) // try this. client will get closed eventually. + { + // reset ManualResetEvent before we do anything else. this + // way there is no race condition. if Send() is called again + // while in here then it will be properly detected next time + // -> otherwise Send might be called right after dequeue but + // before .Reset, which would completely ignore it until + // the next Send call. + sendPending.Reset(); // WaitOne() blocks until .Set() again + + // dequeue & serialize all + // a locked{} TryDequeueAll is twice as fast as + // ConcurrentQueue, see SafeQueue.cs! + if (sendPipe.DequeueAndSerializeAll(ref payload, out int packetSize)) + { + // send messages (blocking) or stop if stream is closed + if (!SendMessagesBlocking(stream, payload, packetSize)) + // break instead of return so stream close still happens! + break; + } + + // don't choke up the CPU: wait until queue not empty anymore + sendPending.WaitOne(); + } + } + catch (ThreadAbortException) + { + // happens on stop. don't log anything. + } + catch (ThreadInterruptedException) + { + // happens if receive thread interrupts send thread. + } + catch (Exception exception) + { + // something went wrong. the thread was interrupted or the + // connection closed or we closed our own connection or ... + // -> either way we should stop gracefully + Log.Info("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); + } + finally + { + // clean up no matter what + // we might get SocketExceptions when sending if the 'host has + // failed to respond' - in which case we should close the connection + // which causes the ReceiveLoop to end and fire the Disconnected + // message. otherwise the connection would stay alive forever even + // though we can't send anymore. + stream.Close(); + client.Close(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta new file mode 100644 index 0000000..ea536ac --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d01598bf851164dc48a24c26913460b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs new file mode 100644 index 0000000..8f04fe9 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs @@ -0,0 +1,23 @@ +namespace Telepathy +{ + public static class Utils + { + // IntToBytes version that doesn't allocate a new byte[4] each time. + // -> important for MMO scale networking performance. + public static void IntToBytesBigEndianNonAlloc(int value, byte[] bytes, int offset = 0) + { + bytes[offset + 0] = (byte)(value >> 24); + bytes[offset + 1] = (byte)(value >> 16); + bytes[offset + 2] = (byte)(value >> 8); + bytes[offset + 3] = (byte)value; + } + + public static int BytesToIntBigEndian(byte[] bytes) + { + return (bytes[0] << 24) | + (bytes[1] << 16) | + (bytes[2] << 8) | + bytes[3]; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs.meta new file mode 100644 index 0000000..0a9253b --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 951d08c05297f4b3e8feb5bfcab86531 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION new file mode 100644 index 0000000..9ec0736 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION @@ -0,0 +1,62 @@ +V1.8 [2021-06-02] +- fix: Do not set timeouts on listener (fixes https://github.com/vis2k/Mirror/issues/2695) +- fix: #104 - ReadSafely now catches ObjectDisposedException too + +V1.7 [2021-02-20] +- ReceiveTimeout: disabled by default for cases where people use Telepathy by + itself without pings etc. + +V1.6 [2021-02-10] +- configurable ReceiveTimeout to avoid TCPs high default timeout +- Server/Client receive queue limit now disconnects instead of showing a + warning. this is necessary for load balancing to avoid situations where one + spamming connection might fill the queue and slow down everyone else. + +V1.5 [2021-02-05] +- fix: client data races & flaky tests fixed by creating a new client state + object every time we connect. fixes data race where an old dieing thread + might still try to modify the current state +- fix: Client.ReceiveThreadFunction catches and ignores ObjectDisposedException + which can happen if Disconnect() closes and disposes the client, while the + ReceiveThread just starts up and still uses the client. +- Server/Client Tick() optional enabled check for Mirror scene changing + +V1.4 [2021-02-03] +- Server/Client.Tick: limit parameter added to process up to 'limit' messages. + makes Mirror & DOTSNET transports easier to implement +- stability: Server/Client send queue limit disconnects instead of showing a + warning. allows for load balancing. better to kick one connection and keep + the server running than slowing everything down for everyone. + +V1.3 [2021-02-02] +- perf: ReceivePipe: byte[] pool for allocation free receives (╯°□°)╯︵ ┻━┻ +- fix: header buffer, payload buffer data races because they were made non + static earlier. server threads would all access the same ones. + => all threaded code was moved into a static ThreadFunctions class to make it + 100% obvious that there should be no shared state in the future + +V1.2 [2021-02-02] +- Client/Server Tick & OnConnected/OnData/OnDisconnected events instead of + having the outside process messages via GetNextMessage. That's easier for + Mirror/DOTSNET and allows for allocation free data message processing later. +- MagnificientSend/RecvPipe to shield Telepathy from all the complexity +- perf: SendPipe: byte[] pool for allocation free sends (╯°□°)╯︵ ┻━┻ + +V1.1 [2021-02-01] +- stability: added more tests +- breaking: Server/Client.Send: ArraySegment parameter and copy internally so + that Transports don't need to worry about it +- perf: Buffer.BlockCopy instead of Array.Copy +- perf: SendMessageBlocking puts message header directly into payload now +- perf: receiveQueues use SafeQueue instead of ConcurrentQueue to avoid + allocations +- Common: removed static state +- perf: SafeQueue.TryDequeueAll: avoid queue.ToArray() allocations. copy into a + list instead. +- Logger.Log/LogWarning/LogError renamed to Log.Info/Warning/Error +- MaxMessageSize is now specified in constructor to prepare for pooling +- flaky tests are ignored for now +- smaller improvements + +V1.0 +- first stable release \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta new file mode 100644 index 0000000..04c1c8a --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d942af06608be434dbeeaa58207d20bd +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs b/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs new file mode 100644 index 0000000..5e0bc05 --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs @@ -0,0 +1,268 @@ +// wraps Telepathy for use as HLAPI TransportLayer +using System; +using System.Net; +using System.Net.Sockets; +using UnityEngine; + +// Replaced by Kcp November 2020 +namespace Mirror +{ + [HelpURL("https://github.com/vis2k/Telepathy/blob/master/README.md")] + [DisallowMultipleComponent] + public class TelepathyTransport : Transport + { + // scheme used by this transport + // "tcp4" means tcp with 4 bytes header, network byte order + public const string Scheme = "tcp4"; + + public ushort port = 7777; + + [Header("Common")] + [Tooltip("Nagle Algorithm can be disabled by enabling NoDelay")] + public bool NoDelay = true; + + [Tooltip("Send timeout in milliseconds.")] + public int SendTimeout = 5000; + + [Tooltip("Receive timeout in milliseconds. High by default so users don't time out during scene changes.")] + public int ReceiveTimeout = 30000; + + [Header("Server")] + [Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")] + public int serverMaxMessageSize = 16 * 1024; + + [Tooltip("Server processes a limit amount of messages per tick to avoid a deadlock where it might end up processing forever if messages come in faster than we can process them.")] + public int serverMaxReceivesPerTick = 10000; + + [Tooltip("Server send queue limit per connection for pending messages. Telepathy will disconnect a connection's queues reach that limit for load balancing. Better to kick one slow client than slowing down the whole server.")] + public int serverSendQueueLimitPerConnection = 10000; + + [Tooltip("Server receive queue limit per connection for pending messages. Telepathy will disconnect a connection's queues reach that limit for load balancing. Better to kick one slow client than slowing down the whole server.")] + public int serverReceiveQueueLimitPerConnection = 10000; + + [Header("Client")] + [Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker host might send multiple fake packets with 2GB headers, causing the connected clients to run out of memory after allocating multiple large packets.")] + public int clientMaxMessageSize = 16 * 1024; + + [Tooltip("Client processes a limit amount of messages per tick to avoid a deadlock where it might end up processing forever if messages come in faster than we can process them.")] + public int clientMaxReceivesPerTick = 1000; + + [Tooltip("Client send queue limit for pending messages. Telepathy will disconnect if the connection's queues reach that limit in order to avoid ever growing latencies.")] + public int clientSendQueueLimit = 10000; + + [Tooltip("Client receive queue limit for pending messages. Telepathy will disconnect if the connection's queues reach that limit in order to avoid ever growing latencies.")] + public int clientReceiveQueueLimit = 10000; + + Telepathy.Client client; + Telepathy.Server server; + + // scene change message needs to halt message processing immediately + // Telepathy.Tick() has a enabledCheck parameter that we can use, but + // let's only allocate it once. + Func enabledCheck; + + void Awake() + { + // tell Telepathy to use Unity's Debug.Log + Telepathy.Log.Info = Debug.Log; + Telepathy.Log.Warning = Debug.LogWarning; + Telepathy.Log.Error = Debug.LogError; + + // allocate enabled check only once + enabledCheck = () => enabled; + + Debug.Log("TelepathyTransport initialized!"); + } + + public override bool Available() + { + // C#'s built in TCP sockets run everywhere except on WebGL + return Application.platform != RuntimePlatform.WebGLPlayer; + } + + // client + private void CreateClient() + { + // create client + client = new Telepathy.Client(clientMaxMessageSize); + // client hooks + // other systems hook into transport events in OnCreate or + // OnStartRunning in no particular order. the only way to avoid + // race conditions where telepathy uses OnConnected before another + // system's hook (e.g. statistics OnData) was added is to wrap + // them all in a lambda and always call the latest hook. + // (= lazy call) + client.OnConnected = () => OnClientConnected.Invoke(); + client.OnData = (segment) => OnClientDataReceived.Invoke(segment, Channels.Reliable); + client.OnDisconnected = () => OnClientDisconnected.Invoke(); + + // client configuration + client.NoDelay = NoDelay; + client.SendTimeout = SendTimeout; + client.ReceiveTimeout = ReceiveTimeout; + client.SendQueueLimit = clientSendQueueLimit; + client.ReceiveQueueLimit = clientReceiveQueueLimit; + } + public override bool ClientConnected() => client != null && client.Connected; + public override void ClientConnect(string address) + { + CreateClient(); + client.Connect(address, port); + } + + public override void ClientConnect(Uri uri) + { + CreateClient(); + 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, serverPort); + } + public override void ClientSend(ArraySegment segment, int channelId) + { + client?.Send(segment); + + // call event. might be null if no statistics are listening etc. + OnClientDataSent?.Invoke(segment, Channels.Reliable); + } + public override void ClientDisconnect() + { + client?.Disconnect(); + client = null; + } + + // messages should always be processed in early update + public override void ClientEarlyUpdate() + { + // note: we need to check enabled in case we set it to false + // when LateUpdate already started. + // (https://github.com/vis2k/Mirror/pull/379) + if (!enabled) return; + + // process a maximum amount of client messages per tick + // IMPORTANT: check .enabled to stop processing immediately after a + // scene change message arrives! + client?.Tick(clientMaxReceivesPerTick, enabledCheck); + } + + // 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 != null && server.Active; + public override void ServerStart() + { + // create server + server = new Telepathy.Server(serverMaxMessageSize); + + // server hooks + // other systems hook into transport events in OnCreate or + // OnStartRunning in no particular order. the only way to avoid + // race conditions where telepathy uses OnConnected before another + // system's hook (e.g. statistics OnData) was added is to wrap + // them all in a lambda and always call the latest hook. + // (= lazy call) + server.OnConnected = (connectionId) => OnServerConnected.Invoke(connectionId); + server.OnData = (connectionId, segment) => OnServerDataReceived.Invoke(connectionId, segment, Channels.Reliable); + server.OnDisconnected = (connectionId) => OnServerDisconnected.Invoke(connectionId); + + // server configuration + server.NoDelay = NoDelay; + server.SendTimeout = SendTimeout; + server.ReceiveTimeout = ReceiveTimeout; + server.SendQueueLimit = serverSendQueueLimitPerConnection; + server.ReceiveQueueLimit = serverReceiveQueueLimitPerConnection; + + server.Start(port); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + server?.Send(connectionId, segment); + + // call event. might be null if no statistics are listening etc. + OnServerDataSent?.Invoke(connectionId, segment, Channels.Reliable); + } + public override void ServerDisconnect(int connectionId) => server?.Disconnect(connectionId); + public override string ServerGetClientAddress(int connectionId) + { + try + { + return server?.GetClientAddress(connectionId); + } + catch (SocketException) + { + // using server.listener.LocalEndpoint causes an Exception + // in UWP + Unity 2019: + // Exception thrown at 0x00007FF9755DA388 in UWF.exe: + // Microsoft C++ exception: Il2CppExceptionWrapper at memory + // location 0x000000E15A0FCDD0. SocketException: An address + // incompatible with the requested protocol was used at + // System.Net.Sockets.Socket.get_LocalEndPoint () + // so let's at least catch it and recover + return "unknown"; + } + } + public override void ServerStop() + { + server?.Stop(); + server = null; + } + + // messages should always be processed in early update + public override void ServerEarlyUpdate() + { + // note: we need to check enabled in case we set it to false + // when LateUpdate already started. + // (https://github.com/vis2k/Mirror/pull/379) + if (!enabled) return; + + // process a maximum amount of server messages per tick + // IMPORTANT: check .enabled to stop processing immediately after a + // scene change message arrives! + server?.Tick(serverMaxReceivesPerTick, enabledCheck); + } + + // common + public override void Shutdown() + { + Debug.Log("TelepathyTransport Shutdown()"); + client?.Disconnect(); + client = null; + server?.Stop(); + server = null; + } + + public override int GetMaxPacketSize(int channelId) + { + return serverMaxMessageSize; + } + + public override string ToString() + { + if (server != null && server.Active && server.listener != null) + { + // printing server.listener.LocalEndpoint causes an Exception + // in UWP + Unity 2019: + // Exception thrown at 0x00007FF9755DA388 in UWF.exe: + // Microsoft C++ exception: Il2CppExceptionWrapper at memory + // location 0x000000E15A0FCDD0. SocketException: An address + // incompatible with the requested protocol was used at + // System.Net.Sockets.Socket.get_LocalEndPoint () + // so let's use the regular port instead. + return $"Telepathy Server port: {port}"; + } + else if (client != null && (client.Connecting || client.Connected)) + { + return $"Telepathy Client port: {port}"; + } + return "Telepathy (inactive/disconnected)"; + } + } +} diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta new file mode 100644 index 0000000..99cde3e --- /dev/null +++ b/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7424c1070fad4ba2a7a96b02fbeb4bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 1000 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Utils.cs b/Assets/Mirror/Runtime/Utils.cs new file mode 100644 index 0000000..d39ed98 --- /dev/null +++ b/Assets/Mirror/Runtime/Utils.cs @@ -0,0 +1,121 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using UnityEngine; + +namespace Mirror +{ + // Handles network messages on client and server + public delegate void NetworkMessageDelegate(NetworkConnection conn, NetworkReader reader, int channelId); + + // Handles requests to spawn objects on the client + public delegate GameObject SpawnDelegate(Vector3 position, Guid assetId); + + public delegate GameObject SpawnHandlerDelegate(SpawnMessage msg); + + // Handles requests to unspawn objects on the client + public delegate void UnSpawnDelegate(GameObject spawned); + + // channels are const ints instead of an enum so people can add their own + // channels (can't extend an enum otherwise). + // + // note that Mirror is slowly moving towards quake style networking which + // will only require reliable for handshake, and unreliable for the rest. + // so eventually we can change this to an Enum and transports shouldn't + // add custom channels anymore. + public static class Channels + { + public const int Reliable = 0; // ordered + public const int Unreliable = 1; // unordered + } + + public static class Utils + { + public static uint GetTrueRandomUInt() + { + // use Crypto RNG to avoid having time based duplicates + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + byte[] bytes = new byte[4]; + rng.GetBytes(bytes); + return BitConverter.ToUInt32(bytes, 0); + } + } + + public static bool IsPrefab(GameObject obj) + { +#if UNITY_EDITOR + return UnityEditor.PrefabUtility.IsPartOfPrefabAsset(obj); +#else + return false; +#endif + } + + public static bool IsSceneObjectWithPrefabParent(GameObject gameObject, out GameObject prefab) + { + prefab = null; + +#if UNITY_EDITOR + if (!UnityEditor.PrefabUtility.IsPartOfPrefabInstance(gameObject)) + { + return false; + } + prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(gameObject); +#endif + + if (prefab == null) + { + Debug.LogError($"Failed to find prefab parent for scene object [name:{gameObject.name}]"); + return false; + } + return true; + } + + // is a 2D point in screen? (from ummorpg) + // (if width = 1024, then indices from 0..1023 are valid (=1024 indices) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPointInScreen(Vector2 point) => + 0 <= point.x && point.x < Screen.width && + 0 <= point.y && point.y < Screen.height; + + // pretty print bytes as KB/MB/GB/etc. from DOTSNET + // 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"; + } + + // universal .spawned function + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NetworkIdentity GetSpawnedInServerOrClient(uint netId) + { + // server / host mode: use the one from server. + // host mode has access to all spawned. + if (NetworkServer.active) + { + NetworkServer.spawned.TryGetValue(netId, out NetworkIdentity entry); + return entry; + } + + // client + if (NetworkClient.active) + { + NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity entry); + return entry; + } + + return null; + } + } +} diff --git a/Assets/Mirror/Runtime/Utils.cs.meta b/Assets/Mirror/Runtime/Utils.cs.meta new file mode 100644 index 0000000..7cf1557 --- /dev/null +++ b/Assets/Mirror/Runtime/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b530ce39098b54374a29ad308c8e4554 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync.meta b/Assets/ParrelSync.meta new file mode 100644 index 0000000..ebeaf60 --- /dev/null +++ b/Assets/ParrelSync.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 83560f6fc7502164aba5de92a83d4f26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/Examples.meta b/Assets/ParrelSync/Examples.meta new file mode 100755 index 0000000..7cf0f2c --- /dev/null +++ b/Assets/ParrelSync/Examples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 654be33bd44a76a4c8c180d1da6ad066 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/Examples/CustomArgumentExample.cs b/Assets/ParrelSync/Examples/CustomArgumentExample.cs new file mode 100755 index 0000000..71657eb --- /dev/null +++ b/Assets/ParrelSync/Examples/CustomArgumentExample.cs @@ -0,0 +1,31 @@ +// This should be editor only +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace ParrelSync.Example +{ + public class CustomArgumentExample : MonoBehaviour + { + // Start is called before the first frame update + void Start() + { + // Is this editor instance running a clone project? + if (ClonesManager.IsClone()) + { + Debug.Log("This is a clone project."); + + //Argument can be set from the clones manager window. + string customArgument = ClonesManager.GetArgument(); + Debug.Log("The custom argument of this clone project is: " + customArgument); + // Do what ever you need with the argument string. + } + else + { + Debug.Log("This is the original project."); + } + } + } +} +#endif \ No newline at end of file diff --git a/Assets/ParrelSync/Examples/CustomArgumentExample.cs.meta b/Assets/ParrelSync/Examples/CustomArgumentExample.cs.meta new file mode 100755 index 0000000..2a17418 --- /dev/null +++ b/Assets/ParrelSync/Examples/CustomArgumentExample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 346d302ecc25a9a41b48b857ce51d873 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/LICENSE.md b/Assets/ParrelSync/LICENSE.md new file mode 100755 index 0000000..cc3f86a --- /dev/null +++ b/Assets/ParrelSync/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2018 Greg M +Copyright (c) 2020 Ian and Contributors + +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. diff --git a/Assets/ParrelSync/LICENSE.md.meta b/Assets/ParrelSync/LICENSE.md.meta new file mode 100755 index 0000000..44f8f2e --- /dev/null +++ b/Assets/ParrelSync/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f4b327eab8d866e4087e166da8cafc09 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync.meta b/Assets/ParrelSync/ParrelSync.meta new file mode 100755 index 0000000..5f99aaf --- /dev/null +++ b/Assets/ParrelSync/ParrelSync.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8f5fec620d3bc9546a41a5b67cb9f8b6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor.meta b/Assets/ParrelSync/ParrelSync/Editor.meta new file mode 100755 index 0000000..93c82e3 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a31ea7d0315594440839cdb0db6bc411 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock.meta b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock.meta new file mode 100755 index 0000000..23e4c49 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b14e706b1e7cb044b23837e8a70cad9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs new file mode 100755 index 0000000..19eb438 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs @@ -0,0 +1,22 @@ +using UnityEditor; +namespace ParrelSync +{ + [InitializeOnLoad] + public class EditorQuit + { + /// + /// Is editor being closed + /// + static public bool IsQuiting { get; private set; } + static void Quit() + { + IsQuiting = true; + } + + static EditorQuit() + { + IsQuiting = false; + EditorApplication.quitting += Quit; + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta new file mode 100755 index 0000000..399bd56 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/EditorQuit.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf2888ff90706904abc2d851c3e59e00 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs new file mode 100755 index 0000000..196ee13 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs @@ -0,0 +1,34 @@ +using UnityEditor; +using UnityEngine; +namespace ParrelSync +{ + /// + /// For preventing assets being modified from the clone instance. + /// + public class ParrelSyncAssetModificationProcessor : UnityEditor.AssetModificationProcessor + { + public static string[] OnWillSaveAssets(string[] paths) + { + if (ClonesManager.IsClone() && Preferences.AssetModPref.Value) + { + if (paths != null && paths.Length > 0 && !EditorQuit.IsQuiting) + { + EditorUtility.DisplayDialog( + ClonesManager.ProjectName + ": Asset modifications saving detected and blocked", + "Asset modifications saving are blocked in the clone instance. \n\n" + + "This is a clone of the original project. \n" + + "Making changes to asset files via the clone editor is not recommended. \n" + + "Please use the original editor window if you want to make changes to the project files.", + "ok" + ); + foreach (var path in paths) + { + Debug.Log("Attempting to save " + path + " are blocked."); + } + } + return new string[0] { }; + } + return paths; + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta new file mode 100755 index 0000000..0dafc3f --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/AssetModBlock/ParrelSyncAssetModificationProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 755e570bd21b39440a923056e60f1450 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs b/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs new file mode 100755 index 0000000..f6297ad --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs @@ -0,0 +1,668 @@ +using System.Collections.Generic; +using System.Diagnostics; +using UnityEngine; +using UnityEditor; +using System.Linq; +using System.IO; +using Debug = UnityEngine.Debug; + +namespace ParrelSync +{ + /// + /// Contains all required methods for creating a linked clone of the Unity project. + /// + public class ClonesManager + { + /// + /// Name used for an identifying file created in the clone project directory. + /// + /// + /// (!) Do not change this after the clone was created, because then connection will be lost. + /// + public const string CloneFileName = ".clone"; + + /// + /// Suffix added to the end of the project clone name when it is created. + /// + /// + /// (!) Do not change this after the clone was created, because then connection will be lost. + /// + public const string CloneNameSuffix = "_clone"; + + public const string ProjectName = "ParrelSync"; + + /// + /// The maximum number of clones + /// + public const int MaxCloneProjectCount = 10; + + /// + /// Name of the file for storing clone's argument. + /// + public const string ArgumentFileName = ".parrelsyncarg"; + + /// + /// Default argument of the new clone + /// + public const string DefaultArgument = "client"; + + #region Managing clones + + /// + /// Creates clone from the project currently open in Unity Editor. + /// + /// + public static Project CreateCloneFromCurrent() + { + if (IsClone()) + { + Debug.LogError("This project is already a clone. Cannot clone it."); + return null; + } + + string currentProjectPath = ClonesManager.GetCurrentProjectPath(); + return ClonesManager.CreateCloneFromPath(currentProjectPath); + } + + /// + /// Creates clone of the project located at the given path. + /// + /// + /// + public static Project CreateCloneFromPath(string sourceProjectPath) + { + Project sourceProject = new Project(sourceProjectPath); + + string cloneProjectPath = null; + + //Find available clone suffix id + for (int i = 0; i < MaxCloneProjectCount; i++) + { + string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; + string possibleCloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; + + if (!Directory.Exists(possibleCloneProjectPath)) + { + cloneProjectPath = possibleCloneProjectPath; + break; + } + } + + if (string.IsNullOrEmpty(cloneProjectPath)) + { + Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount); + return null; + } + + Project cloneProject = new Project(cloneProjectPath); + + Debug.Log("Start cloning project, original project: " + sourceProject + ", clone project: " + cloneProject); + + ClonesManager.CreateProjectFolder(cloneProject); + + //Copy Folders + Debug.Log("Library copy: " + cloneProject.libraryPath); + ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, cloneProject.libraryPath, + "Cloning Project Library '" + sourceProject.name + "'. "); + Debug.Log("Packages copy: " + cloneProject.libraryPath); + ClonesManager.CopyDirectoryWithProgressBar(sourceProject.packagesPath, cloneProject.packagesPath, + "Cloning Project Packages '" + sourceProject.name + "'. "); + + + //Link Folders + ClonesManager.LinkFolders(sourceProject.assetPath, cloneProject.assetPath); + ClonesManager.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath); + ClonesManager.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath); + ClonesManager.LinkFolders(sourceProject.localPackages, cloneProject.localPackages); + + ClonesManager.RegisterClone(cloneProject); + + return cloneProject; + } + + /// + /// Registers a clone by placing an identifying ".clone" file in its root directory. + /// + /// + private static void RegisterClone(Project cloneProject) + { + /// Add clone identifier file. + string identifierFile = Path.Combine(cloneProject.projectPath, ClonesManager.CloneFileName); + File.Create(identifierFile).Dispose(); + + //Add argument file with default argument + string argumentFilePath = Path.Combine(cloneProject.projectPath, ClonesManager.ArgumentFileName); + File.WriteAllText(argumentFilePath, DefaultArgument, System.Text.Encoding.UTF8); + + /// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case. + string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt"); + File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone. + } + + /// + /// Opens a project located at the given path (if one exists). + /// + /// + public static void OpenProject(string projectPath) + { + if (!Directory.Exists(projectPath)) + { + Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist."); + return; + } + + if (projectPath == ClonesManager.GetCurrentProjectPath()) + { + Debug.LogError("Cannot open the project - it is already open."); + return; + } + + //Validate (and update if needed) the "Packages" folder before opening clone project to ensure the clone project will have the + //same "compiling environment" as the original project + ValidateCopiedFoldersIntegrity.ValidateFolder(projectPath, GetOriginalProjectPath(), "Packages"); + + string fileName = GetApplicationPath(); + string args = "-projectPath \"" + projectPath + "\""; + Debug.Log("Opening project \"" + fileName + " " + args + "\""); + ClonesManager.StartHiddenConsoleProcess(fileName, args); + } + + private static string GetApplicationPath() + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + return EditorApplication.applicationPath; + case RuntimePlatform.OSXEditor: + return EditorApplication.applicationPath + "/Contents/MacOS/Unity"; + case RuntimePlatform.LinuxEditor: + return EditorApplication.applicationPath; + default: + throw new System.NotImplementedException("Platform has not supported yet ;("); + } + } + + /// + /// Is this project being opened by an Unity editor? + /// + /// + /// + public static bool IsCloneProjectRunning(string projectPath) + { + + //Determine whether it is opened in another instance by checking the UnityLockFile + string UnityLockFilePath = new string[] { projectPath, "Temp", "UnityLockfile" } + .Aggregate(Path.Combine); + + switch (Application.platform) + { + case (RuntimePlatform.WindowsEditor): + //Windows editor will lock "UnityLockfile" file when project is being opened. + //Sometime, for instance: windows editor crash, the "UnityLockfile" will not be deleted even the project + //isn't being opened, so a check to the "UnityLockfile" lock status may be necessary. + if (Preferences.AlsoCheckUnityLockFileStaPref.Value) + return File.Exists(UnityLockFilePath) && FileUtilities.IsFileLocked(UnityLockFilePath); + else + return File.Exists(UnityLockFilePath); + case (RuntimePlatform.OSXEditor): + //Mac editor won't lock "UnityLockfile" file when project is being opened + return File.Exists(UnityLockFilePath); + case (RuntimePlatform.LinuxEditor): + return File.Exists(UnityLockFilePath); + default: + throw new System.NotImplementedException("IsCloneProjectRunning: Unsupport Platfrom: " + Application.platform); + } + } + + /// + /// Deletes the clone of the currently open project, if such exists. + /// + public static void DeleteClone(string cloneProjectPath) + { + /// Clone won't be able to delete itself. + if (ClonesManager.IsClone()) return; + + ///Extra precautions. + if (cloneProjectPath == string.Empty) return; + if (cloneProjectPath == ClonesManager.GetOriginalProjectPath()) return; + + //Check what OS is + string identifierFile; + string args; + switch (Application.platform) + { + case (RuntimePlatform.WindowsEditor): + Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); + + //The argument file will be deleted first at the beginning of the project deletion process + //to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.) + //If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed. + identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); + File.Delete(identifierFile); + + args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath); + StartHiddenConsoleProcess("cmd.exe", args); + + break; + case (RuntimePlatform.OSXEditor): + Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); + + //The argument file will be deleted first at the beginning of the project deletion process + //to prevent any further reading and writing to it(There's a File.Exist() check at the (file)editor windows.) + //If there's any file in the directory being write/read during the deletion process, the directory can't be fully removed. + identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); + File.Delete(identifierFile); + + FileUtil.DeleteFileOrDirectory(cloneProjectPath); + + break; + case (RuntimePlatform.LinuxEditor): + Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\""); + identifierFile = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); + File.Delete(identifierFile); + + FileUtil.DeleteFileOrDirectory(cloneProjectPath); + + break; + default: + Debug.LogWarning("Not in a known editor. Where are you!?"); + break; + } + } + + #endregion + + #region Creating project folders + + /// + /// Creates an empty folder using data in the given Project object + /// + /// + public static void CreateProjectFolder(Project project) + { + string path = project.projectPath; + Debug.Log("Creating new empty folder at: " + path); + Directory.CreateDirectory(path); + } + + /// + /// Copies the full contents of the unity library. We want to do this to avoid the lengthy re-serialization of the whole project when it opens up the clone. + /// + /// + /// + [System.Obsolete] + public static void CopyLibraryFolder(Project sourceProject, Project destinationProject) + { + if (Directory.Exists(destinationProject.libraryPath)) + { + Debug.LogWarning("Library copy: destination path already exists! "); + return; + } + + Debug.Log("Library copy: " + destinationProject.libraryPath); + ClonesManager.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath, + "Cloning project '" + sourceProject.name + "'. "); + } + + #endregion + + #region Creating symlinks + + /// + /// Creates a symlink between destinationPath and sourcePath (Mac version). + /// + /// + /// + private static void CreateLinkMac(string sourcePath, string destinationPath) + { + sourcePath = sourcePath.Replace(" ", "\\ "); + destinationPath = destinationPath.Replace(" ", "\\ "); + var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); + + Debug.Log("Mac hard link " + command); + + ClonesManager.ExecuteBashCommand(command); + } + + /// + /// Creates a symlink between destinationPath and sourcePath (Linux version). + /// + /// + /// + private static void CreateLinkLinux(string sourcePath, string destinationPath) + { + sourcePath = sourcePath.Replace(" ", "\\ "); + destinationPath = destinationPath.Replace(" ", "\\ "); + var command = string.Format("ln -s {0} {1}", sourcePath, destinationPath); + + Debug.Log("Linux Symlink " + command); + + ClonesManager.ExecuteBashCommand(command); + } + + /// + /// Creates a symlink between destinationPath and sourcePath (Windows version). + /// + /// + /// + private static void CreateLinkWin(string sourcePath, string destinationPath) + { + string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath); + Debug.Log("Windows junction: " + cmd); + ClonesManager.StartHiddenConsoleProcess("cmd.exe", cmd); + } + + //TODO(?) avoid terminal calls and use proper api stuff. See below for windows! + ////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol + //[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + //private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode, + // System.IntPtr InBuffer, int nInBufferSize, + // System.IntPtr OutBuffer, int nOutBufferSize, + // out int pBytesReturned, System.IntPtr lpOverlapped); + + /// + /// Create a link / junction from the original project to it's clone. + /// + /// + /// + public static void LinkFolders(string sourcePath, string destinationPath) + { + if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true)) + { + switch (Application.platform) + { + case (RuntimePlatform.WindowsEditor): + CreateLinkWin(sourcePath, destinationPath); + break; + case (RuntimePlatform.OSXEditor): + CreateLinkMac(sourcePath, destinationPath); + break; + case (RuntimePlatform.LinuxEditor): + CreateLinkLinux(sourcePath, destinationPath); + break; + default: + Debug.LogWarning("Not in a known editor. Application.platform: " + Application.platform); + break; + } + } + else + { + Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath); + } + } + + #endregion + + #region Utility methods + + private static bool? isCloneFileExistCache = null; + + /// + /// Returns true if the project currently open in Unity Editor is a clone. + /// + /// + public static bool IsClone() + { + if (isCloneFileExistCache == null) + { + /// The project is a clone if its root directory contains an empty file named ".clone". + string cloneFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.CloneFileName); + isCloneFileExistCache = File.Exists(cloneFilePath); + } + + return (bool)isCloneFileExistCache; + } + + /// + /// Get the path to the current unityEditor project folder's info + /// + /// + public static string GetCurrentProjectPath() + { + return Application.dataPath.Replace("/Assets", ""); + } + + /// + /// Return a project object that describes all the paths we need to clone it. + /// + /// + public static Project GetCurrentProject() + { + string pathString = ClonesManager.GetCurrentProjectPath(); + return new Project(pathString); + } + + /// + /// Get the argument of this clone project. + /// If this is the original project, will return an empty string. + /// + /// + public static string GetArgument() + { + string argument = ""; + if (IsClone()) + { + string argumentFilePath = Path.Combine(GetCurrentProjectPath(), ClonesManager.ArgumentFileName); + if (File.Exists(argumentFilePath)) + { + argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8); + } + } + + return argument; + } + + /// + /// Returns the path to the original project. + /// If currently open project is the original, returns its own path. + /// If the original project folder cannot be found, retuns an empty string. + /// + /// + public static string GetOriginalProjectPath() + { + if (IsClone()) + { + /// If this is a clone... + /// Original project path can be deduced by removing the suffix from the clone's path. + string cloneProjectPath = ClonesManager.GetCurrentProject().projectPath; + + int index = cloneProjectPath.LastIndexOf(ClonesManager.CloneNameSuffix); + if (index > 0) + { + string originalProjectPath = cloneProjectPath.Substring(0, index); + if (Directory.Exists(originalProjectPath)) return originalProjectPath; + } + + return string.Empty; + } + else + { + /// If this is the original, we return its own path. + return ClonesManager.GetCurrentProjectPath(); + } + } + + /// + /// Returns all clone projects path. + /// + /// + public static List GetCloneProjectsPath() + { + List projectsPath = new List(); + for (int i = 0; i < MaxCloneProjectCount; i++) + { + string originalProjectPath = ClonesManager.GetCurrentProject().projectPath; + string cloneProjectPath = originalProjectPath + ClonesManager.CloneNameSuffix + "_" + i; + + if (Directory.Exists(cloneProjectPath)) + projectsPath.Add(cloneProjectPath); + } + + return projectsPath; + } + + /// + /// Copies directory located at sourcePath to destinationPath. Displays a progress bar. + /// + /// Directory to be copied. + /// Destination directory (created automatically if needed). + /// Optional string added to the beginning of the progress bar window header. + public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath, + string progressBarPrefix = "") + { + var source = new DirectoryInfo(sourcePath); + var destination = new DirectoryInfo(destinationPath); + + long totalBytes = 0; + long copiedBytes = 0; + + ClonesManager.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes, + progressBarPrefix); + EditorUtility.ClearProgressBar(); + } + + /// + /// Copies directory located at sourcePath to destinationPath. Displays a progress bar. + /// Same as the previous method, but uses recursion to copy all nested folders as well. + /// + /// Directory to be copied. + /// Destination directory (created automatically if needed). + /// Total bytes to be copied. Calculated automatically, initialize at 0. + /// To track already copied bytes. Calculated automatically, initialize at 0. + /// Optional string added to the beginning of the progress bar window header. + private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination, + ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "") + { + /// Directory cannot be copied into itself. + if (source.FullName.ToLower() == destination.FullName.ToLower()) + { + Debug.LogError("Cannot copy directory into itself."); + return; + } + + /// Calculate total bytes, if required. + if (totalBytes == 0) + { + totalBytes = ClonesManager.GetDirectorySize(source, true, progressBarPrefix); + } + + /// Create destination directory, if required. + if (!Directory.Exists(destination.FullName)) + { + Directory.CreateDirectory(destination.FullName); + } + + /// Copy all files from the source. + foreach (FileInfo file in source.GetFiles()) + { + try + { + file.CopyTo(Path.Combine(destination.ToString(), file.Name), true); + } + catch (IOException) + { + /// Some files may throw IOException if they are currently open in Unity editor. + /// Just ignore them in such case. + } + + /// Account the copied file size. + copiedBytes += file.Length; + + /// Display the progress bar. + float progress = (float)copiedBytes / (float)totalBytes; + bool cancelCopy = EditorUtility.DisplayCancelableProgressBar( + progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...", + "(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...", + progress); + if (cancelCopy) return; + } + + /// Copy all nested directories from the source. + foreach (DirectoryInfo sourceNestedDir in source.GetDirectories()) + { + DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name); + ClonesManager.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir, + ref totalBytes, ref copiedBytes, progressBarPrefix); + } + } + + /// + /// Calculates the size of the given directory. Displays a progress bar. + /// + /// Directory, which size has to be calculated. + /// If true, size will include all nested directories. + /// Optional string added to the beginning of the progress bar window header. + /// Size of the directory in bytes. + private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false, + string progressBarPrefix = "") + { + EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...", + "Scanning '" + directory.FullName + "'...", 0f); + + /// Calculate size of all files in directory. + long filesSize = directory.GetFiles().Sum((FileInfo file) => file.Length); + + /// Calculate size of all nested directories. + long directoriesSize = 0; + if (includeNested) + { + IEnumerable nestedDirectories = directory.GetDirectories(); + foreach (DirectoryInfo nestedDir in nestedDirectories) + { + directoriesSize += ClonesManager.GetDirectorySize(nestedDir, true, progressBarPrefix); + } + } + + return filesSize + directoriesSize; + } + + /// + /// Starts process in the system console, taking the given fileName and args. + /// + /// + /// + private static void StartHiddenConsoleProcess(string fileName, string args) + { + System.Diagnostics.Process.Start(fileName, args); + } + + /// + /// Thanks to https://github.com/karl-/unity-symlink-utility/blob/master/SymlinkUtility.cs + /// + /// + private static void ExecuteBashCommand(string command) + { + command = command.Replace("\"", "\"\""); + + var proc = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = "-c \"" + command + "\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + using (proc) + { + proc.Start(); + proc.WaitForExit(); + + if (!proc.StandardError.EndOfStream) + { + UnityEngine.Debug.LogError(proc.StandardError.ReadToEnd()); + } + } + } + + public static void OpenProjectInFileExplorer(string path) + { + System.Diagnostics.Process.Start(@path); + } + #endregion + } +} diff --git a/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs.meta new file mode 100755 index 0000000..8eaf7a4 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ClonesManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6148e48ed6b61d748b187d06d3687b83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs b/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs new file mode 100755 index 0000000..f922140 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs @@ -0,0 +1,198 @@ +using UnityEngine; +using UnityEditor; +using System.IO; + +namespace ParrelSync +{ + /// + ///Clones manager Unity editor window + /// + public class ClonesManagerWindow : EditorWindow + { + /// + /// Returns true if project clone exists. + /// + public bool isCloneCreated + { + get { return ClonesManager.GetCloneProjectsPath().Count >= 1; } + } + + [MenuItem("ParrelSync/Clones Manager", priority = 0)] + private static void InitWindow() + { + ClonesManagerWindow window = (ClonesManagerWindow)EditorWindow.GetWindow(typeof(ClonesManagerWindow)); + window.titleContent = new GUIContent("Clones Manager"); + window.Show(); + } + + /// + /// For storing the scroll position of clones list + /// + Vector2 clonesScrollPos; + + private void OnGUI() + { + /// If it is a clone project... + if (ClonesManager.IsClone()) + { + //Find out the original project name and show the help box + string originalProjectPath = ClonesManager.GetOriginalProjectPath(); + if (originalProjectPath == string.Empty) + { + /// If original project cannot be found, display warning message. + EditorGUILayout.HelpBox( + "This project is a clone, but the link to the original seems lost.\nYou have to manually open the original and create a new clone instead of this one.\n", + MessageType.Warning); + } + else + { + /// If original project is present, display some usage info. + EditorGUILayout.HelpBox( + "This project is a clone of the project '" + Path.GetFileName(originalProjectPath) + "'.\nIf you want to make changes the project files or manage clones, please open the original project through Unity Hub.", + MessageType.Info); + } + + //Clone project custom argument. + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Arguments", GUILayout.Width(70)); + if (GUILayout.Button("?", GUILayout.Width(20))) + { + Application.OpenURL(ExternalLinks.CustomArgumentHelpLink); + } + GUILayout.EndHorizontal(); + + string argumentFilePath = Path.Combine(ClonesManager.GetCurrentProjectPath(), ClonesManager.ArgumentFileName); + //Need to be careful with file reading / writing since it will effect the deletion of + // the clone project(The directory won't be fully deleted if there's still file inside being read or write). + //The argument file will be deleted first at the beginning of the project deletion process + //to prevent any further being read and write. + //Will need to take some extra cautious if want to change the design of how file editing is handled. + if (File.Exists(argumentFilePath)) + { + string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8); + string argumentTextAreaInput = EditorGUILayout.TextArea(argument, + GUILayout.Height(50), + GUILayout.MaxWidth(300) + ); + File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8); + } + else + { + EditorGUILayout.LabelField("No argument file found."); + } + } + else// If it is an original project... + { + if (isCloneCreated) + { + GUILayout.BeginVertical("HelpBox"); + GUILayout.Label("Clones of this Project"); + + //List all clones + clonesScrollPos = + EditorGUILayout.BeginScrollView(clonesScrollPos); + var cloneProjectsPath = ClonesManager.GetCloneProjectsPath(); + for (int i = 0; i < cloneProjectsPath.Count; i++) + { + + GUILayout.BeginVertical("GroupBox"); + string cloneProjectPath = cloneProjectsPath[i]; + + bool isOpenInAnotherInstance = ClonesManager.IsCloneProjectRunning(cloneProjectPath); + + if (isOpenInAnotherInstance == true) + EditorGUILayout.LabelField("Clone " + i + " (Running)", EditorStyles.boldLabel); + else + EditorGUILayout.LabelField("Clone " + i); + + + GUILayout.BeginHorizontal(); + EditorGUILayout.TextField("Clone project path", cloneProjectPath, EditorStyles.textField); + if (GUILayout.Button("View Folder", GUILayout.Width(80))) + { + ClonesManager.OpenProjectInFileExplorer(cloneProjectPath); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Arguments", GUILayout.Width(70)); + if (GUILayout.Button("?", GUILayout.Width(20))) + { + Application.OpenURL(ExternalLinks.CustomArgumentHelpLink); + } + GUILayout.EndHorizontal(); + + string argumentFilePath = Path.Combine(cloneProjectPath, ClonesManager.ArgumentFileName); + //Need to be careful with file reading/writing since it will effect the deletion of + //the clone project(The directory won't be fully deleted if there's still file inside being read or write). + //The argument file will be deleted first at the beginning of the project deletion process + //to prevent any further being read and write. + //Will need to take some extra cautious if want to change the design of how file editing is handled. + if (File.Exists(argumentFilePath)) + { + string argument = File.ReadAllText(argumentFilePath, System.Text.Encoding.UTF8); + string argumentTextAreaInput = EditorGUILayout.TextArea(argument, + GUILayout.Height(50), + GUILayout.MaxWidth(300) + ); + File.WriteAllText(argumentFilePath, argumentTextAreaInput, System.Text.Encoding.UTF8); + } + else + { + EditorGUILayout.LabelField("No argument file found."); + } + + EditorGUILayout.Space(); + EditorGUILayout.Space(); + EditorGUILayout.Space(); + + + EditorGUI.BeginDisabledGroup(isOpenInAnotherInstance); + + if (GUILayout.Button("Open in New Editor")) + { + ClonesManager.OpenProject(cloneProjectPath); + } + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Delete")) + { + bool delete = EditorUtility.DisplayDialog( + "Delete the clone?", + "Are you sure you want to delete the clone project '" + ClonesManager.GetCurrentProject().name + "_clone'?", + "Delete", + "Cancel"); + if (delete) + { + ClonesManager.DeleteClone(cloneProjectPath); + } + } + + GUILayout.EndHorizontal(); + EditorGUI.EndDisabledGroup(); + GUILayout.EndVertical(); + + } + EditorGUILayout.EndScrollView(); + + if (GUILayout.Button("Add new clone")) + { + ClonesManager.CreateCloneFromCurrent(); + } + + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + } + else + { + /// If no clone created yet, we must create it. + EditorGUILayout.HelpBox("No project clones found. Create a new one!", MessageType.Info); + if (GUILayout.Button("Create new clone")) + { + ClonesManager.CreateCloneFromCurrent(); + } + } + } + } + } +} diff --git a/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs.meta new file mode 100755 index 0000000..4c62a16 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ClonesManagerWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a041d83486c20b84bbf5077ddfbbca37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs b/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs new file mode 100755 index 0000000..d4d80e6 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs @@ -0,0 +1,13 @@ +namespace ParrelSync +{ + public class ExternalLinks + { + public const string RemoteVersionURL = "https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/VERSION.txt"; + public const string Releases = "https://github.com/VeriorPies/ParrelSync/releases"; + public const string CustomArgumentHelpLink = "https://github.com/VeriorPies/ParrelSync/wiki/Argument"; + + public const string GitHubHome = "https://github.com/VeriorPies/ParrelSync/"; + public const string GitHubIssue = "https://github.com/VeriorPies/ParrelSync/issues"; + public const string FAQ = "https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs"; + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs.meta new file mode 100755 index 0000000..68bfde1 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ExternalLinks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65daf17fbe5101b41977305639f30c65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs b/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs new file mode 100755 index 0000000..d2a4271 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs @@ -0,0 +1,31 @@ +using System.IO; +using UnityEngine; + +namespace ParrelSync +{ + public class FileUtilities : MonoBehaviour + { + public static bool IsFileLocked(string path) + { + FileInfo file = new FileInfo(path); + try + { + using (FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None)) + { + stream.Close(); + } + } + catch (IOException) + { + //the file is unavailable because it is: + //still being written to + //or being processed by another thread + //or does not exist (has already been processed) + return true; + } + + //file is not locked + return false; + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs.meta new file mode 100755 index 0000000..9e384ad --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/FileUtilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11fdc6f78f8c965499a870ca06dca6bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/NonCore.meta b/Assets/ParrelSync/ParrelSync/Editor/NonCore.meta new file mode 100755 index 0000000..d39a37e --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/NonCore.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 74a7aa389726f964ab34c52e208c2a43 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs b/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs new file mode 100755 index 0000000..888800b --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs @@ -0,0 +1,78 @@ +namespace ParrelSync.NonCore +{ + using UnityEditor; + using UnityEngine; + + /// + /// A simple script to display feedback/star dialog after certain time of project being opened/re-compiled. + /// Will only pop-up once unless "Remind me next time" are chosen. + /// Removing this file from project wont effect any other functions. + /// + [InitializeOnLoad] + public class AskFeedbackDialog + { + const string InitializeOnLoadCountKey = "ParrelSync_InitOnLoadCount", StopShowingKey = "ParrelSync_StopShowFeedBack"; + static AskFeedbackDialog() + { + if (EditorPrefs.HasKey(StopShowingKey)) { return; } + + int InitializeOnLoadCount = EditorPrefs.GetInt(InitializeOnLoadCountKey, 0); + if (InitializeOnLoadCount > 20) + { + ShowDialog(); + } + else + { + EditorPrefs.SetInt(InitializeOnLoadCountKey, InitializeOnLoadCount + 1); + } + } + + //[MenuItem("ParrelSync/(Debug)Show AskFeedbackDialog ")] + private static void ShowDialog() + { + int option = EditorUtility.DisplayDialogComplex("Do you like " + ParrelSync.ClonesManager.ProjectName + "?", + "Do you like " + ParrelSync.ClonesManager.ProjectName + "?\n" + + "If so, please don't hesitate to star it on GitHub and contribute to the project!", + "Star on GitHub", + "Close", + "Remind me next time" + ); + + switch (option) + { + // First parameter. + case 0: + Debug.Log("AskFeedbackDialog: Star on GitHub selected"); + EditorPrefs.SetBool(StopShowingKey, true); + EditorPrefs.DeleteKey(InitializeOnLoadCountKey); + Application.OpenURL(ExternalLinks.GitHubHome); + break; + // Second parameter. + case 1: + Debug.Log("AskFeedbackDialog: Close and never show again."); + EditorPrefs.SetBool(StopShowingKey, true); + EditorPrefs.DeleteKey(InitializeOnLoadCountKey); + break; + // Third parameter. + case 2: + Debug.Log("AskFeedbackDialog: Remind me next time"); + EditorPrefs.SetInt(InitializeOnLoadCountKey, 0); + break; + default: + //Debug.Log("Close windows."); + break; + } + } + + ///// + ///// For debug purpose + ///// + //[MenuItem("ParrelSync/(Debug)Delete AskFeedbackDialog keys")] + //private static void DebugDeleteAllKeys() + //{ + // EditorPrefs.DeleteKey(InitializeOnLoadCountKey); + // EditorPrefs.DeleteKey(StopShowingKey); + // Debug.Log("AskFeedbackDialog keys deleted"); + //} + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta new file mode 100755 index 0000000..2bdd8f0 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/NonCore/AskFeedbackDialog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 894412a5b602e6c4ba2cf2d01f4f92b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs b/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs new file mode 100755 index 0000000..fabe8e0 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs @@ -0,0 +1,26 @@ +namespace ParrelSync.NonCore +{ + using UnityEditor; + using UnityEngine; + + public class OtherMenuItem + { + [MenuItem("ParrelSync/GitHub/View this project on GitHub", priority = 10)] + private static void OpenGitHub() + { + Application.OpenURL(ExternalLinks.GitHubHome); + } + + [MenuItem("ParrelSync/GitHub/View FAQ", priority = 11)] + private static void OpenFAQ() + { + Application.OpenURL(ExternalLinks.FAQ); + } + + [MenuItem("ParrelSync/GitHub/View Issues", priority = 12)] + private static void OpenGitHubIssues() + { + Application.OpenURL(ExternalLinks.GitHubIssue); + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta new file mode 100755 index 0000000..e139389 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/NonCore/OtherMenuItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7191fa4bfa12ae749b27f73ed292eaf1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs b/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs new file mode 100755 index 0000000..709aa4c --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs @@ -0,0 +1,110 @@ +using UnityEngine; +using UnityEditor; + +namespace ParrelSync +{ + /// + /// To add value caching for functions + /// + public class BoolPreference + { + public string key { get; private set; } + public bool defaultValue { get; private set; } + public BoolPreference(string key, bool defaultValue) + { + this.key = key; + this.defaultValue = defaultValue; + } + + private bool? valueCache = null; + + public bool Value + { + get + { + if (valueCache == null) + valueCache = EditorPrefs.GetBool(key, defaultValue); + + return (bool)valueCache; + } + set + { + if (valueCache == value) + return; + + EditorPrefs.SetBool(key, value); + valueCache = value; + Debug.Log("Editor preference updated. key: " + key + ", value: " + value); + } + } + + public void ClearValue() + { + EditorPrefs.DeleteKey(key); + valueCache = null; + } + } + + public class Preferences : EditorWindow + { + [MenuItem("ParrelSync/Preferences", priority = 1)] + private static void InitWindow() + { + Preferences window = (Preferences)EditorWindow.GetWindow(typeof(Preferences)); + window.titleContent = new GUIContent(ClonesManager.ProjectName + " Preferences"); + window.Show(); + } + + /// + /// Disable asset saving in clone editors? + /// + public static BoolPreference AssetModPref = new BoolPreference("ParrelSync_DisableClonesAssetSaving", true); + + /// + /// In addition of checking the existence of UnityLockFile, + /// also check is the is the UnityLockFile being opened. + /// + public static BoolPreference AlsoCheckUnityLockFileStaPref = new BoolPreference("ParrelSync_CheckUnityLockFileOpenStatus", true); + + private void OnGUI() + { + if (ClonesManager.IsClone()) + { + EditorGUILayout.HelpBox( + "This is a clone project. Please use the original project editor to change preferences.", + MessageType.Info); + return; + } + + GUILayout.BeginVertical("HelpBox"); + GUILayout.Label("Preferences"); + GUILayout.BeginVertical("GroupBox"); + + AssetModPref.Value = EditorGUILayout.ToggleLeft( + new GUIContent( + "(recommended) Disable asset saving in clone editors- require re-open clone editors", + "Disable asset saving in clone editors so all assets can only be modified from the original project editor" + ), + AssetModPref.Value); + + if (Application.platform == RuntimePlatform.WindowsEditor) + { + AlsoCheckUnityLockFileStaPref.Value = EditorGUILayout.ToggleLeft( + new GUIContent( + "Also check UnityLockFile lock status while checking clone projects running status", + "Disable this can slightly increase Clones Manager window performance, but will lead to in-correct clone project running status" + + "(the Clones Manager window show the clone project is still running even it's not) if the clone editor crashed" + ), + AlsoCheckUnityLockFileStaPref.Value); + } + GUILayout.EndVertical(); + if (GUILayout.Button("Reset to default")) + { + AssetModPref.ClearValue(); + AlsoCheckUnityLockFileStaPref.ClearValue(); + Debug.Log("Editor preferences cleared"); + } + GUILayout.EndVertical(); + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs.meta new file mode 100755 index 0000000..167af3e --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/Preferences.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24641be1c0410a745b529e61b508679f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/Project.cs b/Assets/ParrelSync/ParrelSync/Editor/Project.cs new file mode 100755 index 0000000..471a343 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/Project.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ParrelSync +{ + public class Project : System.ICloneable + { + public string name; + public string projectPath; + string rootPath; + public string assetPath; + public string projectSettingsPath; + public string libraryPath; + public string packagesPath; + public string autoBuildPath; + public string localPackages; + + char[] separator = new char[1] { '/' }; + + + /// + /// Default constructor + /// + public Project() + { + + } + + + /// + /// Initialize the project object by parsing its full path returned by Unity into a bunch of individual folder names and paths. + /// + /// + public Project(string path) + { + ParsePath(path); + } + + + /// + /// Create a new object with the same settings + /// + /// + public object Clone() + { + Project newProject = new Project(); + newProject.rootPath = rootPath; + newProject.projectPath = projectPath; + newProject.assetPath = assetPath; + newProject.projectSettingsPath = projectSettingsPath; + newProject.libraryPath = libraryPath; + newProject.name = name; + newProject.separator = separator; + newProject.packagesPath = packagesPath; + newProject.autoBuildPath = autoBuildPath; + newProject.localPackages = localPackages; + + + return newProject; + } + + + /// + /// Update the project object by renaming and reparsing it. Pass in the new name of a project, and it'll update the other member variables to match. + /// + /// + public void updateNewName(string newName) + { + name = newName; + ParsePath(rootPath + "/" + name + "/Assets"); + } + + + /// + /// Debug override so we can quickly print out the project info. + /// + /// + public override string ToString() + { + string printString = name + "\n" + + rootPath + "\n" + + projectPath + "\n" + + assetPath + "\n" + + projectSettingsPath + "\n" + + packagesPath + "\n" + + autoBuildPath + "\n" + + localPackages + "\n" + + libraryPath; + return (printString); + } + + private void ParsePath(string path) + { + //Unity's Application functions return the Assets path in the Editor. + projectPath = path; + + //pop off the last part of the path for the project name, keep the rest for the root path + List pathArray = projectPath.Split(separator).ToList(); + name = pathArray.Last(); + + pathArray.RemoveAt(pathArray.Count() - 1); + rootPath = string.Join(separator[0].ToString(), pathArray.ToArray()); + + assetPath = projectPath + "/Assets"; + projectSettingsPath = projectPath + "/ProjectSettings"; + libraryPath = projectPath + "/Library"; + packagesPath = projectPath + "/Packages"; + autoBuildPath = projectPath + "/AutoBuild"; + localPackages = projectPath + "/LocalPackages"; + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/Project.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/Project.cs.meta new file mode 100755 index 0000000..a3f2a55 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/Project.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec8d3a1577179ef44815739178cf75b4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs b/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs new file mode 100755 index 0000000..212cd94 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs @@ -0,0 +1,60 @@ +using System; +using UnityEditor; +using UnityEngine; +namespace ParrelSync.Update +{ + /// + /// A simple update checker + /// + public class UpdateChecker + { + //const string LocalVersionFilePath = "Assets/ParrelSync/VERSION.txt"; + public const string LocalVersion = "1.5.1"; + [MenuItem("ParrelSync/Check for update", priority = 20)] + static void CheckForUpdate() + { + using (System.Net.WebClient client = new System.Net.WebClient()) + { + try + { + //This won't work with UPM packages + //string localVersionText = AssetDatabase.LoadAssetAtPath(LocalVersionFilePath).text; + + string localVersionText = LocalVersion; + Debug.Log("Local version text : " + LocalVersion); + + string latesteVersionText = client.DownloadString(ExternalLinks.RemoteVersionURL); + Debug.Log("latest version text got: " + latesteVersionText); + string messageBody = "Current Version: " + localVersionText +"\n" + +"Latest Version: " + latesteVersionText + "\n"; + var latestVersion = new Version(latesteVersionText); + var localVersion = new Version(localVersionText); + + if (latestVersion > localVersion) + { + Debug.Log("There's a newer version"); + messageBody += "There's a newer version available"; + if(EditorUtility.DisplayDialog("Check for update.", messageBody, "Get latest release", "Close")) + { + Application.OpenURL(ExternalLinks.Releases); + } + } + else + { + Debug.Log("Current version is up-to-date."); + messageBody += "Current version is up-to-date."; + EditorUtility.DisplayDialog("Check for update.", messageBody,"OK"); + } + + } + catch (Exception exp) + { + Debug.LogError("Error with checking update. Exception: " + exp); + EditorUtility.DisplayDialog("Update Error","Error with checking update. \nSee console for more details.", + "OK" + ); + } + } + } + } +} diff --git a/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs.meta new file mode 100755 index 0000000..559bbc2 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/UpdateChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3453b3f1a20ea148b5028f8556a7be5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs b/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs new file mode 100755 index 0000000..0807f6d --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs @@ -0,0 +1,73 @@ +namespace ParrelSync +{ + using UnityEditor; + using UnityEngine; + using System; + using System.Text; + using System.Security.Cryptography; + using System.IO; + using System.Linq; + + [InitializeOnLoad] + public class ValidateCopiedFoldersIntegrity + { + const string SessionStateKey = "ValidateCopiedFoldersIntegrity_Init"; + /// + /// Called once on editor startup. + /// Validate copied folders integrity in clone project + /// + static ValidateCopiedFoldersIntegrity() + { + if (!SessionState.GetBool(SessionStateKey, false)) + { + SessionState.SetBool(SessionStateKey, true); + if (!ClonesManager.IsClone()) { return; } + + ValidateFolder(ClonesManager.GetCurrentProjectPath(), ClonesManager.GetOriginalProjectPath(), "Packages"); + } + } + + public static void ValidateFolder(string targetRoot, string originalRoot, string folderName) + { + var targetFolderPath = Path.Combine(targetRoot, folderName); + var targetFolderHash = CreateMd5ForFolder(targetFolderPath); + + var originalFolderPath = Path.Combine(originalRoot, folderName); + var originalFolderHash = CreateMd5ForFolder(originalFolderPath); + + if (targetFolderHash != originalFolderHash) + { + Debug.Log("ParrelSync: Detected changes in '" + folderName + "' directory. Updating cloned project..."); + FileUtil.ReplaceDirectory(originalFolderPath, targetFolderPath); + } + } + + static string CreateMd5ForFolder(string path) + { + // assuming you want to include nested folders + var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories) + .OrderBy(p => p).ToList(); + + MD5 md5 = MD5.Create(); + + for (int i = 0; i < files.Count; i++) + { + string file = files[i]; + + // hash path + string relativePath = file.Substring(path.Length + 1); + byte[] pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLower()); + md5.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); + + // hash contents + byte[] contentBytes = File.ReadAllBytes(file); + if (i == files.Count - 1) + md5.TransformFinalBlock(contentBytes, 0, contentBytes.Length); + else + md5.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0); + } + + return BitConverter.ToString(md5.Hash).Replace("-", "").ToLower(); + } + } +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta b/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta new file mode 100755 index 0000000..de94676 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/Editor/ValidateCopiedFoldersIntegrity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8fb344b9abf5274abd744833474b087 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/package.json b/Assets/ParrelSync/ParrelSync/package.json new file mode 100755 index 0000000..be7c342 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/package.json @@ -0,0 +1,10 @@ +{ + "name": "com.veriorpies.parrelsync", + "displayName": "ParrelSync", + "version": "1.5.1", + "unity": "2018.4", + "description": "ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project.", + "license": "MIT", + "keywords": [ "Networking", "Utils", "Editor", "Extensions" ], + "dependencies": {} +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/package.json.meta b/Assets/ParrelSync/ParrelSync/package.json.meta new file mode 100755 index 0000000..f035e38 --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a2a889c264e34b47a7349cbcb2cbedd7 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/ParrelSync/projectCloner.asmdef b/Assets/ParrelSync/ParrelSync/projectCloner.asmdef new file mode 100755 index 0000000..717443f --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/projectCloner.asmdef @@ -0,0 +1,15 @@ +{ + "name": "ParrelSync", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/ParrelSync/ParrelSync/projectCloner.asmdef.meta b/Assets/ParrelSync/ParrelSync/projectCloner.asmdef.meta new file mode 100755 index 0000000..327ec6a --- /dev/null +++ b/Assets/ParrelSync/ParrelSync/projectCloner.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 894a6cc6ed5cd2645bb542978cbed6a9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/README.md b/Assets/ParrelSync/README.md new file mode 100755 index 0000000..1f0333a --- /dev/null +++ b/Assets/ParrelSync/README.md @@ -0,0 +1,91 @@ +# ParrelSync +[![Release](https://img.shields.io/github/v/release/VeriorPies/ParrelSync?include_prereleases)](https://github.com/VeriorPies/ParrelSync/releases) [![Documentation](https://img.shields.io/badge/documentation-brightgreen.svg)](https://github.com/VeriorPies/ParrelSync/wiki) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/VeriorPies/ParrelSync/blob/master/LICENSE.md) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue.svg)](https://github.com/VeriorPies/ParrelSync/pulls) [![Chats](https://img.shields.io/discord/710688100996743200)](https://discord.gg/TmQk2qG) + +ParrelSync is a Unity editor extension that allows users to test multiplayer gameplay without building the project by having another Unity editor window opened and mirror the changes from the original project. + +
+ +![ShortGif](https://raw.githubusercontent.com/VeriorPies/ParrelSync/master/Images/Showcase%201.gif) +

+Test project changes on clients and server within seconds - both in editor + +
+

+ +## Features +1. Test multiplayer gameplay without building the project +2. GUI tools for managing all project clones +3. Protected assets from being modified by other clone instances +4. Handy APIs to speed up testing workflows +## Installation + +1. Backup your project folder or use a version control system such as [Git](https://git-scm.com/) or [SVN](https://subversion.apache.org/) +2. Download .unitypackage from the [latest release](https://github.com/VeriorPies/ParrelSync/releases) and import it to your project. +3. ParrelSync should appreared in the menu item bar after imported +![UpdateButtonInMenu](https://github.com/VeriorPies/ParrelSync/raw/master/Images/AfterImported.png) + +Check out the [Installation-and-Update](https://github.com/VeriorPies/ParrelSync/wiki/Installation-and-Update) page for more details. + +### UPM Package +ParrelSync can also be installed via UPM package. +After Unity 2019.3.4f1, Unity 2020.1a21, which support path query parameter of git package. You can install ParrelSync by adding the following to Package Manager. + +``` +https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync +``` + + +![UPM_Image](https://github.com/VeriorPies/ParrelSync/raw/master/Images/UPM_1.png?raw=true) ![UPM_Image2](https://github.com/VeriorPies/ParrelSync/raw/master/Images/UPM_2.png?raw=true) + +or by adding + +``` +"com.veriorpies.parrelsync": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync" +``` + +to the `Packages/manifest.json` file + + +## Supported Platform +Currently, ParrelSync supports Windows, macOS and Linux editors. + +ParrelSync has been tested with the following Unity version. However, it should also work with other versions as well. +* *2020.3.1f1* +* *2019.3.0f6* +* *2018.4.22f1* + + +## APIs +There's some useful APIs for speeding up the multiplayer testing workflow. +Here's a basic example: +``` +if (ClonesManager.IsClone()) { + // Automatically connect to local host if this is the clone editor +}else{ + // Automatically start server if this is the original editor +} +``` +Check out [the doc](https://github.com/VeriorPies/ParrelSync/wiki/List-of-APIs) to view the complete API list. + +## How does it work? +For each clone instance, ParrelSync will make a copy of the original project folder and reference the ```Asset```, ```Packages``` and ```ProjectSettings``` folder back to the original project with [symbolic link](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/mklink). Other folders such as ```Library```, ```Temp```, and ```obj``` will remain independent for each clone project. + +All clones are placed right next to the original project with suffix *```_clone_x```*, which will be something like this in the folder hierarchy. +``` +/ProjectName +/ProjectName_clone_0 +/ProjectName_clone_1 +... +``` +## Discord Server +We have a [Discord server](https://discord.gg/TmQk2qG). + +## Need Help? +Some common questions and troubleshooting can be found under the [Troubleshooting & FAQs](https://github.com/VeriorPies/ParrelSync/wiki/Troubleshooting-&-FAQs) page. +You can also [create a question post](https://github.com/VeriorPies/ParrelSync/issues/new/choose), or ask on [Discord](https://discord.gg/TmQk2qG) if you prefer to have a real-time conversation. + +## Support this project +A star will be appreciated :) + +## Credits +This project is originated from hwaet's [UnityProjectCloner](https://github.com/hwaet/UnityProjectCloner) diff --git a/Assets/ParrelSync/README.md.meta b/Assets/ParrelSync/README.md.meta new file mode 100755 index 0000000..3063ee6 --- /dev/null +++ b/Assets/ParrelSync/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fa3f2fa6aced9b54b970c8996957df3b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ParrelSync/VERSION.txt b/Assets/ParrelSync/VERSION.txt new file mode 100755 index 0000000..8e03717 --- /dev/null +++ b/Assets/ParrelSync/VERSION.txt @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/Assets/ParrelSync/VERSION.txt.meta b/Assets/ParrelSync/VERSION.txt.meta new file mode 100755 index 0000000..800cb1b --- /dev/null +++ b/Assets/ParrelSync/VERSION.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9833b240625d4e14995c296b87e34b96 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs.meta b/Assets/Prefabs.meta new file mode 100644 index 0000000..2cfc5e6 --- /dev/null +++ b/Assets/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bec5b603b442a8c4199f30c3906e87c6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab new file mode 100644 index 0000000..61e9209 --- /dev/null +++ b/Assets/Prefabs/Player.prefab @@ -0,0 +1,146 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1666928812108792425 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3019132547437604787} + - component: {fileID: 5477517734192154168} + - component: {fileID: 3781459040239876861} + - component: {fileID: 2750934347978954253} + - component: {fileID: 6803138963659689223} + - component: {fileID: 428742853628225930} + - component: {fileID: -4824969326312294342} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3019132547437604787 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 5.0368776, y: 3.5533748, z: -4.015329} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &5477517734192154168 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &3781459040239876861 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: be9e482d028e9234da250ae990729af7, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!65 &2750934347978954253 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!114 &6803138963659689223 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + serverOnly: 0 + m_AssetId: + hasSpawned: 0 +--- !u!114 &428742853628225930 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d43cf183cda48544ea9aa5bbfa7a4625, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMode: 0 + syncInterval: 0.1 +--- !u!114 &-4824969326312294342 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1666928812108792425} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMode: 0 + syncInterval: 0.1 + clientAuthority: 1 + localPositionSensitivity: 0.01 + localRotationSensitivity: 0.01 + localScaleSensitivity: 0.01 diff --git a/Assets/Prefabs/Player.prefab.meta b/Assets/Prefabs/Player.prefab.meta new file mode 100644 index 0000000..d1d960c --- /dev/null +++ b/Assets/Prefabs/Player.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b0b694a9a2f01754e8eee824eddc942c +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes.meta b/Assets/Scenes.meta new file mode 100644 index 0000000..00e437e --- /dev/null +++ b/Assets/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dd61dba60a160614886340099539a7bd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Main.unity b/Assets/Scenes/Main.unity new file mode 100644 index 0000000..1759e6f --- /dev/null +++ b/Assets/Scenes/Main.unity @@ -0,0 +1,457 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 705507994} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 1874141296} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &207291378 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 207291382} + - component: {fileID: 207291381} + - component: {fileID: 207291379} + - component: {fileID: 207291384} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &207291379 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 207291378} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6d4f9593ba7f845458730213887b5758, type: 3} + m_Name: + m_EditorClassIdentifier: + Port: 7777 + LoggerLevel: 3 + TimeoutMS: 1000 + useRelay: 0 +--- !u!114 &207291381 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 207291378} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f9810247aefdc844781df8cf4c039e9a, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 1 + PersistNetworkManagerToOfflineScene: 0 + runInBackground: 1 + autoStartServerBuild: 1 + serverTickRate: 30 + offlineScene: + onlineScene: + transport: {fileID: 207291379} + networkAddress: localhost + maxConnections: 4 + authenticator: {fileID: 0} + playerPrefab: {fileID: 1666928812108792425, guid: b0b694a9a2f01754e8eee824eddc942c, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 0 + spawnPrefabs: [] + relayJoinCode: + localPlayer: {fileID: 0} + isLoggedIn: 0 +--- !u!4 &207291382 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 207291378} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -1.9334593, y: 13.584466, z: -13.890325} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &207291384 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 207291378} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9f71e61a73431ed4db3d59daab75847e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + offsetX: 0 + offsetY: 0 +--- !u!1 &705507993 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 705507995} + - component: {fileID: 705507994} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &705507994 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 705507993} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 1 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &705507995 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 705507993} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &963194225 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 963194228} + - component: {fileID: 963194227} + - component: {fileID: 963194226} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &963194226 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + m_Enabled: 1 +--- !u!20 &963194227 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &963194228 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963194225} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -17.37} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!850595691 &1874141296 +LightingSettings: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Settings.lighting + serializedVersion: 3 + m_GIWorkflowMode: 1 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_RealtimeEnvironmentLighting: 1 + m_BounceScale: 1 + m_AlbedoBoost: 1 + m_IndirectOutputScale: 1 + m_UsingShadowmask: 1 + m_BakeBackend: 1 + m_LightmapMaxSize: 1024 + m_BakeResolution: 40 + m_Padding: 2 + m_TextureCompression: 1 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAO: 0 + m_MixedBakeMode: 2 + m_LightmapsBakeMode: 1 + m_FilterMode: 1 + m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0} + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_RealtimeResolution: 2 + m_ForceWhiteAlbedo: 0 + m_ForceUpdates: 0 + m_FinalGather: 0 + m_FinalGatherRayCount: 256 + m_FinalGatherFiltering: 1 + m_PVRCulling: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_LightProbeSampleCountMultiplier: 4 + m_PVRBounces: 2 + m_PVRMinBounces: 2 + m_PVREnvironmentMIS: 0 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 diff --git a/Assets/Scenes/Main.unity.meta b/Assets/Scenes/Main.unity.meta new file mode 100644 index 0000000..952bd1e --- /dev/null +++ b/Assets/Scenes/Main.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9fc0d4010bbf28b4594072e72b8655ab +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates.meta b/Assets/ScriptTemplates.meta new file mode 100644 index 0000000..cab2b9b --- /dev/null +++ b/Assets/ScriptTemplates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 44ffc995e27cf42399ce512dd0e86f9a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt new file mode 100644 index 0000000..7db3756 --- /dev/null +++ b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt @@ -0,0 +1,249 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html +*/ + +public class #SCRIPTNAME# : NetworkManager +{ + // Overrides the base singleton so we don't + // have to cast to this type everywhere. + public static new #SCRIPTNAME# singleton { get; private set; } + + #region Unity Callbacks + + public override void OnValidate() + { + base.OnValidate(); + } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Start() + { + singleton = this; + base.Start(); + } + + /// + /// Runs on both Server and Client + /// + public override void LateUpdate() + { + base.LateUpdate(); + } + + /// + /// Runs on both Server and Client + /// + public override void OnDestroy() + { + base.OnDestroy(); + } + + #endregion + + #region Start & Stop + + /// + /// Set the frame rate for a headless server. + /// Override if you wish to disable the behavior or set your own tick rate. + /// + public override void ConfigureHeadlessFrameRate() + { + base.ConfigureHeadlessFrameRate(); + } + + /// + /// called when quitting the application by closing the window / pressing stop in the editor + /// + public override void OnApplicationQuit() + { + base.OnApplicationQuit(); + } + + #endregion + + #region Scene Management + + /// + /// This causes the server to switch scenes and sets the networkSceneName. + /// Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene. + /// + /// + public override void ServerChangeScene(string newSceneName) + { + base.ServerChangeScene(newSceneName); + } + + /// + /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed + /// This allows server to do work / cleanup / prep before the scene changes. + /// + /// Name of the scene that's about to be loaded + public override void OnServerChangeScene(string newSceneName) { } + + /// + /// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene(). + /// + /// The name of the new scene. + public override void OnServerSceneChanged(string sceneName) { } + + /// + /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed + /// This allows client to do work / cleanup / prep before the scene changes. + /// + /// Name of the scene that's about to be loaded + /// Scene operation that's about to happen + /// true to indicate that scene loading will be handled through overrides + public override void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { } + + /// + /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. + /// Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists. + /// + public override void OnClientSceneChanged() + { + base.OnClientSceneChanged(); + } + + #endregion + + #region Server System Callbacks + + /// + /// Called on the server when a new client connects. + /// Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server. + /// + /// Connection from client. + public override void OnServerConnect(NetworkConnectionToClient conn) { } + + /// + /// Called on the server when a client is ready. + /// The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process. + /// + /// Connection from client. + public override void OnServerReady(NetworkConnectionToClient conn) + { + base.OnServerReady(conn); + } + + /// + /// Called on the server when a client adds a new player with ClientScene.AddPlayer. + /// The default implementation for this function creates a new player object from the playerPrefab. + /// + /// Connection from client. + public override void OnServerAddPlayer(NetworkConnectionToClient conn) + { + base.OnServerAddPlayer(conn); + } + + /// + /// Called on the server when a client disconnects. + /// This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected. + /// + /// Connection from client. + public override void OnServerDisconnect(NetworkConnectionToClient conn) + { + base.OnServerDisconnect(conn); + } + + /// + /// Called on server when transport raises an exception. + /// NetworkConnection may be null. + /// + /// Connection of the client...may be null + /// Exception thrown from the Transport. + public override void OnServerError(NetworkConnectionToClient conn, Exception exception) { } + + #endregion + + #region Client System Callbacks + + /// + /// Called on the client when connected to a server. + /// The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects. + /// + public override void OnClientConnect() + { + base.OnClientConnect(); + } + + /// + /// Called on clients when disconnected from a server. + /// This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects. + /// + public override void OnClientDisconnect() + { + base.OnClientDisconnect(); + } + + /// + /// Called on clients when a servers tells the client it is no longer ready. + /// This is commonly used when switching scenes. + /// + public override void OnClientNotReady() { } + + /// + /// Called on client when transport raises an exception. + /// + /// Exception thrown from the Transport. + public override void OnClientError(Exception exception) { } + + #endregion + + #region Start & Stop Callbacks + + // Since there are multiple versions of StartServer, StartClient and StartHost, to reliably customize + // their functionality, users would need override all the versions. Instead these callbacks are invoked + // from all versions, so users only need to implement this one case. + + /// + /// This is invoked when a host is started. + /// StartHost has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartHost() { } + + /// + /// This is invoked when a server is started - including when a host is started. + /// StartServer has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartServer() { } + + /// + /// This is invoked when the client is started. + /// + public override void OnStartClient() { } + + /// + /// This is called when a host is stopped. + /// + public override void OnStopHost() { } + + /// + /// This is called when a server is stopped - including when a host is stopped. + /// + public override void OnStopServer() { } + + /// + /// This is called when a client is stopped. + /// + public override void OnStopClient() { } + + #endregion +} diff --git a/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt.meta b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt.meta new file mode 100644 index 0000000..6221c57 --- /dev/null +++ b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ed73cc79a95879d4abd948a36043c798 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt b/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt new file mode 100644 index 0000000..28b3161 --- /dev/null +++ b/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Mirror; +using UnityEngine; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-authenticators + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkAuthenticator.html +*/ + +public class #SCRIPTNAME# : NetworkAuthenticator +{ + #region Messages + + public struct AuthRequestMessage : NetworkMessage { } + + public struct AuthResponseMessage : NetworkMessage { } + + #endregion + + #region Server + + /// + /// Called on server from StartServer to initialize the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStartServer() + { + // register a handler for the authentication request we expect from client + NetworkServer.RegisterHandler(OnAuthRequestMessage, false); + } + + /// + /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// + /// Connection to client. + public override void OnServerAuthenticate(NetworkConnectionToClient conn) { } + + /// + /// Called on server when the client's AuthRequestMessage arrives + /// + /// Connection to client. + /// The message payload + public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg) + { + AuthResponseMessage authResponseMessage = new AuthResponseMessage(); + + conn.Send(authResponseMessage); + + // Accept the successful authentication + ServerAccept(conn); + } + + #endregion + + #region Client + + /// + /// Called on client from StartClient to initialize the Authenticator + /// Client message handlers should be registered in this method. + /// + public override void OnStartClient() + { + // register a handler for the authentication response we expect from server + NetworkClient.RegisterHandler(OnAuthResponseMessage, false); + } + + /// + /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// + public override void OnClientAuthenticate() + { + AuthRequestMessage authRequestMessage = new AuthRequestMessage(); + + NetworkClient.Send(authRequestMessage); + } + + /// + /// Called on client when the server's AuthResponseMessage arrives + /// + /// The message payload + public void OnAuthResponseMessage(AuthResponseMessage msg) + { + // Authentication has been accepted + ClientAccept(); + } + + #endregion +} diff --git a/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta b/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta new file mode 100644 index 0000000..be22fe6 --- /dev/null +++ b/Assets/ScriptTemplates/51-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 12dc04aca2d89f744bef5a65622ba708 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt new file mode 100644 index 0000000..a53aee4 --- /dev/null +++ b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/guides/networkbehaviour + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkBehaviour.html +*/ + +// NOTE: Do not put objects in DontDestroyOnLoad (DDOL) in Awake. You can do that in Start instead. + +public class #SCRIPTNAME# : NetworkBehaviour +{ + #region Start & Stop Callbacks + + /// + /// This is invoked for NetworkBehaviour objects when they become active on the server. + /// This could be triggered by NetworkServer.Listen() for objects in the scene, or by NetworkServer.Spawn() for objects that are dynamically created. + /// This will be called for objects on a "host" as well as for object on a dedicated server. + /// + public override void OnStartServer() { } + + /// + /// Invoked on the server when the object is unspawned + /// Useful for saving object data in persistent storage + /// + public override void OnStopServer() { } + + /// + /// Called on every NetworkBehaviour when it is activated on a client. + /// Objects on the host have this function called, as there is a local client on the host. The values of SyncVars on object are guaranteed to be initialized correctly with the latest state from the server when this function is called on the client. + /// + public override void OnStartClient() { } + + /// + /// This is invoked on clients when the server has caused this object to be destroyed. + /// This can be used as a hook to invoke effects or do client specific cleanup. + /// + public override void OnStopClient() { } + + /// + /// Called when the local player object has been set up. + /// This happens after OnStartClient(), as it is triggered by an ownership message from the server. This is an appropriate place to activate components or functionality that should only be active for the local player, such as cameras and input. + /// + public override void OnStartLocalPlayer() { } + + /// + /// Called when the local player object is being stopped. + /// This happens before OnStopClient(), as it may be triggered by an ownership message from the server, or because the player object is being destroyed. This is an appropriate place to deactivate components or functionality that should only be active for the local player, such as cameras and input. + /// + public override void OnStopLocalPlayer() {} + + /// + /// This is invoked on behaviours that have authority, based on context and NetworkIdentity.hasAuthority. + /// This is called after OnStartServer and before OnStartClient. + /// When AssignClientAuthority is called on the server, this will be called on the client that owns the object. When an object is spawned with NetworkServer.Spawn with a NetworkConnectionToClient parameter included, this will be called on the client that owns the object. + /// + public override void OnStartAuthority() { } + + /// + /// This is invoked on behaviours when authority is removed. + /// When NetworkIdentity.RemoveClientAuthority is called on the server, this will be called on the client that owns the object. + /// + public override void OnStopAuthority() { } + + #endregion +} diff --git a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta new file mode 100644 index 0000000..c5a0018 --- /dev/null +++ b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 29b2ae9aeacc49b47b711838dd1876a4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt b/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt new file mode 100644 index 0000000..37df416 --- /dev/null +++ b/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/guides/interest-management + API Reference: https://mirror-networking.com/docs/api/Mirror.InterestManagement.html +*/ + +// NOTE: Attach this component to the same object as your Network Manager. + +public class #SCRIPTNAME# : InterestManagement +{ + /// + /// Callback used by the visibility system to determine if an observer (client) can see the NetworkIdentity. + /// If this function returns true, the network connection will be added as an observer. + /// + /// Object to be observed (or not) by a client + /// Network Connection of a client. + /// True if the client can see this object. + [ServerCallback] + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Default behaviour of making the identity object visible to all clients. + // Replace this code with your own logic as appropriate. + return true; + } + + /// + /// Callback used by the visibility system to determine if an observer (client) can see the NetworkIdentity. + /// Add connections to newObservers that should see the identity object. + /// + /// Object to be observed (or not) by clients + /// cached hashset to put the result into + /// true if being rebuilt for the first time + [ServerCallback] + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers, bool initialize) + { + // Default behaviour of making the identity object visible to all clients. + // Replace this code with your own logic as appropriate. + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + newObservers.Add(conn); + } + + /// + /// Called on the server when a new networked object is spawned. + /// + /// NetworkIdentity of the object being spawned + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { } + + /// + /// Called on the server when a networked object is destroyed. + /// + /// NetworkIdentity of the object being destroyed + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { } + + /// + /// Callback used by the visibility system for objects on a host. + /// Objects on a host (with a local client) cannot be disabled or destroyed when + /// they are not visible to the local client, so this function is called to allow + /// custom code to hide these objects. + /// A typical implementation will disable renderer components on the object. + /// This is only called on local clients on a host. + /// + /// NetworkIdentity of the object being considered for visibility + /// True if the identity object should be visible to the host client + [ServerCallback] + public override void SetHostVisibility(NetworkIdentity identity, bool visible) + { + base.SetHostVisibility(identity, visible); + } + + /// + /// Called by NetworkServer in Initialize and Shutdown + /// + [ServerCallback] + public override void Reset() { } + + [ServerCallback] + void Update() + { + // Here is where you'd need to evaluate if observers need to be rebuilt, + // either for a specific object, a subset of objects, or all objects. + + // Review the code in the various Interest Management components + // included with Mirror for inspiration: + // - Distance Interest Management + // - Spatial Hash Interest Management + // - Scene Interest Management + // - Match Interest Management + // - Team Interest Management + } +} diff --git a/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta b/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta new file mode 100644 index 0000000..328373e --- /dev/null +++ b/Assets/ScriptTemplates/53-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fc1892bf5b3ab304a9be7b71d05b6ae8 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt new file mode 100644 index 0000000..a067926 --- /dev/null +++ b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt @@ -0,0 +1,178 @@ +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-room-manager + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkRoomManager.html + + See Also: NetworkManager + Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html +*/ + +/// +/// This is a specialized NetworkManager that includes a networked room. +/// The room has slots that track the joined players, and a maximum player count that is enforced. +/// It requires that the NetworkRoomPlayer component be on the room player objects. +/// NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. +/// +public class #SCRIPTNAME# : NetworkRoomManager +{ + #region Server Callbacks + + /// + /// This is called on the server when the server is started - including when a host is started. + /// + public override void OnRoomStartServer() { } + + /// + /// This is called on the server when the server is stopped - including when a host is stopped. + /// + public override void OnRoomStopServer() { } + + /// + /// This is called on the host when a host is started. + /// + public override void OnRoomStartHost() { } + + /// + /// This is called on the host when the host is stopped. + /// + public override void OnRoomStopHost() { } + + /// + /// This is called on the server when a new client connects to the server. + /// + /// The new connection. + public override void OnRoomServerConnect(NetworkConnectionToClient conn) { } + + /// + /// This is called on the server when a client disconnects. + /// + /// The connection that disconnected. + public override void OnRoomServerDisconnect(NetworkConnectionToClient conn) { } + + /// + /// This is called on the server when a networked scene finishes loading. + /// + /// Name of the new scene. + public override void OnRoomServerSceneChanged(string sceneName) { } + + /// + /// This allows customization of the creation of the room-player object on the server. + /// By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized. + /// + /// The connection the player object is for. + /// The new room-player object. + public override GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn) + { + return base.OnRoomServerCreateRoomPlayer(conn); + } + + /// + /// This allows customization of the creation of the GamePlayer object on the server. + /// By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection. + /// + /// The connection the player object is for. + /// The room player object for this connection. + /// A new GamePlayer object. + public override GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer) + { + return base.OnRoomServerCreateGamePlayer(conn, roomPlayer); + } + + /// + /// This allows customization of the creation of the GamePlayer object on the server. + /// This is only called for subsequent GamePlay scenes after the first one. + /// See OnRoomServerCreateGamePlayer to customize the player object for the initial GamePlay scene. + /// + /// The connection the player object is for. + public override void OnRoomServerAddPlayer(NetworkConnectionToClient conn) + { + base.OnRoomServerAddPlayer(conn); + } + + /// + /// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene. + /// When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object. + /// + /// The connection of the player + /// The room player object. + /// The game player object. + /// False to not allow this player to replace the room player. + public override bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer) + { + return base.OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer); + } + + /// + /// This is called on the server when all the players in the room are ready. + /// The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader. + /// + public override void OnRoomServerPlayersReady() + { + base.OnRoomServerPlayersReady(); + } + + /// + /// This is called on the server when CheckReadyToBegin finds that players are not ready + /// May be called multiple times while not ready players are joining + /// + public override void OnRoomServerPlayersNotReady() { } + + #endregion + + #region Client Callbacks + + /// + /// This is a hook to allow custom behaviour when the game client enters the room. + /// + public override void OnRoomClientEnter() { } + + /// + /// This is a hook to allow custom behaviour when the game client exits the room. + /// + public override void OnRoomClientExit() { } + + /// + /// This is called on the client when it connects to server. + /// + public override void OnRoomClientConnect() { } + + /// + /// This is called on the client when disconnected from a server. + /// + public override void OnRoomClientDisconnect() { } + + /// + /// This is called on the client when a client is started. + /// + public override void OnRoomStartClient() { } + + /// + /// This is called on the client when the client stops. + /// + public override void OnRoomStopClient() { } + + /// + /// This is called on the client when the client is finished loading a new networked scene. + /// + public override void OnRoomClientSceneChanged() { } + + /// + /// Called on the client when adding a player to the room fails. + /// This could be because the room is full, or the connection is not allowed to have more players. + /// + public override void OnRoomClientAddPlayerFailed() { } + + #endregion + + #region Optional UI + + public override void OnGUI() + { + base.OnGUI(); + } + + #endregion +} diff --git a/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt.meta b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt.meta new file mode 100644 index 0000000..fe5bc32 --- /dev/null +++ b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2e5656107e61a93439544b91e5f541f6 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt b/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt new file mode 100644 index 0000000..f970cfe --- /dev/null +++ b/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt @@ -0,0 +1,107 @@ +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-room-player + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkRoomPlayer.html +*/ + +/// +/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system. +/// The RoomPrefab object of the NetworkRoomManager must have this component on it. +/// This component holds basic room player data required for the room to function. +/// Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer. +/// +public class #SCRIPTNAME# : NetworkRoomPlayer +{ + #region Start & Stop Callbacks + + /// + /// This is invoked for NetworkBehaviour objects when they become active on the server. + /// This could be triggered by NetworkServer.Listen() for objects in the scene, or by NetworkServer.Spawn() for objects that are dynamically created. + /// This will be called for objects on a "host" as well as for object on a dedicated server. + /// + public override void OnStartServer() { } + + /// + /// Invoked on the server when the object is unspawned + /// Useful for saving object data in persistent storage + /// + public override void OnStopServer() { } + + /// + /// Called on every NetworkBehaviour when it is activated on a client. + /// Objects on the host have this function called, as there is a local client on the host. The values of SyncVars on object are guaranteed to be initialized correctly with the latest state from the server when this function is called on the client. + /// + public override void OnStartClient() { } + + /// + /// This is invoked on clients when the server has caused this object to be destroyed. + /// This can be used as a hook to invoke effects or do client specific cleanup. + /// + public override void OnStopClient() { } + + /// + /// Called when the local player object has been set up. + /// This happens after OnStartClient(), as it is triggered by an ownership message from the server. This is an appropriate place to activate components or functionality that should only be active for the local player, such as cameras and input. + /// + public override void OnStartLocalPlayer() { } + + /// + /// This is invoked on behaviours that have authority, based on context and NetworkIdentity.hasAuthority. + /// This is called after OnStartServer and before OnStartClient. + /// When is called on the server, this will be called on the client that owns the object. When an object is spawned with NetworkServer.Spawn with a NetworkConnectionToClient parameter included, this will be called on the client that owns the object. + /// + public override void OnStartAuthority() { } + + /// + /// This is invoked on behaviours when authority is removed. + /// When NetworkIdentity.RemoveClientAuthority is called on the server, this will be called on the client that owns the object. + /// + public override void OnStopAuthority() { } + + #endregion + + #region Room Client Callbacks + + /// + /// This is a hook that is invoked on all player objects when entering the room. + /// Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called. + /// + public override void OnClientEnterRoom() { } + + /// + /// This is a hook that is invoked on all player objects when exiting the room. + /// + public override void OnClientExitRoom() { } + + #endregion + + #region SyncVar Hooks + + /// + /// This is a hook that is invoked on clients when the index changes. + /// + /// The old index value + /// The new index value + public override void IndexChanged(int oldIndex, int newIndex) { } + + /// + /// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready. + /// This function is called when the a client player calls SendReadyToBeginMessage() or SendNotReadyToBeginMessage(). + /// + /// The old readyState value + /// The new readyState value + public override void ReadyStateChanged(bool oldReadyState, bool newReadyState) { } + + #endregion + + #region Optional UI + + public override void OnGUI() + { + base.OnGUI(); + } + + #endregion +} diff --git a/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt.meta b/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt.meta new file mode 100644 index 0000000..36a48dd --- /dev/null +++ b/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1ca8a6309173d4248bc7fa0c6ae001e0 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt new file mode 100644 index 0000000..8f16194 --- /dev/null +++ b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt @@ -0,0 +1,83 @@ +using System.Net; +using Mirror; +using Mirror.Discovery; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-discovery + API Reference: https://mirror-networking.com/docs/api/Mirror.Discovery.NetworkDiscovery.html +*/ + +public class DiscoveryRequest : NetworkMessage +{ + // Add properties for whatever information you want sent by clients + // in their broadcast messages that servers will consume. +} + +public class DiscoveryResponse : NetworkMessage +{ + // Add properties for whatever information you want the server to return to + // clients for them to display or consume for establishing a connection. +} + +public class #SCRIPTNAME# : NetworkDiscoveryBase +{ + #region Server + + /// + /// Reply to the client to inform it of this server + /// + /// + /// Override if you wish to ignore server requests based on + /// custom criteria such as language, full server game mode or difficulty + /// + /// Request coming from client + /// Address of the client that sent the request + protected override void ProcessClientRequest(DiscoveryRequest request, IPEndPoint endpoint) + { + base.ProcessClientRequest(request, endpoint); + } + + /// + /// Process the request from a client + /// + /// + /// Override if you wish to provide more information to the clients + /// such as the name of the host player + /// + /// Request coming from client + /// Address of the client that sent the request + /// A message containing information about this server + protected override DiscoveryResponse ProcessRequest(DiscoveryRequest request, IPEndPoint endpoint) + { + return new DiscoveryResponse(); + } + + #endregion + + #region Client + + /// + /// Create a message that will be broadcasted on the network to discover servers + /// + /// + /// Override if you wish to include additional data in the discovery message + /// such as desired game mode, language, difficulty, etc... + /// An instance of ServerRequest with data to be broadcasted + protected override DiscoveryRequest GetRequest() + { + return new DiscoveryRequest(); + } + + /// + /// Process the answer from a server + /// + /// + /// A client receives a reply from a server, this method processes the + /// reply and raises an event + /// + /// Response that came from the server + /// Address of the server that replied + protected override void ProcessResponse(DiscoveryResponse response, IPEndPoint endpoint) { } + + #endregion +} diff --git a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta new file mode 100644 index 0000000..a034ec8 --- /dev/null +++ b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 04337367db30af3459bf9e9f3f880734 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt new file mode 100644 index 0000000..0e38c1a --- /dev/null +++ b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt @@ -0,0 +1,163 @@ +#define onlySyncOnChange_BANDWIDTH_SAVING +using System.Collections.Generic; +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-transform + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkTransformBase.html +*/ + +public class #SCRIPTNAME# : NetworkTransformBase +{ + protected override Transform targetComponent => transform; + + // If you need this template to reference a child target, + // replace the line above with the code below. + + /* + [Header("Target")] + public Transform target; + + protected override Transform targetComponent => target; + */ + + #region Unity Callbacks + + protected override void OnValidate() + { + base.OnValidate(); + } + + /// + /// This calls Reset() + /// + protected override void OnEnable() + { + base.OnEnable(); + } + + /// + /// This calls Reset() + /// + protected override void OnDisable() + { + base.OnDisable(); + } + + /// + /// Buffers are cleared and interpolation times are reset to zero here. + /// This may be called when you are implementing some system of not sending + /// if nothing changed, or just plain resetting if you have not received data + /// for some time, as this will prevent a long interpolation period between old + /// and just received data, as it will look like a lag. Reset() should also be + /// called when authority is changed to another client or server, to prevent + /// old buffers bugging out the interpolation if authority is changed back. + /// + public override void Reset() + { + base.Reset(); + } + + #endregion + + #region NT Base Callbacks + + /// + /// NTSnapshot struct is created from incoming data from server + /// and added to SnapshotInterpolation sorted list. + /// You may want to skip calling the base method for the local player + /// if doing client-side prediction, or perhaps pass altered values, + /// or compare the server data to local values and correct large differences. + /// + protected override void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + base.OnServerToClientSync(position, rotation, scale); + } + + /// + /// NTSnapshot struct is created from incoming data from client + /// and added to SnapshotInterpolation sorted list. + /// You may want to implement anti-cheat checks here in client authority mode. + /// + protected override void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + base.OnClientToServerSync(position, rotation, scale); + } + + /// + /// Called by both CmdTeleport and RpcTeleport on server and clients, respectively. + /// Here you can disable a Character Controller before calling the base method, + /// and re-enable it after the base method call to avoid conflicting with it. + /// + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + } + + /// + /// Called by both CmdTeleport and RpcTeleport on server and clients, respectively. + /// Here you can disable a Character Controller before calling the base method, + /// and re-enable it after the base method call to avoid conflicting with it. + /// + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + } + + /// + /// NTSnapshot struct is created here + /// + protected override NTSnapshot ConstructSnapshot() + { + return base.ConstructSnapshot(); + } + + /// + /// localPosition, localRotation, and localScale are set here: + /// interpolated values are used if interpolation is enabled. + /// goal values are used if interpolation is disabled. + /// + protected override void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated) + { + base.ApplySnapshot(start, goal, interpolated); + } + +#if onlySyncOnChange_BANDWIDTH_SAVING + + /// + /// Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + /// + protected override bool CompareSnapshots(NTSnapshot currentSnapshot) + { + return base.CompareSnapshots(currentSnapshot); + } + +#endif + + #endregion + + #region GUI + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // OnGUI allocates even if it does nothing. avoid in release. + + protected override void OnGUI() + { + base.OnGUI(); + } + + protected override void DrawGizmos(SortedList buffer) + { + base.DrawGizmos(buffer); + } + + protected override void OnDrawGizmos() + { + base.OnDrawGizmos(); + } + +#endif + + #endregion +} diff --git a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta new file mode 100644 index 0000000..be7e6d7 --- /dev/null +++ b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 38d41e2048919f64ea817eb343ffb09d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts.meta b/Assets/Scripts.meta new file mode 100644 index 0000000..c808ac8 --- /dev/null +++ b/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd516c5e3d5ff134ca9eb2748412b438 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor.meta b/Assets/Scripts/Editor.meta new file mode 100644 index 0000000..4fe8142 --- /dev/null +++ b/Assets/Scripts/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2883cd70370bc294cac5fc73878c6194 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/BuildScript.cs b/Assets/Scripts/Editor/BuildScript.cs new file mode 100644 index 0000000..851012c --- /dev/null +++ b/Assets/Scripts/Editor/BuildScript.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using UnityEditor; +using UnityEngine; + +public class BuildScript +{ + [MenuItem("Build/Build All")] + public static void BuildAll() + { + BuildWindowsServer(); + BuildLinuxServer(); + BuildWindowsClient(); + } + + [MenuItem("Build/Build Server (Windows)")] + public static void BuildWindowsServer() + { + BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); + buildPlayerOptions.scenes = new[] { "Assets/Scenes/Main.unity" }; + buildPlayerOptions.locationPathName = "Builds/Windows/Server/Server.exe"; + buildPlayerOptions.target = BuildTarget.StandaloneWindows64; + buildPlayerOptions.options = BuildOptions.CompressWithLz4HC | BuildOptions.EnableHeadlessMode; + + Console.WriteLine("Building Server (Windows)..."); + BuildPipeline.BuildPlayer(buildPlayerOptions); + Console.WriteLine("Built Server (Windows)."); + } + + [MenuItem("Build/Build Server (Linux)")] + public static void BuildLinuxServer() + { + BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); + buildPlayerOptions.scenes = new[] { "Assets/Scenes/Main.unity" }; + buildPlayerOptions.locationPathName = "Builds/Linux/Server/Server.x86_64"; + buildPlayerOptions.target = BuildTarget.StandaloneLinux64; + buildPlayerOptions.options = BuildOptions.CompressWithLz4HC | BuildOptions.EnableHeadlessMode; + + Console.WriteLine("Building Server (Linux)..."); + BuildPipeline.BuildPlayer(buildPlayerOptions); + Console.WriteLine("Built Server (Linux)."); + } + + + [MenuItem("Build/Build Client (Windows)")] + public static void BuildWindowsClient() + { + BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); + buildPlayerOptions.scenes = new[] { "Assets/Scenes/Main.unity" }; + buildPlayerOptions.locationPathName = "Builds/Windows/Client/Client.exe"; + buildPlayerOptions.target = BuildTarget.StandaloneWindows64; + buildPlayerOptions.options = BuildOptions.CompressWithLz4HC; + + Console.WriteLine("Building Client (Windows)..."); + BuildPipeline.BuildPlayer(buildPlayerOptions); + Console.WriteLine("Built Client (Windows)."); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Editor/BuildScript.cs.meta b/Assets/Scripts/Editor/BuildScript.cs.meta new file mode 100644 index 0000000..f4fd5be --- /dev/null +++ b/Assets/Scripts/Editor/BuildScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 367e228e984bf7349887e31e34c70d50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MenuUI.cs b/Assets/Scripts/MenuUI.cs new file mode 100644 index 0000000..750e1aa --- /dev/null +++ b/Assets/Scripts/MenuUI.cs @@ -0,0 +1,226 @@ +// vis2k: GUILayout instead of spacey += ...; removed Update hotkeys to avoid +// confusion if someone accidentally presses one. +using System.ComponentModel; +using System.Collections.Generic; +using UnityEngine; +using Network; +using Mirror; +using Unity.Services.Relay.Models; + +namespace UI +{ + /// + /// An extension for the NetworkManager that displays a default HUD for controlling the network state of the game. + /// This component also shows useful internal state for the networking system in the inspector window of the editor. It allows users to view connections, networked objects, message handlers, and packet statistics. This information can be helpful when debugging networked games. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/MenuUI")] + [RequireComponent(typeof(MyNetworkManager))] + [EditorBrowsable(EditorBrowsableState.Never)] + [HelpURL("https://mirror-networking.com/docs/Components/NetworkManagerHUD.html")] + public class MenuUI : MonoBehaviour + { + private MyNetworkManager m_Manager; + + /// + /// Whether to show the default control HUD at runtime. + /// + public bool showGUI = true; + + /// + /// The horizontal offset in pixels to draw the HUD runtime GUI at. + /// + public int offsetX; + + /// + /// The vertical offset in pixels to draw the HUD runtime GUI at. + /// + public int offsetY; + + void Awake() + { + m_Manager = GetComponent(); + } + + void OnGUI() + { + string[] args = System.Environment.GetCommandLineArgs(); + foreach (string arg in args) + { + if (arg == "-server") + { + m_Manager.StartServer(); + showGUI = false; + } + } + + if (!showGUI) + return; + + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999)); + if (!NetworkClient.isConnected && !NetworkServer.active) + { + StartButtons(); + } + else + { + StatusLabels(); + } + + // client ready + if (NetworkClient.isConnected && !NetworkClient.ready) + { + NetworkClient.Ready(); + + if (NetworkClient.localPlayer == null) + { + NetworkClient.AddPlayer(); + } + + } + + StopButtons(); + + GUILayout.EndArea(); + } + + void StartButtons() + { + if (!NetworkClient.active) + { + // Server Only + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + // cant be a server in webgl build + GUILayout.Box("( WebGL cannot be server )"); + } + else + { + if (GUILayout.Button("Server Only")) m_Manager.StartStandardServer(); + } + + if (m_Manager.isLoggedIn) + { + // Server + Client + if (Application.platform != RuntimePlatform.WebGLPlayer) + { + if (GUILayout.Button("Standard Host (Server + Client)")) + { + m_Manager.StartStandardHost(); + } + + if (GUILayout.Button("Relay Host (Server + Client)")) + { + int maxPlayers = 8; + m_Manager.StartRelayHost(maxPlayers); + } + } + + // Client + IP + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Client (DGS)")) + { + m_Manager.JoinStandardServer(); + } + m_Manager.networkAddress = GUILayout.TextField(m_Manager.networkAddress); + GUILayout.EndHorizontal(); + + // Client + Relay Join Code + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Client (Relay)")) + { + m_Manager.JoinRelayServer(); + } + m_Manager.relayJoinCode = GUILayout.TextField(m_Manager.relayJoinCode); + GUILayout.EndHorizontal(); + + if (GUILayout.Button("Get Relay Regions")) + { + // Note: We are not doing anything with these regions in this example, we are just illustrating how you would go about fetching these regions + m_Manager.GetRelayRegions((List regions) => + { + if (regions.Count > 0) + { + for (int i = 0; i < regions.Count; i++) + { + Region region = regions[i]; + Debug.Log("Found region. ID: " + region.Id + ", Name: " + region.Description); + } + } + else + { + Debug.LogWarning("No regions received"); + } + }, + + () => + { + Debug.LogError("Failed to retrieve the list of Relay regions."); + }); + } + } + else + { + if (GUILayout.Button("Auth Login")) + { + m_Manager.UnityLogin(); + } + } + } + else + { + // Connecting + GUILayout.Label("Connecting to " + m_Manager.networkAddress + ".."); + if (GUILayout.Button("Cancel Connection Attempt")) + { + m_Manager.StopClient(); + } + } + } + + void StatusLabels() + { + // server / client status message + if (NetworkServer.active) + { + GUILayout.Label("Server: active. Transport: " + Transport.activeTransport); + if (m_Manager.IsRelayEnabled()) + { + GUILayout.Label("Relay enabled. Join code: " + m_Manager.relayJoinCode); + } + } + if (NetworkClient.isConnected) + { + GUILayout.Label("Client: address=" + m_Manager.networkAddress); + } + } + + void StopButtons() + { + // stop host if host mode + if (NetworkServer.active && NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Host")) + { + m_Manager.StopHost(); + } + } + // stop client if client-only + else if (NetworkClient.isConnected) + { + if (GUILayout.Button("Stop Client")) + { + m_Manager.StopClient(); + } + } + // stop server if server-only + else if (NetworkServer.active) + { + if (GUILayout.Button("Stop Server")) + { + m_Manager.StopServer(); + } + } + } + } +} diff --git a/Assets/Scripts/MenuUI.cs.meta b/Assets/Scripts/MenuUI.cs.meta new file mode 100644 index 0000000..6c30052 --- /dev/null +++ b/Assets/Scripts/MenuUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f71e61a73431ed4db3d59daab75847e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MyNetworkManager.cs b/Assets/Scripts/MyNetworkManager.cs new file mode 100644 index 0000000..2adb001 --- /dev/null +++ b/Assets/Scripts/MyNetworkManager.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Mirror; +using Unity.Services.Authentication; +using Unity.Services.Core; + +using Utp; + +namespace Network +{ + public class MyNetworkManager : RelayNetworkManager + { + /// + /// The local player object that spawns in. + /// + public Player localPlayer; + private string m_SessionId = ""; + private string m_Username; + private string m_UserId; + + /// + /// Flag to determine if the user is logged into the backend. + /// + public bool isLoggedIn = false; + + /// + /// List of players currently connected to the server. + /// + private List m_Players; + + public override void Awake() + { + base.Awake(); + m_Players = new List(); + + m_Username = SystemInfo.deviceName; + } + + public async void UnityLogin() + { + try + { + await UnityServices.InitializeAsync(); + await AuthenticationService.Instance.SignInAnonymouslyAsync(); + Debug.Log("Logged into Unity, player ID: " + AuthenticationService.Instance.PlayerId); + isLoggedIn = true; + } + catch (Exception e) + { + isLoggedIn = false; + Debug.Log(e); + } + } + + private void Update() + { + if (NetworkManager.singleton.isNetworkActive) + { + if (localPlayer == null) + { + FindLocalPlayer(); + } + } + else + { + localPlayer = null; + m_Players.Clear(); + } + } + + + public override void OnStartServer() + { + Debug.Log("MyNetworkManager: Server Started!"); + + m_SessionId = System.Guid.NewGuid().ToString(); + } + + public override void OnServerAddPlayer(NetworkConnectionToClient conn) + { + base.OnServerAddPlayer(conn); + + foreach (KeyValuePair kvp in NetworkServer.spawned) + { + Player comp = kvp.Value.GetComponent(); + + // Add to player list if new + if (comp != null && !m_Players.Contains(comp)) + { + comp.sessionId = m_SessionId; + m_Players.Add(comp); + } + } + } + + public override void OnStopServer() + { + Debug.Log("MyNetworkManager: Server Stopped!"); + m_SessionId = ""; + } + + public override void OnServerDisconnect(NetworkConnectionToClient conn) + { + base.OnServerDisconnect(conn); + + Dictionary spawnedPlayers = NetworkServer.spawned; + + // Update players list on client disconnect + foreach (Player player in m_Players) + { + bool playerFound = false; + + foreach (KeyValuePair kvp in spawnedPlayers) + { + Player comp = kvp.Value.GetComponent(); + + // Verify the player is still in the match + if (comp != null && player == comp) + { + playerFound = true; + break; + } + } + + if (!playerFound) + { + m_Players.Remove(player); + break; + } + } + } + + public override void OnStopClient() + { + base.OnStopClient(); + + Debug.Log("MyNetworkManager: Left the Server!"); + + localPlayer = null; + + m_SessionId = ""; + } + + public override void OnClientConnect(NetworkConnection conn) + { + Debug.Log($"MyNetworkManager: {m_Username} Connected to Server!"); + } + + public override void OnClientDisconnect(NetworkConnection conn) + { + base.OnClientDisconnect(conn); + Debug.Log("MyNetworkManager: Disconnected from Server!"); + } + + /// + /// Finds the local player if they are spawned in the scene. + /// + void FindLocalPlayer() + { + //Check to see if the player is loaded in yet + if (NetworkClient.localPlayer == null) + return; + + localPlayer = NetworkClient.localPlayer.GetComponent(); + } + } +} + diff --git a/Assets/Scripts/MyNetworkManager.cs.meta b/Assets/Scripts/MyNetworkManager.cs.meta new file mode 100644 index 0000000..49f55a3 --- /dev/null +++ b/Assets/Scripts/MyNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9810247aefdc844781df8cf4c039e9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Player.cs b/Assets/Scripts/Player.cs new file mode 100644 index 0000000..dfc23f6 --- /dev/null +++ b/Assets/Scripts/Player.cs @@ -0,0 +1,61 @@ +using Mirror; +using UnityEngine; + +public class Player : NetworkBehaviour +{ + /// + /// The Sessions ID for the current server. + /// + [SyncVar] + public string sessionId = ""; + + /// + /// Player name. + /// + public string username; + + public string ip; + + /// + /// Platform the user is on. + /// + public string platform; + + /// + /// Shifts the players position in space based on the inputs received. + /// + void HandleMovement() + { + if (isLocalPlayer) + { + float moveHorizontal = Input.GetAxis("Horizontal"); + float moveVertical = Input.GetAxis("Vertical"); + Vector3 movement = new Vector3(moveHorizontal * 0.1f, moveVertical * 0.1f, 0); + transform.position = transform.position + movement; + } + } + + private void Awake() + { + username = SystemInfo.deviceName; + platform = Application.platform.ToString(); + ip = NetworkManager.singleton.networkAddress; + } + + private void Start() + { + } + + void Update() + { + HandleMovement(); + } + + /// + /// Called after player has spawned in the scene. + /// + public override void OnStartServer() + { + Debug.Log("Player has been spawned on the server!"); + } +} diff --git a/Assets/Scripts/Player.cs.meta b/Assets/Scripts/Player.cs.meta new file mode 100644 index 0000000..e3392a2 --- /dev/null +++ b/Assets/Scripts/Player.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d43cf183cda48544ea9aa5bbfa7a4625 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets.meta b/Assets/StreamingAssets.meta new file mode 100644 index 0000000..031eb97 --- /dev/null +++ b/Assets/StreamingAssets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4af96ed9316df6f42928c8cddc9e363e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport.meta b/Assets/UTPTransport.meta new file mode 100644 index 0000000..82c768b --- /dev/null +++ b/Assets/UTPTransport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f0c45a93e400d24ab42c7e81736f130 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/README.md b/Assets/UTPTransport/README.md new file mode 100644 index 0000000..730839e --- /dev/null +++ b/Assets/UTPTransport/README.md @@ -0,0 +1,46 @@ +# UTPTransport for Mirror + +This sample contains a [Transport](https://mirror-networking.gitbook.io/docs/transports) for the [Mirror Networking library](https://mirror-networking.com/) that is compatible with the [Unity Transport package](https://docs.unity3d.com/Packages/com.unity.transport@latest) and the [Unity Relay service](https://docs.unity.com/relay). + +## Dependencies + +- [Mirror Networking library](https://mirror-networking.com/) +- [Unity Jobs package](https://docs.unity3d.com/Packages/com.unity.jobs@latest) +- [Unity Relay package](https://docs.unity3d.com/Packages/com.unity.services.relay@latest) + +## Installation + +1. Visit the [Mirror page in the Asset Store](https://assetstore.unity.com/packages/tools/network/mirror-129321) and add Mirror to `My Assets`. +2. Import Mirror using the Package Manager (`Window -> Package Manager -> Packages: My Assets -> Mirror -> Download/Import`). +3. Import Unity Jobs using the Package Manager (`Window -> Package Manager -> Add -> Add package from git URL... -> "com.unity.jobs"`). +3. Import Unity Relay using the Package Manager (`Window -> Package Manager -> Add -> Add package from git URL... -> "com.unity.services.relay"`). +4. Copy the `Assets\UTPTransport` directory from this sample into the `Assets/` directory in your project. +5. Attach the `Mirror.NetworkManager` component to your `GameObject`. +6. Attach the `UTP.UtpTransport` component to your `GameObject`. +7. Assign the `UTP.UtpTransport` component to the `Transport` field of the `Mirror.NetworkManager` component. + +## Utilizing Relay + +If you want to use Relay, you must use `UTP.RelayNetworkManager` instead of `Mirror.NetworkManager`. + +`UTP.RelayNetworkManager` inherits from `Mirror.NetworkManager` and adds additional functionality to interact with the Relay service. + +_Note:_ +In order to use Relay, you must authenticate with the [Unity Authentication Service](https://docs.unity.com/authentication/IntroUnityAuthentication.htm). +This is required regardless of whether or not you are utilizing your own authentication service. + +The following snippet demonstrates how to authenticate with the Unity Authentication Service: +```csharp +try +{ + await UnityServices.InitializeAsync(); + await AuthenticationService.Instance.SignInAnonymouslyAsync(); + Debug.Log("Logged into Unity, player ID: " + AuthenticationService.Instance.PlayerId); +} +catch (Exception e) +{ + Debug.LogError(e); +} +``` + +Once authenticated, you may use `UTP.RelayNetworkManager` to either allocate a Relay server or join a Relay server using a join code. \ No newline at end of file diff --git a/Assets/UTPTransport/README.md.meta b/Assets/UTPTransport/README.md.meta new file mode 100644 index 0000000..9b3162b --- /dev/null +++ b/Assets/UTPTransport/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1f22811f6246b944c98d47e1eea727ba +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Relay.meta b/Assets/UTPTransport/Relay.meta new file mode 100644 index 0000000..a599f91 --- /dev/null +++ b/Assets/UTPTransport/Relay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 697f0dc3a9718d846bd1db6a9d67dc76 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Relay/IRelayManager.cs b/Assets/UTPTransport/Relay/IRelayManager.cs new file mode 100644 index 0000000..8fc09e6 --- /dev/null +++ b/Assets/UTPTransport/Relay/IRelayManager.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Unity.Services.Relay.Models; + +namespace Utp +{ + public interface IRelayManager + { + /// + /// The allocation managed by a host who is running as a client and server. + /// + public Allocation ServerAllocation { get; set; } + + /// + /// The allocation managed by a client who is connecting to a server. + /// + public JoinAllocation JoinAllocation { get; set; } + + /// + /// Get a Relay Service JoinAllocation from a given joinCode. + /// + /// The code to look up the joinAllocation for. + /// A callback to invoke when the Relay allocation is successfully retrieved from the join code. + /// A callback to invoke when the Relay allocation is unsuccessfully retrieved from the join code. + public void GetAllocationFromJoinCode(string joinCode, Action onSuccess, Action onFailure); + + /// + /// Get a list of Regions from the Relay Service. + /// + /// A callback to invoke when the list of regions is successfully retrieved. + /// A callback to invoke when the list of regions is unsuccessfully retrieved. + public void GetRelayRegions(Action> onSuccess, Action onFailure); + + /// + /// Allocate a Relay Server. + /// + /// The max number of players that may connect to this server. + /// The region to allocate the server in. May be null. + /// A callback to invoke when the Relay server is successfully allocated. + /// A callback to invoke when the Relay server is unsuccessfully allocated. + public void AllocateRelayServer(int maxPlayers, string regionId, Action onSuccess, Action onFailure); + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Relay/IRelayManager.cs.meta b/Assets/UTPTransport/Relay/IRelayManager.cs.meta new file mode 100644 index 0000000..9d7afed --- /dev/null +++ b/Assets/UTPTransport/Relay/IRelayManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41b580b690239b64fa461b579052d5d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Relay/RelayManager.cs b/Assets/UTPTransport/Relay/RelayManager.cs new file mode 100644 index 0000000..5af44d7 --- /dev/null +++ b/Assets/UTPTransport/Relay/RelayManager.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; + +using Unity.Services.Relay; +using Unity.Services.Relay.Models; + +namespace Utp +{ + public class RelayManager : MonoBehaviour, IRelayManager + { + /// + /// The allocation managed by a host who is running as a client and server. + /// + public Allocation ServerAllocation { get; set; } + + /// + /// The allocation managed by a client who is connecting to a server. + /// + public JoinAllocation JoinAllocation { get; set; } + + /// + /// A callback for when a Relay server is allocated and a join code is fetched. + /// + public Action OnRelayServerAllocated { get; set; } + + /// + /// The interface to the Relay services API. + /// + public IRelayServiceSDK RelayServiceSDK { get; set; } = new WrappedRelayServiceSDK(); + + private void Awake() + { + UtpLog.Info("RelayManager initialized"); + } + + /// + /// Retrieve the corresponding to the specified join code. + /// + /// The join code that will be used to retrieve the JoinAllocation. + /// A callback to invoke when the Relay allocation is successfully retrieved from the join code. + /// A callback to invoke when the Relay allocation is unsuccessfully retrieved from the join code. + public void GetAllocationFromJoinCode(string joinCode, Action onSuccess, Action onFailure) + { + StartCoroutine(GetAllocationFromJoinCodeTask(joinCode, onSuccess, onFailure)); + } + + private IEnumerator GetAllocationFromJoinCodeTask(string joinCode, Action onSuccess, Action onFailure) + { + Task joinAllocation = RelayServiceSDK.JoinAllocationAsync(joinCode); + + while (!joinAllocation.IsCompleted) + { + yield return null; + } + + if (joinAllocation.IsFaulted) + { + joinAllocation.Exception.Flatten().Handle((Exception err) => + { + UtpLog.Error($"Unable to get Relay allocation from join code, encountered an error: {err.Message}."); + + return true; + }); + + onFailure?.Invoke(); + + yield break; + } + + JoinAllocation = joinAllocation.Result; + + onSuccess?.Invoke(); + } + + /// + /// Get a list of Regions from the Relay Service. + /// + /// A callback to invoke when the list of regions is successfully retrieved. + /// A callback to invoke when the list of regions is unsuccessfully retrieved. + public void GetRelayRegions(Action> onSuccess, Action onFailure) + { + StartCoroutine(GetRelayRegionsTask(onSuccess, onFailure)); + } + + private IEnumerator GetRelayRegionsTask(Action> onSuccess, Action onFailure) + { + Task> listRegions = RelayServiceSDK.ListRegionsAsync(); + + while (!listRegions.IsCompleted) + { + yield return null; + } + + if (listRegions.IsFaulted) + { + listRegions.Exception.Flatten().Handle((Exception err) => + { + UtpLog.Error($"Unable to retrieve the list of Relay regions, encountered an error: {err.Message}."); + return true; + }); + + onFailure?.Invoke(); + + yield break; + } + + onSuccess?.Invoke(listRegions.Result); + } + + /// + /// Allocate a Relay Server. + /// + /// The max number of players that may connect to this server. + /// The region to allocate the server in. May be null. + /// A callback to invoke when the Relay server is successfully allocated. + /// A callback to invoke when the Relay server is unsuccessfully allocated. + public void AllocateRelayServer(int maxPlayers, string regionId, Action onSuccess, Action onFailure) + { + StartCoroutine(AllocateRelayServerTask(maxPlayers, regionId, onSuccess, onFailure)); + } + + private IEnumerator AllocateRelayServerTask(int maxPlayers, string regionId, Action onSuccess, Action onFailure) + { + Task createAllocation = RelayServiceSDK.CreateAllocationAsync(maxPlayers, regionId); + + while (!createAllocation.IsCompleted) + { + yield return null; + } + + if (createAllocation.IsFaulted) + { + createAllocation.Exception.Flatten().Handle((Exception err) => + { + UtpLog.Error($"Unable to allocate Relay server, encountered an error creating a Relay allocation: {err.Message}."); + return true; + }); + + onFailure?.Invoke(); + + yield break; + } + + ServerAllocation = createAllocation.Result; + + UtpLog.Verbose($"Received allocation: {ServerAllocation.AllocationId}"); + + StartCoroutine(GetJoinCodeTask(onSuccess, onFailure)); + } + + private IEnumerator GetJoinCodeTask(Action onSuccess, Action onFailure) + { + Task getJoinCode = RelayServiceSDK.GetJoinCodeAsync(ServerAllocation.AllocationId); + + while (!getJoinCode.IsCompleted) + { + yield return null; + } + + if (getJoinCode.IsFaulted) + { + getJoinCode.Exception.Flatten().Handle((Exception err) => + { + UtpLog.Error($"Unable to allocate Relay server, encountered an error retrieving the join code: {err.Message}."); + return true; + }); + + onFailure?.Invoke(); + + yield break; + } + + onSuccess?.Invoke(getJoinCode.Result); + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Relay/RelayManager.cs.meta b/Assets/UTPTransport/Relay/RelayManager.cs.meta new file mode 100644 index 0000000..a8516e6 --- /dev/null +++ b/Assets/UTPTransport/Relay/RelayManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5d43cd5fdaa54a43a8a99c348f15c68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Relay/RelayUtils.cs b/Assets/UTPTransport/Relay/RelayUtils.cs new file mode 100644 index 0000000..2e23d63 --- /dev/null +++ b/Assets/UTPTransport/Relay/RelayUtils.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; + +using Unity.Networking.Transport; +using Unity.Networking.Transport.Relay; +using Unity.Services.Relay.Models; + +namespace Utp +{ + public class RelayUtils + { + /// + /// Construct the ServerData needed to create a RelayNetworkParameter for a host. + /// + /// The Allocation for the Relay Server. + /// The type of connection to the Relay Server. + /// The RelayServerData. + public static RelayServerData HostRelayData(Allocation allocation, RelayServerEndpoint.NetworkOptions connectionType) + { + //Get string from connection + string connectionTypeString = GetStringFromConnectionType(connectionType); + + if (String.IsNullOrEmpty(connectionTypeString)) + { + throw new ArgumentException($"ConnectionType {connectionType} is invalid"); + } + + // Select endpoint based on desired connectionType + var endpoint = GetEndpointForConnectionType(allocation.ServerEndpoints, connectionTypeString); + + if (endpoint == null) + { + throw new ArgumentException($"endpoint for connectionType {connectionType} not found"); + } + + // Prepare the server endpoint using the Relay server IP and port + var serverEndpoint = NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.Port); + + // UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them + var allocationIdBytes = ConvertFromAllocationIdBytes(allocation.AllocationIdBytes); + var connectionData = ConvertConnectionData(allocation.ConnectionData); + var key = ConvertFromHMAC(allocation.Key); + + // Prepare the Relay server data and compute the nonce value + // The host passes its connectionData twice into this function + var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationIdBytes, ref connectionData, + ref connectionData, ref key, connectionTypeString == "dtls"); + relayServerData.ComputeNewNonce(); + + return relayServerData; + } + + /// + /// Construct the ServerData needed to create a RelayNetworkParameter for a player. + /// + /// The JoinAllocation for the Relay Server. + /// The type of connection to the Relay Server. + /// The RelayServerData. + public static RelayServerData PlayerRelayData(JoinAllocation allocation, RelayServerEndpoint.NetworkOptions connectionType) + { + //Get string from connection + string connectionTypeString = GetStringFromConnectionType(connectionType); + + if (String.IsNullOrEmpty(connectionTypeString)) + { + throw new ArgumentException($"ConnectionType {connectionType} is invalid"); + } + + // Select endpoint based on desired connectionType + var endpoint = GetEndpointForConnectionType(allocation.ServerEndpoints, connectionTypeString); + + if (endpoint == null) + { + throw new ArgumentException($"endpoint for connectionType {connectionType} not found"); + } + + // Prepare the server endpoint using the Relay server IP and port + var serverEndpoint = NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.Port); + + // UTP uses pointers instead of managed arrays for performance reasons, so we use these helper functions to convert them + var allocationIdBytes = ConvertFromAllocationIdBytes(allocation.AllocationIdBytes); + var connectionData = ConvertConnectionData(allocation.ConnectionData); + var hostConnectionData = ConvertConnectionData(allocation.HostConnectionData); + var key = ConvertFromHMAC(allocation.Key); + + // Prepare the Relay server data and compute the nonce values + // A player joining the host passes its own connectionData as well as the host's + var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationIdBytes, ref connectionData, + ref hostConnectionData, ref key, connectionTypeString == "dtls"); + relayServerData.ComputeNewNonce(); + + return relayServerData; + } + + /// + /// Gets a network type and returns its string alternative. + /// + /// The type of connection to stringify. + /// The connection type as a string. + private static string GetStringFromConnectionType(RelayServerEndpoint.NetworkOptions connectionType) + { + switch(connectionType) + { + case (RelayServerEndpoint.NetworkOptions.Tcp): return "tcp"; + case (RelayServerEndpoint.NetworkOptions.Udp): return "udp"; + default: return String.Empty; + } + } + + #region Helper Methods + + private static RelayAllocationId ConvertFromAllocationIdBytes(byte[] allocationIdBytes) + { + unsafe + { + fixed (byte* ptr = allocationIdBytes) + { + return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length); + } + } + } + + private static RelayConnectionData ConvertConnectionData(byte[] connectionData) + { + unsafe + { + fixed (byte* ptr = connectionData) + { + return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length); + } + } + } + + private static RelayHMACKey ConvertFromHMAC(byte[] hmac) + { + unsafe + { + fixed (byte* ptr = hmac) + { + return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length); + } + } + } + + private static RelayServerEndpoint GetEndpointForConnectionType(List endpoints, string connectionType) + { + foreach (var endpoint in endpoints) + { + if (endpoint.ConnectionType == connectionType) + { + return endpoint; + } + } + + return null; + } + + #endregion + } +} diff --git a/Assets/UTPTransport/Relay/RelayUtils.cs.meta b/Assets/UTPTransport/Relay/RelayUtils.cs.meta new file mode 100644 index 0000000..0602bed --- /dev/null +++ b/Assets/UTPTransport/Relay/RelayUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa6d388801800e24fbbe27224aa2f93b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs new file mode 100644 index 0000000..c6db453 --- /dev/null +++ b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.Services.Relay; +using Unity.Services.Relay.Models; + +namespace Utp +{ + public class WrappedRelayServiceSDK : IRelayServiceSDK + { + public Task CreateAllocationAsync(int maxConnections, string region = null) + { + return Relay.Instance.CreateAllocationAsync(maxConnections, region); + } + + public Task GetJoinCodeAsync(Guid allocationId) + { + return Relay.Instance.GetJoinCodeAsync(allocationId); + } + + public Task JoinAllocationAsync(string joinCode) + { + return Relay.Instance.JoinAllocationAsync(joinCode); + } + + public Task> ListRegionsAsync() + { + return Relay.Instance.ListRegionsAsync(); + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs.meta b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs.meta new file mode 100644 index 0000000..2dda4dc --- /dev/null +++ b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d359ccea5711ce40adf90e926fbad9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/RelayNetworkManager.cs b/Assets/UTPTransport/RelayNetworkManager.cs new file mode 100644 index 0000000..19d85f5 --- /dev/null +++ b/Assets/UTPTransport/RelayNetworkManager.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; + +using Mirror; + +using UnityEngine; +using Unity.Services.Relay.Models; + +namespace Utp +{ + public class RelayNetworkManager : NetworkManager + { + private UtpTransport utpTransport; + + /// + /// Server's join code if using Relay. + /// + public string relayJoinCode = ""; + + public override void Awake() + { + base.Awake(); + + utpTransport = GetComponent(); + + string[] args = System.Environment.GetCommandLineArgs(); + for (int key = 0; key < args.Length; key++) + { + if (args[key] == "-port") + { + if (key + 1 < args.Length) + { + string value = args[key + 1]; + + try + { + utpTransport.Port = ushort.Parse(value); + } + catch + { + UtpLog.Warning($"Unable to parse {value} into transport Port"); + } + } + } + } + } + + /// + /// Get the port the server is listening on. + /// + /// The port. + public ushort GetPort() + { + return utpTransport.Port; + } + + /// + /// Get whether Relay is enabled or not. + /// + /// True if enabled, false otherwise. + public bool IsRelayEnabled() + { + return utpTransport.useRelay; + } + + /// + /// Ensures Relay is disabled. Starts the server, listening for incoming connections. + /// + public void StartStandardServer() + { + utpTransport.useRelay = false; + StartServer(); + } + + /// + /// Ensures Relay is disabled. Starts a network "host" - a server and client in the same application + /// + public void StartStandardHost() + { + utpTransport.useRelay = false; + StartHost(); + } + + /// + /// Gets available Relay regions. + /// + /// + public void GetRelayRegions(Action> onSuccess, Action onFailure) + { + utpTransport.GetRelayRegions(onSuccess, onFailure); + } + + /// + /// Ensures Relay is enabled. Starts a network "host" - a server and client in the same application + /// + public void StartRelayHost(int maxPlayers, string regionId = null) + { + utpTransport.useRelay = true; + utpTransport.AllocateRelayServer(maxPlayers, regionId, + (string joinCode) => + { + relayJoinCode = joinCode; + + StartHost(); + }, + () => + { + UtpLog.Error($"Failed to start a Relay host."); + }); + } + + /// + /// Ensures Relay is disabled. Starts the client, connects it to the server with networkAddress. + /// + public void JoinStandardServer() + { + utpTransport.useRelay = false; + StartClient(); + } + + /// + /// Ensures Relay is enabled. Starts the client, connects to the server with the relayJoinCode. + /// + public void JoinRelayServer() + { + utpTransport.useRelay = true; + utpTransport.ConfigureClientWithJoinCode(relayJoinCode, + () => + { + StartClient(); + }, + () => + { + UtpLog.Error($"Failed to join Relay server."); + }); + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/RelayNetworkManager.cs.meta b/Assets/UTPTransport/RelayNetworkManager.cs.meta new file mode 100644 index 0000000..8e75169 --- /dev/null +++ b/Assets/UTPTransport/RelayNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a3130411ad885e4aa635aea96a572a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests.meta b/Assets/UTPTransport/Tests.meta new file mode 100644 index 0000000..aeafdc8 --- /dev/null +++ b/Assets/UTPTransport/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 64d6aa8db4ba959d4852e0a251e225b2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/RelayManagerTests.cs b/Assets/UTPTransport/Tests/RelayManagerTests.cs new file mode 100644 index 0000000..8740838 --- /dev/null +++ b/Assets/UTPTransport/Tests/RelayManagerTests.cs @@ -0,0 +1,228 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Unity.Services.Relay; +using Unity.Services.Relay.Models; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Utp +{ + public class RelayManagerTests + { + private RelayManager _relayManager; + + [SetUp] + public void SetUp() + { + var obj = new GameObject(); + _relayManager = obj.AddComponent(); + } + + [TearDown] + public void TearDown() + { + _relayManager.OnRelayServerAllocated = null; + + GameObject.Destroy(_relayManager.gameObject); + } + + [Test] + public void GetAllocationFromJoinCode_FaultedTask_InvokesOnFailure() + { + _relayManager.RelayServiceSDK = new TaskAlwaysFaults(); + + bool didInvokeOnSuccess = false; + bool didInvokeOnFailure = false; + + string validJoinCode = "test"; + Action onSuccess = () => { didInvokeOnSuccess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.GetAllocationFromJoinCode(joinCode: validJoinCode, onSuccess: onSuccess, onFailure: onFailure); + + LogAssert.Expect(LogType.Error, new Regex(@"Unable to get Relay allocation from join code, encountered an error:")); + Assert.That(didInvokeOnSuccess, Is.False); + Assert.That(didInvokeOnFailure, Is.True); + } + + [Test] + public void GetAllocationFromJoinCode_CompletedTask_InvokesOnSuccess() + { + _relayManager.RelayServiceSDK = new TaskAlwaysCompletes(); + + bool didInvokeOnSuccess = false; + bool didInvokeOnFailure = false; + + string validJoinCode = "test"; + Action onSuccess = () => { didInvokeOnSuccess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.GetAllocationFromJoinCode(joinCode: validJoinCode, onSuccess: onSuccess, onFailure: onFailure); + + Assert.That(didInvokeOnSuccess, Is.True); + Assert.That(didInvokeOnFailure, Is.False); + } + + [Test] + public void GetRelayRegions_FaultedTask_InvokesOnFailure() + { + _relayManager.RelayServiceSDK = new TaskAlwaysFaults(); + + bool didInvokeOnSucess = false; + bool didInvokeOnFailure = false; + Action> onSuccess = (regions) => { didInvokeOnSucess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.GetRelayRegions(onSuccess: onSuccess, onFailure: onFailure); + + LogAssert.Expect(LogType.Error, new Regex(@"Unable to retrieve the list of Relay regions, encountered an error:")); + Assert.That(didInvokeOnSucess, Is.False); + Assert.That(didInvokeOnFailure, Is.True); + } + + [Test] + public void GetRelayRegions_CompletedTask_InvokesOnSuccess() + { + _relayManager.RelayServiceSDK = new TaskAlwaysCompletes(); + + bool didInvokeOnSucess = false; + bool didInvokeOnFailure = false; + Action> onSuccess = (regions) => { didInvokeOnSucess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.GetRelayRegions(onSuccess: onSuccess, onFailure: onFailure); + + Assert.That(didInvokeOnSucess, Is.True); + Assert.That(didInvokeOnFailure, Is.False); + } + + [Test] + public void AllocateRelayServer_FaultedTask_InvokesOnFailure() + { + _relayManager.RelayServiceSDK = new TaskAlwaysFaults(); + + bool didInvokeOnSuccess = false; + bool didInvokeOnFailure = false; + + int validMaxPlayers = 8; + string validRegionId = "test"; + Action onSuccess = (code) => { didInvokeOnSuccess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.AllocateRelayServer(maxPlayers: validMaxPlayers, regionId: validRegionId, onSuccess: onSuccess, onFailure: onFailure); + + LogAssert.Expect(LogType.Error, new Regex(@"Unable to allocate Relay server, encountered an error creating a Relay allocation:")); + Assert.That(_relayManager.ServerAllocation, Is.Null); + Assert.That(didInvokeOnSuccess, Is.False); + Assert.That(didInvokeOnFailure, Is.True); + } + + [Test] + public void AllocateRelayServer_CompletedTask_InvokesOnSuccess() + { + _relayManager.RelayServiceSDK = new TaskAlwaysCompletes(); + + bool didInvokeOnSuccess = false; + bool didInvokeOnFailure = false; + + int validMaxPlayers = 8; + string validRegionId = "test"; + Action onSuccess = (code) => { didInvokeOnSuccess = true; }; + Action onFailure = () => { didInvokeOnFailure = true; }; + _relayManager.AllocateRelayServer(maxPlayers: validMaxPlayers, regionId: validRegionId, onSuccess: onSuccess, onFailure: onFailure); + + Assert.That(_relayManager.ServerAllocation, Is.Not.Null); + Assert.That(didInvokeOnSuccess, Is.True); + Assert.That(didInvokeOnFailure, Is.False); + } + + private class TaskAlwaysCompletes : IRelayServiceSDK + { + public Task CreateAllocationAsync(int maxConnections, string region = null) + { + byte[] resultKey = new byte[4]; + byte[] resultConnectionData = new byte[4]; + byte[] resultAllocationIdBytes = new byte[4]; + Guid resultAllocationId = new Guid(); + List resultEndpointList = new List(); + string localHostIp = "127.0.0.1"; + ushort samplePort = 12345; + RelayServer resultRelayServer = new RelayServer(ipV4: localHostIp, port: samplePort); + return Task.FromResult( + result: new Allocation( + allocationId: resultAllocationId, + serverEndpoints: resultEndpointList, + relayServer: resultRelayServer, + key: resultKey, + connectionData: resultConnectionData, + allocationIdBytes: resultAllocationIdBytes, + region: region + ) + ); + } + + public Task GetJoinCodeAsync(Guid allocationId) + { + string validJoinCode = "test"; + return Task.FromResult(validJoinCode); + } + + public Task JoinAllocationAsync(string joinCode) + { + Guid joinAllocationAllocationId = new Guid(); + List joinAllocationEndpointList = new List(); + string localHostIp = "127.0.0.1"; + ushort samplePort = 12345; + byte[] joinAllocationKey = new byte[4]; + byte[] joinAllocationHostConnectionData = new byte[4]; + byte[] joinAllocationConnectionData = new byte[4]; + string joinAllocationRegion = string.Empty; + byte[] joinAllocationAllocationIdBytes = new byte[4]; + RelayServer joinAllocationRelayServer = new RelayServer(ipV4: localHostIp, port: samplePort); + return Task.FromResult( + result: new JoinAllocation( + allocationId: joinAllocationAllocationId, + serverEndpoints: joinAllocationEndpointList, + relayServer: joinAllocationRelayServer, + key: joinAllocationKey, + hostConnectionData: joinAllocationHostConnectionData, + connectionData: joinAllocationConnectionData, + region: joinAllocationRegion, + allocationIdBytes: joinAllocationAllocationIdBytes + ) + ); + } + + public Task> ListRegionsAsync() + { + List regionList = new List(); + Region validRegion = new Region(id: "valid-region", description: "test"); + regionList.Add(validRegion); + return Task.FromResult>( + result: regionList + ); + } + } + + private class TaskAlwaysFaults : IRelayServiceSDK + { + public Task CreateAllocationAsync(int maxConnections, string region = null) + { + return Task.FromException(new Exception("Task faulted!")); + } + + public Task GetJoinCodeAsync(Guid allocationId) + { + return Task.FromException(new Exception("Task faulted!")); + } + + public Task JoinAllocationAsync(string joinCode) + { + return Task.FromException(new Exception("Task faulted!")); + } + + public Task> ListRegionsAsync() + { + return Task.FromException>(new Exception("Task faulted!")); + } + } + } +} diff --git a/Assets/UTPTransport/Tests/RelayManagerTests.cs.meta b/Assets/UTPTransport/Tests/RelayManagerTests.cs.meta new file mode 100644 index 0000000..76939ac --- /dev/null +++ b/Assets/UTPTransport/Tests/RelayManagerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fbe7efef4c6ea429dbb7716f8aeaa316 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/RelayUtilsTest.cs b/Assets/UTPTransport/Tests/RelayUtilsTest.cs new file mode 100644 index 0000000..4a84576 --- /dev/null +++ b/Assets/UTPTransport/Tests/RelayUtilsTest.cs @@ -0,0 +1,280 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Services.Relay.Models; +using Unity.Networking.Transport.Relay; +using Utp; +using System.Collections.Generic; + +namespace Utp +{ + public class RelayUtilsTest + { + + //Test connection types + static RelayServerEndpoint.NetworkOptions[] connectionTypes = new RelayServerEndpoint.NetworkOptions[] + { + RelayServerEndpoint.NetworkOptions.Udp, + RelayServerEndpoint.NetworkOptions.Tcp + }; + + #region HostRelayData + + /// + /// Tests the HostRelayData call inside RelayUtils. + /// + /// Udp/Tcp connection option. + [Test] + public void HostRelayData_NormalConnectionState_ReturnsRelayServerData([ValueSource(nameof(connectionTypes))]RelayServerEndpoint.NetworkOptions connectionType) + { + //Get connection type as string + string connectionTypeString = "invalid"; + if (connectionType == RelayServerEndpoint.NetworkOptions.Udp || connectionType == RelayServerEndpoint.NetworkOptions.Tcp) + { + connectionTypeString = connectionType == RelayServerEndpoint.NetworkOptions.Udp ? "udp" : "tcp"; + } + + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + serverEndpoints.Add(new RelayServerEndpoint(connectionTypeString, connectionType, false, false, "0.0.0.0", 00000)); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + + Allocation allocation = new Allocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region + ); + + //Assert data against null/default data + RelayServerData data = RelayUtils.HostRelayData(allocation, connectionType); + Assert.AreNotEqual(data, default(RelayServerData)); + } + + /// + /// Tests that null allocation data throws a null exception inside HostRelayData. + /// + /// Udp/Tcp connection option. + [Test] + public void HostRelayData_WithNullAllocation_ThrowsNullReferenceException([ValueSource(nameof(connectionTypes))] RelayServerEndpoint.NetworkOptions connectionType) + { + //Create null allocation & Assert null + Allocation allocation = null; + + Assert.Throws(() => + { + RelayServerData data = RelayUtils.HostRelayData(allocation, connectionType); + }); + } + + /// + /// Tests that a bad connection type in HostRelayData will throw an argument exception. + /// + [Test] + public void HostRelayData_WithInvalidConnectionType_ThrowsArgumentException() + { + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + serverEndpoints.Add(new RelayServerEndpoint("invalidConnType", (RelayServerEndpoint.NetworkOptions)9, false, false, "0.0.0.0", 00000)); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + + Allocation allocation = new Allocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region + ); + + //Assert exception thrown + Assert.Throws(() => + { + RelayServerData data = RelayUtils.HostRelayData(allocation, (RelayServerEndpoint.NetworkOptions)99); + }); + } + + /// + /// Tests that the endpoints list inside HostRelayData's allocation must have entries. + /// + /// Udp/Tcp connection option. + [Test] + public void HostRelayData_WithEmptyEndpointsInAllocation_ThrowsArgumentException([ValueSource(nameof(connectionTypes))] RelayServerEndpoint.NetworkOptions connectionType) + { + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + + Allocation allocation = new Allocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region + ); + + //Assert exception thrown + Assert.Throws(() => + { + RelayServerData data = RelayUtils.HostRelayData(allocation, connectionType); + }); + } + + #endregion + + #region PlayerRelayData + + /// + /// Tests the PlayerRelayData call inside RelayUtils. + /// + /// Udp/Tcp connection option. + [Test] + public void PlayerRelayData_NormalConnectionState_ReturnsRelayServerData([ValueSource(nameof(connectionTypes))] RelayServerEndpoint.NetworkOptions connectionType) + { + //Get connection type as string + string connectionTypeString = "invalid"; + if (connectionType == RelayServerEndpoint.NetworkOptions.Udp || connectionType == RelayServerEndpoint.NetworkOptions.Tcp) + { + connectionTypeString = connectionType == RelayServerEndpoint.NetworkOptions.Udp ? "udp" : "tcp"; + } + + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + serverEndpoints.Add(new RelayServerEndpoint(connectionTypeString, connectionType, false, false, "0.0.0.0", 00000)); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + byte[] hostConnectionData = new byte[16]; + + JoinAllocation allocation = new JoinAllocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region, + hostConnectionData + ); + + //Assert data against null/default data + RelayServerData data = RelayUtils.PlayerRelayData(allocation, connectionType); + Assert.AreNotEqual(data, default(RelayServerData)); + } + + /// + /// Tests that null allocation data throws a null exception inside PlayerRelayData. + /// + /// Udp/Tcp connection option. + [Test] + public void PlayerRelayData_WithNullAllocation_ThrowsNullReferenceException([ValueSource(nameof(connectionTypes))] RelayServerEndpoint.NetworkOptions connectionType) + { + //Create null allocation & Assert null + JoinAllocation allocation = null; + + Assert.Throws(() => + { + RelayServerData data = RelayUtils.PlayerRelayData(allocation, connectionType); + }); + } + + /// + /// Tests that a bad connection type in HostRelayData will throw an argument exception. + /// + [Test] + public void PlayerRelayData_WithInvalidConnectionType_ThrowsArgumentException() + { + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + serverEndpoints.Add(new RelayServerEndpoint("invalidConnType", (RelayServerEndpoint.NetworkOptions)99, false, false, "0.0.0.0", 00000)); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + byte[] hostConnectionData = new byte[16]; + + JoinAllocation allocation = new JoinAllocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region, + hostConnectionData + ); + + //Assert exception thrown + Assert.Throws(() => + { + RelayServerData data = RelayUtils.PlayerRelayData(allocation, (RelayServerEndpoint.NetworkOptions)99); + }); + } + + /// + /// Tests that the endpoints list inside PlayerRelayData's allocation must have entries. + /// + /// Udp/Tcp connection option. + [Test] + public void PlayerRelayData_WithEmptyEndpointsInAllocation_ThrowsArgumentException([ValueSource(nameof(connectionTypes))] RelayServerEndpoint.NetworkOptions connectionType) + { + //Create dummy data to inject into temporary relay allocation + RelayServer relayServer = new RelayServer("0.0.0.0", 0000); + System.Guid allocationId = new System.Guid("00000000-0000-0000-0000-000000000000"); + List serverEndpoints = new List(); + byte[] key = new byte[16]; + byte[] connectionData = new byte[16]; + byte[] allocationIdBytes = new byte[16]; + string region = string.Empty; + byte[] hostConnectionData = new byte[16]; + + JoinAllocation allocation = new JoinAllocation( + allocationId, + serverEndpoints, + relayServer, + key, + connectionData, + allocationIdBytes, + region, + hostConnectionData + ); + + //Assert exception thrown + Assert.Throws(() => + { + RelayServerData data = RelayUtils.PlayerRelayData(allocation, connectionType); + }); + } + + #endregion + + } + +} \ No newline at end of file diff --git a/Assets/UTPTransport/Tests/RelayUtilsTest.cs.meta b/Assets/UTPTransport/Tests/RelayUtilsTest.cs.meta new file mode 100644 index 0000000..3809d01 --- /dev/null +++ b/Assets/UTPTransport/Tests/RelayUtilsTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80d3219cc6ab93648add3777d0c76201 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/UTPTests.asmdef b/Assets/UTPTransport/Tests/UTPTests.asmdef new file mode 100644 index 0000000..5f3fc6e --- /dev/null +++ b/Assets/UTPTransport/Tests/UTPTests.asmdef @@ -0,0 +1,14 @@ +{ + "name": "UTPTests", + "references": [ + "Mirror", + "UtpTransport", + "Unity.Services.Relay", + "Unity.Services.Core", + "Unity.Services.Authentication", + "Unity.Networking.Transport" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/Assets/UTPTransport/Tests/UTPTests.asmdef.meta b/Assets/UTPTransport/Tests/UTPTests.asmdef.meta new file mode 100644 index 0000000..34370ef --- /dev/null +++ b/Assets/UTPTransport/Tests/UTPTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3eb65614fbb0e652f83b012284c808c4 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/UtpClientTests.cs b/Assets/UTPTransport/Tests/UtpClientTests.cs new file mode 100644 index 0000000..0b05e46 --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpClientTests.cs @@ -0,0 +1,109 @@ +using NUnit.Framework; +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Utp +{ + public class UtpClientTests + { + private UtpServer _server; + private UtpClient _client; + + [SetUp] + public void SetUp() + { + _server = new UtpServer(timeoutInMilliseconds: 1000); + _client = new UtpClient(timeoutInMilliseconds: 1000); + } + + [TearDown] + public void TearDown() + { + _client.Disconnect(); + _server.Stop(); + } + + [Test] + public void IsConnected_ClientIsNotConnectedToServer_ReturnsFalse() + { + Assert.That(_client.IsConnected(), Is.False); + } + + [Test] + public void IsConnected_ClientTriesToConnectToNonExistentServer_ReturnsFalse() + { + _client.Connect("localhost", 7777); + Assert.That(_client.IsConnected(), Is.False); + } + + [UnityTest] + public IEnumerator IsConnected_ClientIsConnectedToServer_ReturnsTrue() + { + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.That(_client.IsConnected(), Is.True); + } + + [UnityTest] + public IEnumerator OnConnected_ClientConnectsToServer_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _client.OnConnected += () => { callbackWasInvoked = true; }; + + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.That(callbackWasInvoked, Is.True); + } + + [UnityTest] + public IEnumerator OnDisconnected_ClientDisconnectsFromServer_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _client.OnDisconnected += () => { callbackWasInvoked = true; }; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + _client.Disconnect(); + yield return new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.That(callbackWasInvoked, Is.True); + } + + [UnityTest] + public IEnumerator OnReceivedData_ServerSendsDataToClient_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _client.OnReceivedData += (segment) => { callbackWasInvoked = true; }; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + int clientConnectionId = 1; + var dummyDataToSend = new ArraySegment(new byte[4]); + int validChannelId = 1; + _server.Send(connectionId: clientConnectionId, segment: dummyDataToSend, channelId: validChannelId); + yield return tickClientAndServerForSeconds(_client, _server, 5f); + + Assert.That(callbackWasInvoked, Is.True); + } + + private IEnumerator tickClientAndServerForSeconds(UtpClient client, UtpServer server, float numSeconds) + { + float elapsedTime = 0f; + while (elapsedTime < numSeconds) + { + client.Tick(); + server.Tick(); + yield return null; + elapsedTime += Time.deltaTime; + } + } + } +} diff --git a/Assets/UTPTransport/Tests/UtpClientTests.cs.meta b/Assets/UTPTransport/Tests/UtpClientTests.cs.meta new file mode 100644 index 0000000..d34e110 --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpClientTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62b2afeeafe82034984c1b79e5ea0b2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/UtpServerTests.cs b/Assets/UTPTransport/Tests/UtpServerTests.cs new file mode 100644 index 0000000..ab7ca1c --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpServerTests.cs @@ -0,0 +1,170 @@ +using NUnit.Framework; +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Utp +{ + public class UtpServerTests + { + private UtpServer _server; + private UtpClient _client; + + [SetUp] + public void SetUp() + { + _server = new UtpServer(timeoutInMilliseconds: 1000); + _client = new UtpClient(timeoutInMilliseconds: 1000); + } + + [TearDown] + public void TearDown() + { + _client.Disconnect(); + _server.Stop(); + } + + [Test] + public void IsActive_ServerWasNotStarted_ReturnsFalse() + { + Assert.IsFalse(_server.IsActive(), "Server is active without being started."); + } + + [Test] + public void IsActive_ServerWasStarted_ReturnsTrue() + { + _server.Start(7777); + + Assert.IsTrue(_server.IsActive(), "Server did not start and is not active."); + } + + [Test] + public void GetClientAddress_ConnectionIdOfNonExistentClient_ReturnsEmptyString() + { + int connectionIdOfNonExistentClient = 1; + + string clientAddress = _server.GetClientAddress(connectionIdOfNonExistentClient); + + Assert.IsEmpty(clientAddress, "Client address was not empty."); + } + + [UnityTest] + public IEnumerator GetClientAddress_ConnectionIdOfConnectedClient_ReturnsClientAddress() + { + int idOfConnectedClient = 1; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + string clientAddress = _server.GetClientAddress(idOfConnectedClient); + + Assert.IsNotEmpty(clientAddress, "Client address was empty, indicating the client is probably not connected."); + } + + [Test] + public void Disconnect_ConnectionIdOfNonExistentClient_LogsWarning() + { + int connectionIdOfNonExistentClient = 1; + + _server.Disconnect(connectionId: connectionIdOfNonExistentClient); + + LogAssert.Expect(LogType.Warning, "Connection not found: 1"); + } + + [UnityTest] + public IEnumerator Disconnect_ConnectionIdOfConnectedClient_ClientIsDisconnected() + { + int connectionIdOfConnectedClient = 1; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + _server.Disconnect(connectionId: connectionIdOfConnectedClient); + yield return new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 10f); + + Assert.IsFalse(_client.IsConnected(), "Client was not successfully disconnected from server"); + } + + [Test] + public void FindConnection_ConnectionIdOfNonExistentClient_ReturnsDefaultNetworkConnection() + { + int connectionIdOfNonExistentClient = 1; + + Unity.Networking.Transport.NetworkConnection connection = _server.FindConnection(connectionIdOfNonExistentClient); + + Assert.That(connection, Is.EqualTo(default(Unity.Networking.Transport.NetworkConnection))); + } + + [UnityTest] + public IEnumerator FindConnection_ConnectionIdOfConnectedClient_ReturnsNetworkConnection() + { + int connectionIdOfConnectedClient = 1; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Unity.Networking.Transport.NetworkConnection connection = _server.FindConnection(connectionIdOfConnectedClient); + + Assert.That(connection, Is.Not.EqualTo(default(Unity.Networking.Transport.NetworkConnection))); + } + + [UnityTest] + public IEnumerator OnConnected_ClientConnectsToServer_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _server.OnConnected += (int connectionId) => { callbackWasInvoked = true; }; + _server.Start(7777); + + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.That(callbackWasInvoked, Is.True); + } + + [UnityTest] + public IEnumerator OnDisconnect_ServerDisconnectsClient_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _server.OnDisconnected += (int connectionId) => { callbackWasInvoked = true; }; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + int idOfConnectedClient = 1; + _server.Disconnect(idOfConnectedClient); + yield return new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.That(callbackWasInvoked, Is.True); + } + + [UnityTest] + public IEnumerator OnReceivedData_ClientSendsDataToServer_CallbackIsInvoked() + { + bool callbackWasInvoked = false; + _server.OnReceivedData += (connectionId, segment) => { callbackWasInvoked = true; }; + _server.Start(7777); + _client.Connect("localhost", 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + int validChannelId = 1; + var dummyDataToSend = new ArraySegment(new byte[4]); + _client.Send(segment: dummyDataToSend, channelId: validChannelId); + yield return tickClientAndServerForSeconds(client: _client, server: _server, numSeconds: 5f); + + Assert.That(callbackWasInvoked, Is.True); + } + + private IEnumerator tickClientAndServerForSeconds(UtpClient client, UtpServer server, float numSeconds) + { + float elapsedTime = 0f; + while (elapsedTime < numSeconds) + { + client.Tick(); + server.Tick(); + yield return null; + elapsedTime += Time.deltaTime; + } + } + } +} diff --git a/Assets/UTPTransport/Tests/UtpServerTests.cs.meta b/Assets/UTPTransport/Tests/UtpServerTests.cs.meta new file mode 100644 index 0000000..af11d59 --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpServerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca6a46d66e881ae1bbc31d9b6754357c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/UtpTransportTests.cs b/Assets/UTPTransport/Tests/UtpTransportTests.cs new file mode 100644 index 0000000..c8eed79 --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpTransportTests.cs @@ -0,0 +1,110 @@ +using NUnit.Framework; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Utp +{ + public class UtpTransportTests + { + private UtpTransport _server; + private UtpTransport _client; + + [SetUp] + public void SetUp() + { + var serverObj = new GameObject(); + _server = serverObj.AddComponent(); + + var clientObj = new GameObject(); + _client = clientObj.AddComponent(); + } + + [TearDown] + public void TearDown() + { + _client.ClientDisconnect(); + GameObject.Destroy(_client.gameObject); + + _server.ServerStop(); + GameObject.Destroy(_server.gameObject); + } + + [Test] + public void ServerActive_ServerIsNotActive_ReturnsFalse() + { + Assert.IsFalse(_server.ServerActive(), "Server is running, but should not be."); + } + + [Test] + public void ServerActive_ServerIsActive_ReturnsTrue() + { + _server.ServerStart(); + + Assert.IsTrue(_server.ServerActive(), "Server is not running, but should be."); + } + + [Test] + public void ServerStop_ServerIsActive_ServerIsNoLongerActive() + { + _server.ServerStart(); + + _server.ServerStop(); + + Assert.IsFalse(_server.ServerActive(), "Server is running, but should not be."); + } + + [Test] + public void ServerGetClientAddress_ConnectionIdOfNonExistentClient_ReturnsEmptyString() + { + int connectionIdForNonExistentClient = 1; + + string clientAddress = _server.ServerGetClientAddress(connectionId: connectionIdForNonExistentClient); + + Assert.IsEmpty(clientAddress, "A client address was returned instead of an empty string."); + } + + [UnityTest] + public IEnumerator ServerGetClientAddress_ConnectionIdOfConnectedClient_ReturnsClientAddress() + { + int connectionIdForConnectedClient = 1; + _server.ServerStart(); + _client.ClientConnect(_server.ServerUri()); + yield return new WaitForTransportToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + string clientAddress = _server.ServerGetClientAddress(connectionId: connectionIdForConnectedClient); + + Assert.IsNotEmpty(clientAddress, "A client address was not returned, connection possibly timed out.."); + } + + [UnityTest] + public IEnumerator ServerGetClientAddress_ConnectionIdOfDisconnectedClient_ReturnsEmptyString() + { + int connectionIdOfDisconnectedClient = 1; + _server.ServerStart(); + _client.ClientConnect(_server.ServerUri()); + yield return new WaitForTransportToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + _client.ClientDisconnect(); + + string clientAddress = _server.ServerGetClientAddress(connectionId: connectionIdOfDisconnectedClient); + + Assert.IsNotEmpty(clientAddress, "A client address was not returned, connection possibly timed out.."); + } + + [Test] + public void ClientConnected_ClientIsNotConnectedToServer_ReturnsFalse() + { + Assert.IsFalse(_client.ClientConnected(), "Client is connected, but should not be."); + } + + [UnityTest] + public IEnumerator ClientConnected_ClientIsConnectedToServer_ReturnsTrue() + { + _server.ServerStart(); + _client.ClientConnect(_server.ServerUri()); + yield return new WaitForTransportToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + Assert.IsTrue(_client.ClientConnected(), "Client is not connected, but should be."); + } + } +} diff --git a/Assets/UTPTransport/Tests/UtpTransportTests.cs.meta b/Assets/UTPTransport/Tests/UtpTransportTests.cs.meta new file mode 100644 index 0000000..b72af54 --- /dev/null +++ b/Assets/UTPTransport/Tests/UtpTransportTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9cdd61b1ca0aaa1998bb786f462d9eaa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs new file mode 100644 index 0000000..ada41d5 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs @@ -0,0 +1,60 @@ +using System.Collections; +using UnityEngine; + +namespace Utp +{ + internal class WaitForClientAndServerToConnect : IEnumerator + { + public enum Status + { + Undetermined, + ClientConnected, + TimedOut, + } + + public Status Result { get; private set; } = Status.Undetermined; + + public object Current => null; + + private float _elapsedTime = 0f; + private float _timeout = 0f; + + private UtpClient _client = null; + private UtpServer _server = null; + + public WaitForClientAndServerToConnect(UtpClient client, UtpServer server, float timeoutInSeconds) + { + _client = client; + _server = server; + _timeout = timeoutInSeconds; + } + + public bool MoveNext() + { + _client.Tick(); + _server.Tick(); + + _elapsedTime += Time.deltaTime; + + if (_elapsedTime >= _timeout) + { + Result = Status.TimedOut; + return false; + } + else if (_client.IsConnected()) + { + Result = Status.ClientConnected; + return false; + } + + return true; + } + + public void Reset() + { + _elapsedTime = 0f; + _timeout = 0f; + Result = Status.Undetermined; + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs.meta b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs.meta new file mode 100644 index 0000000..47e6fe3 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5597dd24bc475894293e2a3e9d1c5669 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs new file mode 100644 index 0000000..167fbf6 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using System.Collections; +using UnityEngine.TestTools; + +namespace Utp +{ + public class WaitForClientAndServerToConnectTests + { + private UtpServer _server; + private UtpClient _client; + + [SetUp] + public void SetUp() + { + _server = new UtpServer(timeoutInMilliseconds: 1000); + _client = new UtpClient(timeoutInMilliseconds: 1000); + } + + [TearDown] + public void TearDown() + { + _client.Disconnect(); + _server.Stop(); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientDoesNotConnectToServer_ResultIsTimedOut() + { + var waitForConnection = new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 5f); + + yield return waitForConnection; + + Assert.That(waitForConnection.Result, Is.EqualTo(WaitForClientAndServerToConnect.Status.TimedOut)); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientConnectsToServer_ResultIsClientConnected() + { + var waitForConnection = new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + _server.Start(port: 7777); + _client.Connect(address: "localhost", port: 7777); + yield return waitForConnection; + + Assert.That(waitForConnection.Result, Is.EqualTo(WaitForClientAndServerToConnect.Status.ClientConnected)); + } + } +} diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs.meta b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs.meta new file mode 100644 index 0000000..3016732 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToConnectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2c9a7c72fbcae54ea82ce66d46ff6b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs new file mode 100644 index 0000000..c86348d --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs @@ -0,0 +1,60 @@ +using System.Collections; +using UnityEngine; + +namespace Utp +{ + internal class WaitForClientAndServerToDisconnect : IEnumerator + { + public enum Status + { + Undetermined, + ClientDisconnected, + TimedOut, + } + + public Status Result { get; private set; } = Status.Undetermined; + + public object Current => null; + + private float _elapsedTime = 0f; + private float _timeout = 0f; + + private UtpClient _client = null; + private UtpServer _server = null; + + public WaitForClientAndServerToDisconnect(UtpClient client, UtpServer server, float timeoutInSeconds) + { + _client = client; + _server = server; + _timeout = timeoutInSeconds; + } + + public bool MoveNext() + { + _client.Tick(); + _server.Tick(); + + _elapsedTime += Time.deltaTime; + + if (_elapsedTime >= _timeout) + { + Result = Status.TimedOut; + return false; + } + else if (!_client.IsConnected()) + { + Result = Status.ClientDisconnected; + return false; + } + + return true; + } + + public void Reset() + { + _elapsedTime = 0f; + _timeout = 0f; + Result = Status.Undetermined; + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs.meta b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs.meta new file mode 100644 index 0000000..1c0b884 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b49e0f4a1a1667428043ebc441a7802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs new file mode 100644 index 0000000..5284eea --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using System.Collections; +using UnityEngine.TestTools; + +namespace Utp +{ + public class WaitForClientAndServerToDisconnectTests + { + private UtpServer _server; + private UtpClient _client; + + [SetUp] + public void SetUp() + { + _server = new UtpServer(timeoutInMilliseconds: 1000); + _client = new UtpClient(timeoutInMilliseconds: 1000); + } + + [TearDown] + public void TearDown() + { + _client.Disconnect(); + _server.Stop(); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientIsNotConnectedToServer_ResultIsClientDisconnected() + { + var waitForDisconnect = new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 5f); + + yield return waitForDisconnect; + + Assert.That(waitForDisconnect.Result, Is.EqualTo(WaitForClientAndServerToDisconnect.Status.ClientDisconnected)); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientRemainsConnectedToTheServer_ResultIsTimedOut() + { + var waitForDisconnect = new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 5f); + _server.Start(port: 7777); + _client.Connect(address: "localhost", port: 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + yield return waitForDisconnect; + + Assert.That(waitForDisconnect.Result, Is.EqualTo(WaitForClientAndServerToDisconnect.Status.TimedOut)); + } + + [UnityTest] + public IEnumerator IEnumerator_ServerDisconnectsClient_ResultIsClientDisconnected() + { + var waitForDisconnect = new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 30f); + _server.Start(port: 7777); + _client.Connect(address: "localhost", port: 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + _server.Disconnect(connectionId: 1); + yield return waitForDisconnect; + + Assert.That(waitForDisconnect.Result, Is.EqualTo(WaitForClientAndServerToDisconnect.Status.ClientDisconnected)); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientDisconnectsFromServer_ResultIsClientDisconnected() + { + var waitForDisconnect = new WaitForClientAndServerToDisconnect(client: _client, server: _server, timeoutInSeconds: 30f); + _server.Start(port: 7777); + _client.Connect(address: "localhost", port: 7777); + yield return new WaitForClientAndServerToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + + _client.Disconnect(); + yield return waitForDisconnect; + + Assert.That(waitForDisconnect.Result, Is.EqualTo(WaitForClientAndServerToDisconnect.Status.ClientDisconnected)); + } + } +} diff --git a/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs.meta b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs.meta new file mode 100644 index 0000000..326fd55 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForClientAndServerToDisconnectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6a9d832f32aba64aa8d888c04550606 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs b/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs new file mode 100644 index 0000000..3b2c7b2 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs @@ -0,0 +1,60 @@ +using System.Collections; +using UnityEngine; + +namespace Utp +{ + internal class WaitForTransportToConnect : IEnumerator + { + public enum Status + { + Undetermined, + ClientConnected, + TimedOut, + } + + public Status Result { get; private set; } = Status.Undetermined; + + public object Current => null; + + private float _elapsedTime = 0f; + private float _timeout = 0f; + + private UtpTransport _client = null; + private UtpTransport _server = null; + + public WaitForTransportToConnect(UtpTransport client, UtpTransport server, float timeoutInSeconds) + { + _client = client; + _server = server; + _timeout = timeoutInSeconds; + } + + public bool MoveNext() + { + _client.ClientEarlyUpdate(); + _server.ServerEarlyUpdate(); + + _elapsedTime += Time.deltaTime; + + if (_elapsedTime >= _timeout) + { + Result = Status.TimedOut; + return false; + } + else if (_client.ClientConnected()) + { + Result = Status.ClientConnected; + return false; + } + + return true; + } + + public void Reset() + { + _elapsedTime = 0f; + _timeout = 0f; + Result = Status.Undetermined; + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs.meta b/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs.meta new file mode 100644 index 0000000..0d6ac79 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForTransportToConnect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abac8cf85ba76ae4ea3228f295d5c817 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs b/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs new file mode 100644 index 0000000..293aa53 --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs @@ -0,0 +1,55 @@ +using NUnit.Framework; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Utp +{ + public class WaitForTransportToConnectTests + { + private UtpTransport _server; + private UtpTransport _client; + + [SetUp] + public void SetUp() + { + var ServerObj = new GameObject(); + _server = ServerObj.AddComponent(); + + var ClientObj = new GameObject(); + _client = ClientObj.AddComponent(); + } + + [TearDown] + public void TearDown() + { + _client.ClientDisconnect(); + GameObject.Destroy(_client.gameObject); + + _server.ServerStop(); + GameObject.Destroy(_server.gameObject); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientIsNotConnected_ResultIsTimedOut() + { + var waitForConnection = new WaitForTransportToConnect(client: _client, server: _server, timeoutInSeconds: 5f); + + yield return waitForConnection; + + Assert.That(waitForConnection.Result, Is.EqualTo(WaitForTransportToConnect.Status.TimedOut)); + } + + [UnityTest] + public IEnumerator IEnumerator_ClientConnectsToServer_ResultIsClientConnected() + { + var waitForConnection = new WaitForTransportToConnect(client: _client, server: _server, timeoutInSeconds: 30f); + _server.ServerStart(); + _client.ClientConnect(_server.ServerUri()); + + yield return waitForConnection; + + Assert.That(waitForConnection.Result, Is.EqualTo(WaitForTransportToConnect.Status.ClientConnected)); + } + } +} diff --git a/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs.meta b/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs.meta new file mode 100644 index 0000000..5afab2b --- /dev/null +++ b/Assets/UTPTransport/Tests/WaitForTransportToConnectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78e0b17ba6753b644a328ce529254c1d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp.meta b/Assets/UTPTransport/Utp.meta new file mode 100644 index 0000000..24a0c2d --- /dev/null +++ b/Assets/UTPTransport/Utp.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 808ac3e8ea3c6e84cbe28303c140a0f8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp/UtpClient.cs b/Assets/UTPTransport/Utp/UtpClient.cs new file mode 100644 index 0000000..cdbcfd0 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpClient.cs @@ -0,0 +1,523 @@ +using Mirror; +using System; +using Unity.Collections; +using Unity.Jobs; +using Unity.Networking.Transport; +using Unity.Networking.Transport.Relay; +using Unity.Services.Relay.Models; +using Unity.Burst; +using Unity.Collections.LowLevel.Unsafe; + +namespace Utp +{ + #region Jobs + + [BurstCompile] + struct ClientUpdateJob : IJob + { + /// + /// Used to bind, listen, and send data to connections. + /// + public NetworkDriver driver; + + /// + /// client connections to this server. + /// + public Unity.Networking.Transport.NetworkConnection connection; + + /// + /// Temporary storage for connection events that occur on job threads so they may be dequeued on the main thread. + /// + public NativeQueue.ParallelWriter connectionEventsQueue; + + /// + /// Process all incoming events/messages on this connection. + /// + public void Execute() + { + //Back out if connection is invalid + if (!connection.IsCreated) + { + return; + } + + NetworkEvent.Type netEvent; + while ((netEvent = connection.PopEvent(driver, out DataStreamReader stream)) != NetworkEvent.Type.Empty) + { + //Create new event + UtpConnectionEvent connectionEvent = default(UtpConnectionEvent); + + switch (netEvent) + { + //Connect event + case (NetworkEvent.Type.Connect): + { + + connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnConnected, + connectionId = connection.GetHashCode() + }; + + //Queue event + connectionEventsQueue.Enqueue(connectionEvent); + + break; + } + + //Data recieved event + case (NetworkEvent.Type.Data): + { + //Create managed array of data + NativeArray nativeMessage = new NativeArray(stream.Length, Allocator.Temp); + + //Read data from stream + stream.ReadBytes(nativeMessage); + + connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnReceivedData, + connectionId = connection.GetHashCode(), + eventData = GetFixedList(nativeMessage) + }; + + //Queue event + connectionEventsQueue.Enqueue(connectionEvent); + + break; + } + + //Disconnect event + case (NetworkEvent.Type.Disconnect): + { + connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnDisconnected, + connectionId = connection.GetHashCode() + }; + + //Queue event + connectionEventsQueue.Enqueue(connectionEvent); + + break; + } + + } + } + } + + public FixedList4096Bytes GetFixedList(NativeArray data) + { + FixedList4096Bytes retVal = new FixedList4096Bytes(); + + if (data.Length > 0) + { + unsafe + { + retVal.AddRange(NativeArrayUnsafeUtility.GetUnsafePtr(data), data.Length); + } + } + + return retVal; + } + } + + [BurstCompile] + struct ClientSendJob : IJob + { + /// + /// Used to bind, listen, and send data to connections. + /// + public NetworkDriver driver; + + /// + /// The network pipeline to stream data. + /// + public NetworkPipeline pipeline; + + /// + /// The client's network connection instance. + /// + public Unity.Networking.Transport.NetworkConnection connection; + + /// + /// The segment of data to send over (deallocates after use). + /// + [DeallocateOnJobCompletion] + public NativeArray data; + + public void Execute() + { + //Back out if connection is invalid + if (!connection.IsCreated) + { + return; + } + + DataStreamWriter writer; + int writeStatus = driver.BeginSend(pipeline, connection, out writer); + + //If endpoint was success, write data to stream + if (writeStatus == (int)Unity.Networking.Transport.Error.StatusCode.Success) + { + writer.WriteBytes(data); + driver.EndSend(writer); + } + } + } + + #endregion + + /// + /// A client for Mirror using UTP. + /// + public class UtpClient : UtpEntity + { + /// + /// Invokes when connected to a server. + /// + public Action OnConnected; + + /// + /// Invokes when data has been received. + /// + public Action> OnReceivedData; + + /// + /// Invokes when disconnected from a server. + /// + public Action OnDisconnected; + + /// + /// Used alongside a driver to connect, send, and receive data from a listen server. + /// + private Unity.Networking.Transport.NetworkConnection connection; + + /// + /// The number of pipelines tracked in the header size array. + /// + private const int NUM_PIPELINES = 2; + + /// + /// The driver's max header size for UTP transport. + /// + private int[] driverMaxHeaderSize = new int[NUM_PIPELINES]; + + /// + /// Whether the client is connected to the server or not. + /// + private bool isConnected; + + /// + /// Constructor for UTP client. + /// + /// The response timeout in miliseconds. + public UtpClient(int timeoutInMilliseconds) + { + this.timeoutInMilliseconds = timeoutInMilliseconds; + } + + /// + /// Constructor for UTP client. + /// + /// Action that is invoked when connected. + /// Action that is invoked when receiving data. + /// Action that is invoked when disconnected. + /// The response timeout in miliseconds. + public UtpClient(Action OnConnected, Action> OnReceivedData, Action OnDisconnected, int timeoutInMilliseconds) + : this(timeoutInMilliseconds) + { + this.OnConnected = OnConnected; + this.OnReceivedData = OnReceivedData; + this.OnDisconnected = OnDisconnected; + } + + /// + /// Attempt to connect to a listen server at a given IP/port. Currently only supports IPV4. + /// + /// The host address at which the listen server is running. + /// The port which the listen server is listening on. + public void Connect(string address, ushort port) + { + if (IsConnected()) + { + UtpLog.Warning($"Abandoning connection attempt, this client is already connected to a server."); + return; + } + + if (string.IsNullOrEmpty(address)) + { + UtpLog.Error("Abandoning connection attempt, a null or empty address was provided."); + return; + } + + if (address == "localhost") + { + address = "127.0.0.1"; + } + + // TODO: Support for IPv6. + NetworkEndPoint endpoint; + if (!NetworkEndPoint.TryParse(address, port, out endpoint)) + { + UtpLog.Error($"Abandoning connection attempt, failed to convert {address}:{port} into a valid NetworkEndpoint."); + return; + } + + connectionsEventsQueue = new NativeQueue(Allocator.Persistent); + + var settings = new NetworkSettings(); + settings.WithNetworkConfigParameters(disconnectTimeoutMS: timeoutInMilliseconds); + + driver = NetworkDriver.Create(settings); + reliablePipeline = driver.CreatePipeline(typeof(ReliableSequencedPipelineStage)); + unreliablePipeline = driver.CreatePipeline(typeof(UnreliableSequencedPipelineStage)); + + connection = driver.Connect(endpoint); + + if (IsValidConnection(connection)) + { + UtpLog.Info($"Client connected to the server at {address}:{port}."); + } + else + { + UtpLog.Error($"Client failed to connect to the server at {address}:{port}."); + } + } + + /// + /// Attempt to connect to a Relay host given a join allocation. + /// + /// + public void RelayConnect(JoinAllocation joinAllocation) + { + if (IsConnected()) + { + UtpLog.Warning($"Abandoning connection attempt, this client is already connected to a server."); + return; + } + + connectionsEventsQueue = new NativeQueue(Allocator.Persistent); + + RelayServerData relayServerData = RelayUtils.PlayerRelayData(joinAllocation, RelayServerEndpoint.NetworkOptions.Udp); + + var networkSettings = new NetworkSettings(); + networkSettings.WithRelayParameters(ref relayServerData); + + driver = NetworkDriver.Create(networkSettings); + reliablePipeline = driver.CreatePipeline(typeof(ReliableSequencedPipelineStage)); + unreliablePipeline = driver.CreatePipeline(typeof(UnreliableSequencedPipelineStage)); + + connection = driver.Connect(relayServerData.Endpoint); + + if (IsValidConnection(connection)) + { + UtpLog.Info($"Client connected to the Relay server at {relayServerData.Endpoint.Address}:{relayServerData.Endpoint.Port}."); + } + else + { + UtpLog.Error($"Client failed to connect to the Relay server at {relayServerData.Endpoint.Address}:{relayServerData.Endpoint.Port}."); + } + } + + /// + /// Disconnect from a listen server. + /// + public void Disconnect() + { + jobHandle.Complete(); + + //If there is an existing connection, force a disconnect + if (connection.IsCreated) + { + UtpLog.Info("Client disconnecting from server"); + + //Disconnect from server + connection.Disconnect(driver); + connection = default(Unity.Networking.Transport.NetworkConnection); + + //We need to ensure the driver has the opportunity to send a disconnect event to the server + driver.ScheduleUpdate().Complete(); + + //Invoke disconnect action + OnDisconnected?.Invoke(); + } + + //Flush the event queue + if (connectionsEventsQueue.IsCreated) + { + ProcessIncomingEvents(); + connectionsEventsQueue.Dispose(); + } + + //Dispose of existing network driver + if (driver.IsCreated) + { + driver.Dispose(); + driver = default(NetworkDriver); + } + } + + /// + /// Tick the client, creating the client job and scheduling it. Processes incoming events + /// + public void Tick() + { + // First complete the job that was initialized in the previous frame + jobHandle.Complete(); + + // Trigger Mirror callbacks for events that resulted in the last jobs work + ProcessIncomingEvents(); + + //Cache driver & connection info + cacheConnectionInfo(); + + // Need to ensure the driver did not become inactive + if (!IsNetworkDriverInitialized()) + { + driverMaxHeaderSize = new int[NUM_PIPELINES]; + return; + } + + // Create a new job + var job = new ClientUpdateJob + { + driver = driver, + connection = connection, + connectionEventsQueue = connectionsEventsQueue.AsParallelWriter() + }; + + // Schedule job + jobHandle = driver.ScheduleUpdate(); + jobHandle = job.Schedule(jobHandle); + } + + /// + /// Send data to the listen server over a particular channel. + /// + /// The data to send. + /// The 'Mirror.Channels' channel to send the data over. + public void Send(ArraySegment segment, int channelId) + { + //Get pipeline for job + NetworkPipeline pipeline = channelId == Channels.Reliable ? reliablePipeline : unreliablePipeline; + + //Convert ArraySegment to NativeArray for burst compile + NativeArray segmentArray = new NativeArray(segment.Count, Allocator.Persistent); + NativeArray.Copy(segment.Array, segment.Offset, segmentArray, 0, segment.Count); + + // Create a new job + var job = new ClientSendJob + { + driver = driver, + pipeline = pipeline, + connection = connection, + data = segmentArray + }; + + // Schedule job + jobHandle = job.Schedule(jobHandle); + } + + /// + /// Processes connection events from the queue. + /// + public void ProcessIncomingEvents() + { + // Exit if the driver is not active or connection isn't ready + if (!IsNetworkDriverInitialized() || !connection.IsCreated) + { + return; + } + + //Process event queue + while (connectionsEventsQueue.IsCreated && connectionsEventsQueue.TryDequeue(out UtpConnectionEvent connectionEvent)) + { + switch (connectionEvent.eventType) + { + //Connect action + case ((byte)UtpConnectionEventType.OnConnected): + { + OnConnected?.Invoke(); + break; + } + + //Receive data action + case ((byte)UtpConnectionEventType.OnReceivedData): + { + OnReceivedData?.Invoke(new ArraySegment(connectionEvent.eventData.ToArray())); + break; + } + + //Disconnect action + case ((byte)UtpConnectionEventType.OnDisconnected): + { + OnDisconnected?.Invoke(); + break; + } + + //Invalid action + default: + { + UtpLog.Warning($"Invalid connection event: {connectionEvent.eventType}"); + break; + } + + } + } + } + + /// + /// Returns this client's driver's max header size based on the requested channel. + /// + /// The channel to check. + /// This client's max header size. + public int GetMaxHeaderSize(int channelId = Channels.Reliable) + { + if (IsConnected() && IsNetworkDriverInitialized()) + { + return driverMaxHeaderSize[channelId]; + } + + return 0; + } + + /// + /// Whether or not the client is connected to a server. + /// + /// True if connected to a server, false otherwise. + public bool IsConnected() + { + return isConnected; + } + + /// + /// Caches important properties to allow for getter methods to be called without interfering with the job system. + /// + private void cacheConnectionInfo() + { + //Check for an active connection from this client + if (IsValidConnection(connection)) + { + bool isInitialized = IsNetworkDriverInitialized(); + + //If driver is active, cache its max header size for UTP transport + if (isInitialized) + { + driverMaxHeaderSize[Channels.Reliable] = driver.MaxHeaderSize(reliablePipeline); + driverMaxHeaderSize[Channels.Unreliable] = driver.MaxHeaderSize(unreliablePipeline); + } + + //Set connection state + isConnected = isInitialized && connection.GetState(driver) == Unity.Networking.Transport.NetworkConnection.State.Connected; + } + else + { + //If there is no valid connection, set values accordingly + driverMaxHeaderSize[Channels.Reliable] = 0; + driverMaxHeaderSize[Channels.Unreliable] = 0; + isConnected = false; + } + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Utp/UtpClient.cs.meta b/Assets/UTPTransport/Utp/UtpClient.cs.meta new file mode 100644 index 0000000..945d8e4 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4920709ed36995f4da41358c3d9b1bb8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp/UtpConnectionEvents.cs b/Assets/UTPTransport/Utp/UtpConnectionEvents.cs new file mode 100644 index 0000000..5f49bcf --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpConnectionEvents.cs @@ -0,0 +1,35 @@ +using Unity.Collections; + +namespace Utp +{ + /// + /// Connection events seen on a connection. + /// + public enum UtpConnectionEventType + { + OnConnected, + OnReceivedData, + OnDisconnected + } + + /// + /// Struct to store events and the related data for that event. + /// + public struct UtpConnectionEvent + { + /// + /// The event type. + /// + public byte eventType; + + /// + /// Event data, only used for OnReceived event. + /// + public FixedList4096Bytes eventData; + + /// + /// The connection ID of the connection corresponding to this event. + /// + public int connectionId; + } +} diff --git a/Assets/UTPTransport/Utp/UtpConnectionEvents.cs.meta b/Assets/UTPTransport/Utp/UtpConnectionEvents.cs.meta new file mode 100644 index 0000000..d8e828d --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpConnectionEvents.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a25d7544ca7545240b011913e19001aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp/UtpEntity.cs b/Assets/UTPTransport/Utp/UtpEntity.cs new file mode 100644 index 0000000..a8be8f0 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpEntity.cs @@ -0,0 +1,61 @@ +using Unity.Collections; +using Unity.Jobs; +using Unity.Networking.Transport; + +namespace Utp +{ + /// + /// A server or client inside Utp. + /// + public abstract class UtpEntity + { + /// + /// Temporary storage for connection events that occur on job threads so they may be dequeued on the main thread. + /// + protected NativeQueue connectionsEventsQueue; + + /// + /// Used alongside a connection to connect, send, and receive data from a listen server. + /// + protected NetworkDriver driver; + + /// + /// A pipeline on the driver that is sequenced, and ensures messages are delivered. + /// + protected NetworkPipeline reliablePipeline; + + /// + /// A pipeline on the driver that is sequenced, but does not ensure messages are delivered. + /// + protected NetworkPipeline unreliablePipeline; + + /// + /// Job handle to schedule jobs. + /// + protected JobHandle jobHandle; + + /// + /// Timeout(ms) to be set on drivers. + /// + protected int timeoutInMilliseconds; + + /// + /// Returns whether a connection is a valid one. Checks against default connection object. + /// + /// The connection to validate. + /// True or false, whether the connection is valid. + public bool IsValidConnection(NetworkConnection connection) + { + return connection.IsCreated; + } + + /// + /// Determine whether the NetworkDriver has been initialized. + /// + /// True if initialized, false otherwise. + public bool IsNetworkDriverInitialized() + { + return driver.IsCreated; + } + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Utp/UtpEntity.cs.meta b/Assets/UTPTransport/Utp/UtpEntity.cs.meta new file mode 100644 index 0000000..5dbfa8f --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpEntity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec774ce582bc26247a29b15cc7f0e0ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp/UtpLog.cs b/Assets/UTPTransport/Utp/UtpLog.cs new file mode 100644 index 0000000..89000c5 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpLog.cs @@ -0,0 +1,28 @@ +using UnityEngine; +using System; + +namespace Utp +{ + /// + /// Different levels of log sensitivity. + /// + public enum LogLevel : int + { + Off, + Error, + Warning, + Info, + Verbose + } + + /// + /// The logging class for UTP activity. + /// + public static class UtpLog + { + public static Action Verbose = Debug.Log; + public static Action Info = Debug.Log; + public static Action Warning = Debug.LogWarning; + public static Action Error = Debug.LogError; + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/Utp/UtpLog.cs.meta b/Assets/UTPTransport/Utp/UtpLog.cs.meta new file mode 100644 index 0000000..738a3a9 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpLog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9879fd29fbc4a4e439ccde8bef4344fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/Utp/UtpServer.cs b/Assets/UTPTransport/Utp/UtpServer.cs new file mode 100644 index 0000000..d3b0a39 --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpServer.cs @@ -0,0 +1,616 @@ +using Mirror; +using System; +using UnityEngine; +using Unity.Collections; +using Unity.Jobs; +using Unity.Networking.Transport; +using Unity.Networking.Transport.Relay; +using Unity.Services.Relay.Models; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Burst; + +namespace Utp +{ + + #region Jobs + + /// + /// Job used to update connections. + /// + [BurstCompile] + struct ServerUpdateConnectionsJob : IJob + { + /// + /// Used to bind, listen, and send data to connections. + /// + public NetworkDriver driver; + + /// + /// Client connections to this server. + /// + public NativeList connections; + + /// + /// Temporary storage for connection events that occur on job threads so they may be dequeued on the main thread. + /// + public NativeQueue.ParallelWriter connectionsEventsQueue; + + public void Execute() + { + //Iterate through connections list + for (int i = 0; i < connections.Length; i++) + { + //If a connection is no longer established, remove it + if (driver.GetConnectionState(connections[i]) == Unity.Networking.Transport.NetworkConnection.State.Disconnected) + { + connections.RemoveAtSwapBack(i--); + } + } + + // Accept new connections + Unity.Networking.Transport.NetworkConnection networkConnection; + while ((networkConnection = driver.Accept()) != default(Unity.Networking.Transport.NetworkConnection)) + { + //Set up connection event + UtpConnectionEvent connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnConnected, + connectionId = networkConnection.GetHashCode() + }; + + //Queue connection event + connectionsEventsQueue.Enqueue(connectionEvent); + + //Add connection to connection list + connections.Add(networkConnection); + } + } + + /// + /// Disconnect and remove a connection via it's ID. + /// + /// The ID of the connection to disconnect. + public void Disconnect(int connectionId) + { + foreach (Unity.Networking.Transport.NetworkConnection connection in connections) + { + if (connection.GetHashCode() == connectionId) + { + connection.Disconnect(driver); + + //Set up connection event + UtpConnectionEvent connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnDisconnected, + connectionId = connection.GetHashCode() + }; + + //Queue connection event + connectionsEventsQueue.Enqueue(connectionEvent); + + return; + } + } + } + } + + /// + /// Job to query incoming events for all connections. + /// + [BurstCompile] + struct ServerUpdateJob : IJobParallelForDefer + { + /// + /// Used to bind, listen, and send data to connections. + /// + public NetworkDriver.Concurrent driver; + + /// + /// client connections to this server. + /// + public NativeArray connections; + + /// + /// Temporary storage for connection events that occur on job threads so they may be dequeued on the main thread. + /// + public NativeQueue.ParallelWriter connectionsEventsQueue; + + /// + /// Process all incoming events/messages on this connection. + /// + /// The current index being accessed in the array. + public void Execute(int index) + { + NetworkEvent.Type netEvent; + while ((netEvent = driver.PopEventForConnection(connections[index], out DataStreamReader stream)) != NetworkEvent.Type.Empty) + { + if (netEvent == NetworkEvent.Type.Data) + { + NativeArray nativeMessage = new NativeArray(stream.Length, Allocator.Temp); + stream.ReadBytes(nativeMessage); + + //Set up connection event + UtpConnectionEvent connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnReceivedData, + eventData = GetFixedList(nativeMessage), + connectionId = connections[index].GetHashCode() + }; + + //Queue connection event + connectionsEventsQueue.Enqueue(connectionEvent); + } + else if (netEvent == NetworkEvent.Type.Disconnect) + { + //Set up disconnect event + UtpConnectionEvent connectionEvent = new UtpConnectionEvent() + { + eventType = (byte)UtpConnectionEventType.OnDisconnected, + connectionId = connections[index].GetHashCode() + }; + + //Queue disconnect event + connectionsEventsQueue.Enqueue(connectionEvent); + } + } + } + + /// + /// Convert unmanaged native array to 4096 Byte list. Uses unsafe code. + /// + /// The data to convert. + /// An unmanaged fixed list of data. + public FixedList4096Bytes GetFixedList(NativeArray data) + { + FixedList4096Bytes retVal = new FixedList4096Bytes(); + + if (data.Length > 0) + { + unsafe + { + retVal.AddRange(NativeArrayUnsafeUtility.GetUnsafePtr(data), data.Length); + } + } + + return retVal; + } + } + + [BurstCompile] + struct ServerSendJob : IJob + { + /// + /// Used to bind, listen, and send data to connections. + /// + public NetworkDriver driver; + + /// + /// The network pipeline to stream data. + /// + public NetworkPipeline pipeline; + + /// + /// The client's network connection instance. + /// + public Unity.Networking.Transport.NetworkConnection connection; + + /// + /// The segment of data to send over (deallocates after use). + /// + [DeallocateOnJobCompletion] + public NativeArray data; + + public void Execute() + { + DataStreamWriter writer; + int writeStatus = driver.BeginSend(pipeline, connection, out writer); + + //If Acquire was success + if (writeStatus == (int)Unity.Networking.Transport.Error.StatusCode.Success) + { + writer.WriteBytes(data); + driver.EndSend(writer); + } + } + } + + #endregion + + /// + /// A listen server for Mirror using UTP. + /// + public class UtpServer : UtpEntity + { + /// + /// Invokes when a client has connected to the server. + /// + public Action OnConnected; + + /// + /// Invokes when data has been received by a third party. + /// + public Action> OnReceivedData; + + /// + /// Invokes when a client has disconnected. + /// + public Action OnDisconnected; + + /// + /// Client connections to this server. + /// + private NativeList connections; + + /// + /// The number of pipelines tracked in the header size array. + /// + private const int NUM_PIPELINES = 2; + + /// + /// The driver's max header size for UTP transport. + /// + private int[] driverMaxHeaderSize = new int[NUM_PIPELINES]; + + /// + /// Constructor for UTP server. + /// + /// The response timeout in miliseconds. + public UtpServer(int timeoutInMilliseconds) + { + this.timeoutInMilliseconds = timeoutInMilliseconds; + } + + /// + /// Constructor for UTP server. + /// + /// Action that is invoked when connected. + /// Action that is invoked when receiving data. + /// Action that is invoked when disconnected. + /// The response timeout in miliseconds. + public UtpServer(Action OnConnected, Action> OnReceivedData, Action OnDisconnected, int timeoutInMilliseconds) + : this(timeoutInMilliseconds) + { + this.OnConnected = OnConnected; + this.OnReceivedData = OnReceivedData; + this.OnDisconnected = OnDisconnected; + } + + /// + /// Initialize the server. Currently only supports IPV4. + /// + /// The port to listen for connections on. + /// Whether or not to use start a server using Unity's Relay Service. + /// The Relay allocation, if using Relay. + public void Start(ushort port, bool useRelay = false, Allocation allocation = null) + { + if (IsNetworkDriverInitialized()) + { + UtpLog.Warning("Attempting to start a server that is already active."); + return; + } + + //Instantiate network settings + var settings = new NetworkSettings(); + settings.WithNetworkConfigParameters(disconnectTimeoutMS: timeoutInMilliseconds); + + //Create IPV4 endpoint + NetworkEndPoint endpoint = NetworkEndPoint.AnyIpv4; + endpoint.Port = port; + + if (useRelay) + { + //Instantiate relay network data + RelayServerData relayServerData = RelayUtils.HostRelayData(allocation, RelayServerEndpoint.NetworkOptions.Udp); + RelayNetworkParameter relayNetworkParameter = new RelayNetworkParameter { ServerData = relayServerData }; + NetworkSettings networkSettings = new NetworkSettings(); + + //Initialize relay network + RelayParameterExtensions.WithRelayParameters(ref networkSettings, ref relayServerData); + + //Instantiate network driver + driver = NetworkDriver.Create(networkSettings); + } + else + { + //Initialize network settings + NetworkSettings networkSettings = new NetworkSettings(); + + //Instantiate network driver + driver = NetworkDriver.Create(networkSettings); + endpoint.Port = port; + } + + //Initialize connections list & event queue + connections = new NativeList(16, Allocator.Persistent); + connectionsEventsQueue = new NativeQueue(Allocator.Persistent); + + //Create network pipelines + reliablePipeline = driver.CreatePipeline(typeof(ReliableSequencedPipelineStage)); + unreliablePipeline = driver.CreatePipeline(typeof(UnreliableSequencedPipelineStage)); + + int bindReturnCode = driver.Bind(endpoint); + if (!driver.Bound) + { + UtpLog.Error($"Unable to start server, failed to bind the specified port {endpoint.Port}. {nameof(NetworkDriver.Bind)}() returned {bindReturnCode}."); + return; + } + + int listenReturnCode = driver.Listen(); + if (!driver.Listening) + { + UtpLog.Error($"Unable to start server, failed to listen. {nameof(NetworkDriver.Listen)} returned {listenReturnCode}."); + return; + } + + UtpLog.Info(useRelay ? ("Relay server started") : ($"Server started on port: {endpoint.Port}")); + } + + /// + /// Tick the server, creating the server jobs and scheduling them. Processes events created by the jobs. + /// + public void Tick() + { + //If the network driver has shut down, back out + if (!IsNetworkDriverInitialized()) + { + return; + } + + // First complete the job that was initialized in the previous frame + jobHandle.Complete(); + + // Trigger Mirror callbacks for events that resulted in the last jobs work + ProcessIncomingEvents(); + + //Cache driver & connection info + cacheConnectionInfo(); + + // Create a new jobs + var serverUpdateJob = new ServerUpdateJob + { + driver = driver.ToConcurrent(), + connections = connections.AsDeferredJobArray(), + connectionsEventsQueue = connectionsEventsQueue.AsParallelWriter() + }; + + var connectionJob = new ServerUpdateConnectionsJob + { + driver = driver, + connections = connections, + connectionsEventsQueue = connectionsEventsQueue.AsParallelWriter() + }; + + // Schedule jobs + jobHandle = driver.ScheduleUpdate(); + + // We are explicitly scheduling ServerUpdateJob before ServerUpdateConnectionsJob so that disconnect events are enqueued before the corresponding NetworkConnection is removed + jobHandle = serverUpdateJob.Schedule(connections, 1, jobHandle); + jobHandle = connectionJob.Schedule(jobHandle); + } + + /// + /// Stop a running server. + /// + public void Stop() + { + UtpLog.Info("Stopping server"); + + jobHandle.Complete(); + + //Dispose of event queue + if (connectionsEventsQueue.IsCreated) + { + connectionsEventsQueue.Dispose(); + } + + //Dispose of connections + if (connections.IsCreated) + { + connections.Dispose(); + } + + //Dispose of driver + if (driver.IsCreated) + { + driver.Dispose(); + driver = default(NetworkDriver); + } + } + + /// + /// Disconnect and remove a connection via it's ID. + /// + /// The ID of the connection to disconnect. + public void Disconnect(int connectionId) + { + jobHandle.Complete(); + + //Continue if connection was found + if (TryGetConnection(connectionId, out Unity.Networking.Transport.NetworkConnection connection)) + { + UtpLog.Info($"Disconnecting connection with ID: {connectionId}"); + connection.Disconnect(driver); + + // When disconnecting, we need to ensure the driver has the opportunity to send a disconnect event to the client + driver.ScheduleUpdate().Complete(); + + //Invoke disconnect action + OnDisconnected?.Invoke(connectionId); + } + else + { + UtpLog.Warning($"Connection not found: {connectionId}"); + } + } + + /// + /// Send data to a connection over a particular channel. + /// + /// The ID of the connection to send data to. + /// The data to send. + /// The 'Mirror.Channels' channel to send the data over. + public void Send(int connectionId, ArraySegment segment, int channelId) + { + jobHandle.Complete(); + + //Continue if connection was found + if (TryGetConnection(connectionId, out Unity.Networking.Transport.NetworkConnection connection)) + { + //Get pipeline for job + NetworkPipeline pipeline = channelId == Channels.Reliable ? reliablePipeline : unreliablePipeline; + + //Convert ArraySegment to NativeArray for burst compile + NativeArray segmentArray = new NativeArray(segment.Count, Allocator.Persistent); + NativeArray.Copy(segment.Array, segment.Offset, segmentArray, 0, segment.Count); + + // Create a new job + var job = new ClientSendJob + { + driver = driver, + pipeline = pipeline, + connection = connection, + data = segmentArray + }; + + // Schedule job + jobHandle = job.Schedule(jobHandle); + } + } + + /// + /// Look up a client's address via it's ID. If using Relay, this will always return the address of the Relay server. + /// + /// The ID of the connection. + /// The client address, or Relay server if using Relay. + public string GetClientAddress(int connectionId) + { + //If a connection was found, get its address + if (TryGetConnection(connectionId, out Unity.Networking.Transport.NetworkConnection connection)) + { + NetworkEndPoint endpoint = driver.RemoteEndPoint(connection); + return endpoint.Address; + } + else + { + UtpLog.Warning($"Connection not found: {connectionId}"); + return String.Empty; + } + } + + public int GetMaxHeaderSize(int channelId = Channels.Reliable) + { + if (IsNetworkDriverInitialized()) + { + return driverMaxHeaderSize[channelId]; + } + + return 0; + } + + /// + /// Processes connection events from the queue. + /// + public void ProcessIncomingEvents() + { + //Check if the server is active + if (!IsNetworkDriverInitialized()) + { + return; + } + + //Process the events in the event list + UtpConnectionEvent connectionEvent; + while (connectionsEventsQueue.TryDequeue(out connectionEvent)) + { + switch (connectionEvent.eventType) + { + //Connect action + case ((byte)UtpConnectionEventType.OnConnected): + { + OnConnected?.Invoke(connectionEvent.connectionId); + break; + } + + //Receive data action + case ((byte)UtpConnectionEventType.OnReceivedData): + { + OnReceivedData?.Invoke(connectionEvent.connectionId, new ArraySegment(connectionEvent.eventData.ToArray())); + break; + } + + //Disconnect action + case ((byte)UtpConnectionEventType.OnDisconnected): + { + OnDisconnected?.Invoke(connectionEvent.connectionId); + break; + } + + //Invalid action + default: + { + UtpLog.Warning($"Invalid connection event: {connectionEvent.eventType}"); + break; + } + + } + } + } + + /// + /// Processes connection events from the queue. + /// + /// The ID of the connection to find. + /// The connection if found in the list, a default connection otherwise. + public Unity.Networking.Transport.NetworkConnection FindConnection(int connectionId) + { + jobHandle.Complete(); + + if (connections.IsCreated) + { + foreach (Unity.Networking.Transport.NetworkConnection connection in connections) + { + if (connection.GetHashCode() == connectionId) + { + return connection; + } + } + } + + return default(Unity.Networking.Transport.NetworkConnection); + } + + /// + /// Returns whether a connection is valid. + /// + /// The id of the connection to check. + /// Whether the connection is valid. + private bool TryGetConnection(int connectionId, out Unity.Networking.Transport.NetworkConnection connection) + { + connection = FindConnection(connectionId); + return connection.GetHashCode() == connectionId; + } + + /// + /// Determine whether the server is running or not. + /// + /// True if running, false otherwise. + public bool IsActive() + { + return IsNetworkDriverInitialized(); + } + + private void cacheConnectionInfo() + { + bool isInitialized = IsNetworkDriverInitialized(); + + //If driver is active, cache its max header size for UTP transport + if (isInitialized) + { + driverMaxHeaderSize[Channels.Reliable] = driver.MaxHeaderSize(reliablePipeline); + driverMaxHeaderSize[Channels.Unreliable] = driver.MaxHeaderSize(unreliablePipeline); + } + + } + } +} + diff --git a/Assets/UTPTransport/Utp/UtpServer.cs.meta b/Assets/UTPTransport/Utp/UtpServer.cs.meta new file mode 100644 index 0000000..590116b --- /dev/null +++ b/Assets/UTPTransport/Utp/UtpServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cff6392feda009b4588c6f927b3a4fd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/UtpTransport.asmdef b/Assets/UTPTransport/UtpTransport.asmdef new file mode 100644 index 0000000..524a2bd --- /dev/null +++ b/Assets/UTPTransport/UtpTransport.asmdef @@ -0,0 +1,21 @@ +{ + "name": "UtpTransport", + "rootNamespace": "", + "references": [ + "Mirror", + "Unity.Collections", + "Unity.Jobs", + "Unity.Services.Relay", + "Unity.Networking.Transport", + "Unity.Burst" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/UTPTransport/UtpTransport.asmdef.meta b/Assets/UTPTransport/UtpTransport.asmdef.meta new file mode 100644 index 0000000..23968ca --- /dev/null +++ b/Assets/UTPTransport/UtpTransport.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 007ed940a76231e4b9e88d212b846a54 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UTPTransport/UtpTransport.cs b/Assets/UTPTransport/UtpTransport.cs new file mode 100644 index 0000000..a562aa4 --- /dev/null +++ b/Assets/UTPTransport/UtpTransport.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using Mirror; +using UnityEngine; +using Unity.Networking.Transport; +using Unity.Services.Relay.Models; + +namespace Utp +{ + /// + /// Component that implements Mirror's Transport class, utilizing the Unity Transport Package (UTP). + /// + [DisallowMultipleComponent] + public class UtpTransport : Transport + { + /// + /// The scheme used by this transport. + /// + public const string Scheme = "udp"; + + [Header("Transport Configuration")] + + /// + /// The port at which to connect. + /// + public ushort Port = 7777; + + [Header("Debugging")] + + /// + /// The level of logging sensitivity. + /// + public LogLevel LoggerLevel = LogLevel.Info; + + [Header("Timeout in MS")] + + /// + /// The timeout for the Utp server, in milliseconds. + /// + public int TimeoutMS = 1000; + + /// + /// Whether to use Relay or not. + /// + // Relay toggle + public bool useRelay; + + /// + /// The UTP server object. + /// + private UtpServer server; + + /// + /// The UTP client object. + /// + private UtpClient client; + + /// + /// The Relay manager. + /// + private IRelayManager relayManager; + + /// + /// Calls when script is being loaded. + /// + private void Awake() + { + SetupDefaultCallbacks(); + + //Logging delegates + if (LoggerLevel < LogLevel.Verbose) UtpLog.Verbose = _ => { }; + if (LoggerLevel < LogLevel.Info) UtpLog.Info = _ => { }; + if (LoggerLevel < LogLevel.Warning) UtpLog.Warning = _ => { }; + if (LoggerLevel < LogLevel.Error) UtpLog.Error = _ => { }; + + //Instantiate new UTP server + server = new UtpServer( + (connectionId) => OnServerConnected.Invoke(connectionId), + (connectionId, message) => OnServerDataReceived.Invoke(connectionId, message, Channels.Reliable), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + TimeoutMS); + + //Instantiate new UTP client + client = new UtpClient( + () => OnClientConnected.Invoke(), + (message) => OnClientDataReceived.Invoke(message, Channels.Reliable), + () => OnClientDisconnected.Invoke(), + TimeoutMS); + + if (!TryGetComponent(out relayManager)) + { + //Add relay manager component + relayManager = gameObject.AddComponent(); + } + + UtpLog.Info("UTPTransport initialized!"); + } + + private void SetupDefaultCallbacks() + { + if (OnServerConnected == null) + { + OnServerConnected = (connId) => UtpLog.Warning("OnServerConnected called with no handler"); + } + if (OnServerDisconnected == null) + { + OnServerDisconnected = (connId) => UtpLog.Warning("OnServerDisconnected called with no handler"); + } + if (OnServerDataReceived == null) + { + OnServerDataReceived = (connId, data, channel) => UtpLog.Warning("OnServerDataReceived called with no handler"); + } + if (OnClientConnected == null) + { + OnClientConnected = () => UtpLog.Warning("OnClientConnected called with no handler"); + } + if (OnClientDisconnected == null) + { + OnClientDisconnected = () => UtpLog.Warning("OnClientDisconnected called with no handler"); + } + if (OnClientDataReceived == null) + { + OnClientDataReceived = (data, channel) => UtpLog.Warning("OnClientDataReceived called with no handler"); + } + } + + /// + /// Checks to see if UTP is available on this platform. + /// + /// If UTP is available on the current platform. + public override bool Available() + { + return Application.platform != RuntimePlatform.WebGLPlayer; + } + + /// + /// Connects client to a server address. + /// + /// The address to connect to. + public override void ClientConnect(string address) + { + // We entirely ignore the address that is passed when utilizing Relay + if (useRelay) + { + // The data we need to connect is embedded in the relayManager's JoinAllocation + client.RelayConnect(relayManager.JoinAllocation); + } + else + { + //Join normal IP with port + if (address.Contains(":")) + { + string[] hostAndPort = address.Split(':'); + client.Connect(hostAndPort[0], Convert.ToUInt16(hostAndPort[1])); + } + else + { + // fallback to default port + client.Connect(address, Port); + } + } + } + + #region Relay methods + + /// + /// Configures a new Relay client with a join code. + /// + /// The Relay join code. + /// A callback to invoke when the Relay allocation is successfully retrieved from the join code. + /// A callback to invoke when the Relay allocation is unsuccessfully retrieved from the join code. + public void ConfigureClientWithJoinCode(string joinCode, Action onSuccess, Action onFailure) + { + relayManager.GetAllocationFromJoinCode(joinCode, onSuccess, onFailure); + } + + /// + /// Gets region ID's from all the Relay regions (Only use if Relay is enabled). + /// + /// A callback to invoke when the list of regions is successfully retrieved. + /// A callback to invoke when the list of regions is unsuccessfully retrieved. + public void GetRelayRegions(Action> onSuccess, Action onFailure) + { + relayManager.GetRelayRegions(onSuccess, onFailure); + } + + /// + /// Allocates a new Relay server. + /// + /// The maximum player count. + /// The region ID. + /// A callback to invoke when the Relay server is successfully allocated. + /// A callback to invoke when the Relay server is unsuccessfully allocated. + public void AllocateRelayServer(int maxPlayers, string regionId, Action onSuccess, Action onFailure) + { + relayManager.AllocateRelayServer(maxPlayers, regionId, onSuccess, onFailure); + } + + /// + /// Returns the max packet size for any packet going over the network + /// + /// + /// + public override int GetMaxPacketSize(int channelId = Channels.Reliable) + { + //Check for client activity + if (client != null && client.IsConnected()) + { + return NetworkParameterConstants.MTU - client.GetMaxHeaderSize(channelId); + } + else if (server != null && server.IsActive()) + { + return NetworkParameterConstants.MTU - server.GetMaxHeaderSize(channelId); + } + else + { + //Fall back on default MTU + return NetworkParameterConstants.MTU; + } + } + + #endregion + + #region Client overrides + + public override bool ClientConnected() => client.IsConnected(); + public override void ClientDisconnect() => client.Disconnect(); + public override void ClientSend(ArraySegment segment, int channelId) => client.Send(segment, channelId); + + public override void ClientEarlyUpdate() + { + if (enabled) client.Tick(); + } + + #endregion + + #region Server overrides + + public override bool ServerActive() => server.IsActive(); + public override void ServerStart() + { + server.Start(Port, useRelay, relayManager.ServerAllocation); + } + + public override void ServerStop() => server.Stop(); + public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId); + public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId); + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) => server.Send(connectionId, segment, channelId); + + public override void ServerEarlyUpdate() + { + if (enabled) server.Tick(); + } + + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder(); + builder.Scheme = Scheme; + builder.Port = Port; + + return builder.Uri; + } + + #endregion + + #region Transport overrides + + public override void Shutdown() + { + if (client.IsConnected()) client.Disconnect(); + if (server.IsNetworkDriverInitialized()) server.Stop(); + } + + public override string ToString() => "UTP"; + + #endregion + } +} \ No newline at end of file diff --git a/Assets/UTPTransport/UtpTransport.cs.meta b/Assets/UTPTransport/UtpTransport.cs.meta new file mode 100644 index 0000000..a4dc3c9 --- /dev/null +++ b/Assets/UTPTransport/UtpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d4f9593ba7f845458730213887b5758 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..27ed399 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contribution Guidelines + +Thank you for your interest in contributing to the Unity Relay Mirror Adapter! + +## Communication + +Before starting on a project that you intend to contribute to the +Unity Relay Mirror Adapter, we +**strongly** recommend posting on our +[Issues page](https://github.com/unity-technologies/unity-relay-mirror-sample/issues) and +briefly outlining the changes you plan to make. This will enable us to provide +some context that may be helpful for you. This could range from advice and +feedback on how to optimally perform your changes or reasons for not doing it. + +If you're looking for input on what to contribute, feel free to reach +out to us directly and/or browse the GitHub issues with +the `Requests` or `Bug` label. + +## Git Branches + +The main branch corresponds to the most recent version of the project. + +When contributing to the project, please make sure that your Pull Request (PR) +contains the following: + +- Detailed description of the changes performed +- Corresponding changes to documentation and unit tests (if + applicable) +- Summary of the tests performed to validate your changes +- Issue numbers that the PR resolves (if any) + +## Contributor License Agreements + +When you open a pull request, you will be asked to acknolwedge our Contributor +License Agreement. We allow both individual contributions and contributions made +on behalf of companies. We use an open source tool called CLA assistant. If you +have any questions on our CLA, please +[submit an issue](https://github.com/unity-technologies/unity-relay-mirror-sample/issues). diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..87ef654 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,5 @@ +Relay Mirror Adapter copyright 2022 Unity Technologies. + +This software is subject to, and made available under, the terms of service for Unity Relay (see https://unity3d.com/legal/one-operate-services-terms-of-service), and is an "Operate Service" as defined therein. + +Your use of the Services constitutes your acceptance of such terms. Unless expressly provided otherwise, the software under this license is made available strictly on an AS IS BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the terms of service for details on these and other terms and conditions. diff --git a/Packages/manifest.json b/Packages/manifest.json new file mode 100644 index 0000000..ebd66c2 --- /dev/null +++ b/Packages/manifest.json @@ -0,0 +1,46 @@ +{ + "dependencies": { + "com.unity.collab-proxy": "1.17.2", + "com.unity.ide.rider": "3.0.15", + "com.unity.ide.visualstudio": "2.0.16", + "com.unity.ide.vscode": "1.2.5", + "com.unity.jobs": "0.70.0-preview.7", + "com.unity.services.relay": "1.0.4", + "com.unity.test-framework": "1.1.31", + "com.unity.textmeshpro": "3.0.6", + "com.unity.timeline": "1.4.8", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.2", + "com.unity.ugui": "1.0.0", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json new file mode 100644 index 0000000..06ed057 --- /dev/null +++ b/Packages/packages-lock.json @@ -0,0 +1,480 @@ +{ + "dependencies": { + "com.unity.burst": { + "version": "1.6.6", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collab-proxy": { + "version": "1.17.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.0.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collections": { + "version": "1.4.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.6.6", + "com.unity.nuget.mono-cecil": "1.11.4", + "com.unity.test-framework": "1.1.31" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ide.rider": { + "version": "3.0.15", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.16", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.5", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.jobs": { + "version": "0.70.0-preview.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.collections": "1.4.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.2.6", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.nuget.mono-cecil": { + "version": "1.11.4", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.0.2", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.services.authentication": { + "version": "2.0.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.services.core": "1.3.1", + "com.unity.modules.unitywebrequest": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.core": { + "version": "1.4.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.modules.androidjni": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.qos": { + "version": "1.0.2", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.4.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.services.authentication": "2.0.0", + "com.unity.collections": "1.2.4" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.relay": { + "version": "1.0.4", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.4.0", + "com.unity.services.authentication": "2.0.0", + "com.unity.services.qos": "1.0.2", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.transport": "1.1.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.sysroot": { + "version": "2.0.3", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.sysroot.linux-x86_64": { + "version": "2.0.2", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.sysroot": "2.0.3" + }, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.31", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "3.0.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.4.8", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.toolchain.win-x86_64-linux-x86_64": { + "version": "2.0.2", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.sysroot": "2.0.3", + "com.unity.sysroot.linux-x86_64": "2.0.2" + }, + "url": "https://packages.unity.com" + }, + "com.unity.transport": { + "version": "1.1.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.collections": "1.2.4", + "com.unity.burst": "1.6.6", + "com.unity.mathematics": "1.2.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.uielementsnative": "1.0.0" + } + }, + "com.unity.modules.uielementsnative": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/ProjectSettings/AudioManager.asset b/ProjectSettings/AudioManager.asset new file mode 100644 index 0000000..07ebfb0 --- /dev/null +++ b/ProjectSettings/AudioManager.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 1024 diff --git a/ProjectSettings/ClusterInputManager.asset b/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 0000000..e7886b2 --- /dev/null +++ b/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset new file mode 100644 index 0000000..cdc1f3e --- /dev/null +++ b/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0 + m_ClothInterCollisionStiffness: 0 + m_ContactsGeneration: 1 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 250, y: 250, z: 250} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_EnableUnifiedHeightmaps: 1 + m_DefaultMaxAngluarSpeed: 7 diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 0000000..93e7b16 --- /dev/null +++ b/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/Main.unity + guid: 9fc0d4010bbf28b4594072e72b8655ab + m_configObjects: {} diff --git a/ProjectSettings/EditorSettings.asset b/ProjectSettings/EditorSettings.asset new file mode 100644 index 0000000..1e44a0a --- /dev/null +++ b/ProjectSettings/EditorSettings.asset @@ -0,0 +1,30 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_ExternalVersionControlSupport: Visible Meta Files + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 0 + m_DefaultBehaviorMode: 0 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 0 + m_SpritePackerPaddingPower: 1 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref + m_ProjectGenerationRootNamespace: + m_CollabEditorSettings: + inProgressEnabled: 1 + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_AsyncShaderCompilation: 1 + m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptions: 3 + m_ShowLightmapResolutionOverlay: 1 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 0000000..43369e3 --- /dev/null +++ b/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,63 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_LegacyDeferred: + m_Mode: 1 + m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, + type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_LogWhenShaderIsCompiled: 0 + m_AllowEnlightenSupportForUpgradedProject: 0 diff --git a/ProjectSettings/InputManager.asset b/ProjectSettings/InputManager.asset new file mode 100644 index 0000000..17c8f53 --- /dev/null +++ b/ProjectSettings/InputManager.asset @@ -0,0 +1,295 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 diff --git a/ProjectSettings/NavMeshAreas.asset b/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 0000000..3b0b7c3 --- /dev/null +++ b/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,91 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..be4a797 --- /dev/null +++ b/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreviewPackages: 0 + m_EnablePackageDependencies: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Capabilities: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 0000000..47880b1 --- /dev/null +++ b/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_VelocityThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_AutoSimulation: 1 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_AlwaysShowColliders: 0 + m_ShowColliderSleep: 1 + m_ShowColliderContacts: 0 + m_ShowColliderAABB: 0 + m_ContactArrowScale: 0.2 + m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412} + m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432} + m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745} + m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804} + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..67a94da --- /dev/null +++ b/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset new file mode 100644 index 0000000..8ab233c --- /dev/null +++ b/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,693 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 22 + productGUID: 04da073b036b76b4bb13c79105e8aa24 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: Unity Technologies + productName: Unity Relay Mirror Sample + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 1 + m_ShowUnitySplashLogo: 1 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1920 + defaultScreenHeight: 1080 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 1 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + iosAllowHTTPDownload: 1 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 1 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 1 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + fullscreenMode: 1 + xboxSpeechDB: 0 + xboxEnableHeadOrientation: 0 + xboxEnableGuest: 0 + xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 0 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + m_SupportedAspectRatios: + 4:3: 1 + 5:4: 1 + 16:10: 1 + 16:9: 1 + Others: 1 + bundleVersion: 0.1 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + useHDRDisplay: 0 + D3DHDRBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: {} + buildNumber: + Standalone: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 0 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 19 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 1 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 1 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSTargetOSVersionString: 11.0 + tvOSSdkVersion: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 11.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} + tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] + tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] + tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] + tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] + iOSLaunchScreenType: 0 + iOSLaunchScreenPortrait: {fileID: 0} + iOSLaunchScreenLandscape: {fileID: 0} + iOSLaunchScreenBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreenFillPct: 100 + iOSLaunchScreenSize: 100 + iOSLaunchScreenCustomXibPath: + iOSLaunchScreeniPadType: 0 + iOSLaunchScreeniPadImage: {fileID: 0} + iOSLaunchScreeniPadBackgroundColor: + serializedVersion: 2 + rgba: 0 + iOSLaunchScreeniPadFillPct: 100 + iOSLaunchScreeniPadSize: 100 + iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: + iOSDeviceRequirements: [] + iOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + appleEnableAutomaticSigning: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: c0afd0d1d80e3634a9dac47e8a0426ea + templatePackageId: com.unity.template.3d@4.2.8 + templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 + AndroidSplashScreenScale: 0 + androidSplashScreen: {fileID: 0} + AndroidKeystoreName: + AndroidKeyaliasName: + AndroidBuildApkPerCpuArchitecture: 0 + AndroidTVCompatibility: 0 + AndroidIsGame: 1 + AndroidEnableTango: 0 + androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 + m_AndroidBanners: + - width: 320 + height: 180 + banner: {fileID: 0} + androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 + AndroidMinifyWithR8: 0 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 + m_BuildTargetIcons: [] + m_BuildTargetPlatformIcons: [] + m_BuildTargetBatching: + - m_BuildTarget: Standalone + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: tvOS + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: Android + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: iPhone + m_StaticBatching: 1 + m_DynamicBatching: 0 + - m_BuildTarget: WebGL + m_StaticBatching: 0 + m_DynamicBatching: 0 + m_BuildTargetGraphicsJobs: + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 1 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 1 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 1 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 1 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 1 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 1 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 1 + - m_BuildTarget: LuminSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: + - m_BuildTarget: PS4Player + m_GraphicsJobMode: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobMode: 0 + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: AndroidPlayer + m_APIs: 150000000b000000 + m_Automatic: 0 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + - m_BuildTarget: AppleTVSupport + m_APIs: 10000000 + m_Automatic: 1 + - m_BuildTarget: WebGLSupport + m_APIs: 0b000000 + m_Automatic: 1 + m_BuildTargetVRSettings: + - m_BuildTarget: Standalone + m_Enabled: 0 + m_Devices: + - Oculus + - OpenVR + openGLRequireES31: 0 + openGLRequireES31AEP: 0 + openGLRequireES32: 0 + m_TemplateCustomTags: {} + mobileMTRendering: + Android: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetNormalMapEncoding: [] + playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 + actionOnDotNetUnhandledException: 1 + enableInternalProfiler: 0 + logObjCUncaughtExceptions: 1 + enableCrashReportAPI: 0 + cameraUsageDescription: + locationUsageDescription: + microphoneUsageDescription: + bluetoothUsageDescription: + switchNMETAOverride: + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchUseGOLDLinker: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchTitleNames_12: + switchTitleNames_13: + switchTitleNames_14: + switchTitleNames_15: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchPublisherNames_12: + switchPublisherNames_13: + switchPublisherNames_14: + switchPublisherNames_15: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchIcons_12: {fileID: 0} + switchIcons_13: {fileID: 0} + switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchSmallIcons_12: {fileID: 0} + switchSmallIcons_13: {fileID: 0} + switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchTouchScreenUsage: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchRatingsInt_12: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 + switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 0 + switchSupportedNpadCount: 8 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchPlayerConnectionEnabled: 1 + switchUseNewStyleFilepaths: 0 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 + ps4NPAgeRating: 12 + ps4NPTitleSecret: + ps4NPTrophyPackPath: + ps4ParentalLevel: 11 + ps4ContentID: ED1633-NPXX51362_00-0000000000000000 + ps4Category: 0 + ps4MasterVersion: 01.00 + ps4AppVersion: 01.00 + ps4AppType: 0 + ps4ParamSfxPath: + ps4VideoOutPixelFormat: 0 + ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 + ps4VideoOutReprojectionRate: 60 + ps4PronunciationXMLPath: + ps4PronunciationSIGPath: + ps4BackgroundImagePath: + ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: + ps4SaveDataImagePath: + ps4SdkOverride: + ps4BGMPath: + ps4ShareFilePath: + ps4ShareOverlayImagePath: + ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: + ps4NPtitleDatPath: + ps4RemotePlayKeyAssignment: -1 + ps4RemotePlayKeyMappingDir: + ps4PlayTogetherPlayerCount: 0 + ps4EnterButtonAssignment: 1 + ps4ApplicationParam1: 0 + ps4ApplicationParam2: 0 + ps4ApplicationParam3: 0 + ps4ApplicationParam4: 0 + ps4DownloadDataSize: 0 + ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 + ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps4pnSessions: 1 + ps4pnPresence: 1 + ps4pnFriends: 1 + ps4pnGameCustomData: 1 + playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 + restrictedAudioUsageRights: 0 + ps4UseResolutionFallback: 0 + ps4ReprojectionSupport: 0 + ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 + ps4SocialScreenEnabled: 0 + ps4ScriptOptimizationLevel: 0 + ps4Audio3dVirtualSpeakerCount: 14 + ps4attribCpuUsage: 0 + ps4PatchPkgPath: + ps4PatchLatestPkgPath: + ps4PatchChangeinfoPath: + ps4PatchDayOne: 0 + ps4attribUserManagement: 0 + ps4attribMoveSupport: 0 + ps4attrib3DSupport: 0 + ps4attribShareSupport: 0 + ps4attribExclusiveVR: 0 + ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 + ps4attribEyeToEyeDistanceSettingVR: 0 + ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 + monoEnv: + splashScreenBackgroundSourceLandscape: {fileID: 0} + splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 + spritePackerPolicy: + webGLMemorySize: 16 + webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 + webGLDataCaching: 1 + webGLDebugSymbols: 0 + webGLEmscriptenArgs: + webGLModulesDirectory: + webGLTemplate: APPLICATION:Default + webGLAnalyzeBuildSize: 0 + webGLUseEmbeddedResources: 0 + webGLCompressionFormat: 1 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + scriptingDefineSymbols: + 1: MIRROR;MIRROR_1726_OR_NEWER;MIRROR_3_0_OR_NEWER;MIRROR_3_12_OR_NEWER;MIRROR_4_0_OR_NEWER;MIRROR_5_0_OR_NEWER;MIRROR_6_0_OR_NEWER;MIRROR_7_0_OR_NEWER;MIRROR_8_0_OR_NEWER;MIRROR_9_0_OR_NEWER;MIRROR_10_0_OR_NEWER;MIRROR_11_0_OR_NEWER;MIRROR_12_0_OR_NEWER;MIRROR_13_0_OR_NEWER;MIRROR_14_0_OR_NEWER;MIRROR_15_0_OR_NEWER;MIRROR_16_0_OR_NEWER;MIRROR_17_0_OR_NEWER;MIRROR_18_0_OR_NEWER;MIRROR_24_0_OR_NEWER;MIRROR_26_0_OR_NEWER;MIRROR_27_0_OR_NEWER;MIRROR_28_0_OR_NEWER;MIRROR_29_0_OR_NEWER;MIRROR_30_0_OR_NEWER;MIRROR_30_5_2_OR_NEWER;MIRROR_32_1_2_OR_NEWER;MIRROR_32_1_4_OR_NEWER;MIRROR_35_0_OR_NEWER;MIRROR_35_1_OR_NEWER;MIRROR_37_0_OR_NEWER;MIRROR_38_0_OR_NEWER;MIRROR_39_0_OR_NEWER;MIRROR_40_0_OR_NEWER;MIRROR_41_0_OR_NEWER;MIRROR_42_0_OR_NEWER;MIRROR_43_0_OR_NEWER;MIRROR_44_0_OR_NEWER;MIRROR_46_0_OR_NEWER;MIRROR_47_0_OR_NEWER;MIRROR_53_0_OR_NEWER;MIRROR_55_0_OR_NEWER;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: {} + il2cppCompilerConfiguration: {} + managedStrippingLevel: {} + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 + useReferenceAssemblies: 1 + enableRoslynAnalyzers: 1 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + assemblyVersionValidation: 1 + gcWBarrierValidation: 0 + apiCompatibilityLevelPerPlatform: {} + m_RenderingPath: 1 + m_MobileRenderingPath: 1 + metroPackageName: Template_3D + metroPackageVersion: + metroCertificatePath: + metroCertificatePassword: + metroCertificateSubject: + metroCertificateIssuer: + metroCertificateNotAfter: 0000000000000000 + metroApplicationDescription: Template_3D + wsaImages: {} + metroTileShortName: + metroTileShowName: 0 + metroMediumTileShowName: 0 + metroLargeTileShowName: 0 + metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 + metroDefaultTileSize: 1 + metroTileForegroundText: 2 + metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} + metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1} + metroSplashScreenUseBackgroundColor: 0 + platformCapabilities: {} + metroTargetDeviceFamilies: {} + metroFTAName: + metroFTAFileTypes: [] + metroProtocolName: + vcxProjDefaultLanguage: + XboxOneProductId: + XboxOneUpdateKey: + XboxOneSandboxId: + XboxOneContentId: + XboxOneTitleId: + XboxOneSCId: + XboxOneGameOsOverridePath: + XboxOnePackagingOverridePath: + XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 + XboxOnePackageEncryption: 0 + XboxOnePackageUpdateGranularity: 2 + XboxOneDescription: + XboxOneLanguage: + - enus + XboxOneCapability: [] + XboxOneGameRating: {} + XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 + XboxOneEnableGPUVariability: 1 + XboxOneSockets: {} + XboxOneSplashScreen: {fileID: 0} + XboxOneAllowedProductIds: [] + XboxOnePersistentLocalStorageSize: 0 + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} + cloudServicesEnabled: + UNet: 1 + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + cloudProjectId: 9134ca3b-e51a-4d98-ab4e-65149173f3e8 + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] + projectName: UnityMirrorSample + organizationId: wj_erik + cloudEnabled: 0 + legacyClampBlendShapeWeights: 0 + virtualTexturingSupportEnabled: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt new file mode 100644 index 0000000..dd7e0b0 --- /dev/null +++ b/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 2020.3.40f1 +m_EditorVersionWithRevision: 2020.3.40f1 (ba48d4efcef1) diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset new file mode 100644 index 0000000..2146c22 --- /dev/null +++ b/ProjectSettings/QualitySettings.asset @@ -0,0 +1,235 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 2 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 1 + textureQuality: 1 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.3 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 0 + lodBias: 0.4 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + vSyncCount: 1 + lodBias: 0.7 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 2 + textureQuality: 0 + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 1.5 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + - serializedVersion: 2 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + blendWeights: 4 + textureQuality: 0 + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + vSyncCount: 1 + lodBias: 2 + maximumLODLevel: 0 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + excludedTargetPlatforms: [] + m_PerPlatformDefaultQuality: + Android: 2 + Lumin: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Nintendo 3DS: 5 + Nintendo Switch: 5 + PS4: 5 + PS5: 5 + PSP2: 2 + Stadia: 5 + Standalone: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset new file mode 100644 index 0000000..1c92a78 --- /dev/null +++ b/ProjectSettings/TagManager.asset @@ -0,0 +1,43 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 2 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset new file mode 100644 index 0000000..558a017 --- /dev/null +++ b/ProjectSettings/TimeManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + Fixed Timestep: 0.02 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/ProjectSettings/UnityConnectSettings.asset b/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 0000000..6125b30 --- /dev/null +++ b/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_TestInitMode: 0 + CrashReportingSettings: + m_EventUrl: https://perf-events.cloud.unity3d.com + m_Enabled: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/ProjectSettings/VFXManager.asset b/ProjectSettings/VFXManager.asset new file mode 100644 index 0000000..3a95c98 --- /dev/null +++ b/ProjectSettings/VFXManager.asset @@ -0,0 +1,12 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 diff --git a/ProjectSettings/VersionControlSettings.asset b/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000..dca2881 --- /dev/null +++ b/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/ProjectSettings/XRSettings.asset b/ProjectSettings/XRSettings.asset new file mode 100644 index 0000000..482590c --- /dev/null +++ b/ProjectSettings/XRSettings.asset @@ -0,0 +1,10 @@ +{ + "m_SettingKeys": [ + "VR Device Disabled", + "VR Device User Alert" + ], + "m_SettingValues": [ + "False", + "False" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e793f1 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Unity Relay Mirror Sample +The Unity Relay Mirror Sample demonstrates how to use the [Unity Transport Package](https://docs.unity3d.com/Packages/com.unity.transport@latest), the [Unity Relay service](https://docs.unity.com/relay), and the [Mirror Networking library](https://mirror-networking.com/) together. + +* The Unity Transport Package is a low-level networking library that provides a connection-based abstraction layer over UDP sockets with optional functionality such as reliability, ordering, and fragmentation. +* Relay is a Unity service that facilitates securely connecting players by using a join code style workflow without the need for dedicated game servers or peer-to-peer communication. +* The Mirror Networking library is a high-level networking library for the Unity Platform. + +The [Unity Relay](https://docs.unity.com/relay) documentation contains additional information on the usage of this sample. +## Requirements +The sample has the following requirements: +* [Unity Editor version 2020.3.40f1](https://unity3d.com/unity/whats-new/2020.3.40) +* Unity services + * [Unity Authentication Service](https://docs.unity.com/authentication) + * [Unity Relay Service](https://docs.unity.com/relay) +* Unity packages + * [Unity Relay](https://docs.unity3d.com/Packages/com.unity.services.relay@latest) + * [Unity Transport](https://docs.unity3d.com/Packages/com.unity.transport@latest) + * [Unity Jobs](https://docs.unity3d.com/Packages/com.unity.jobs@latest) +* [Mirror Networking library](https://mirror-networking.com/) +## Installation +If you would like to use the code from this sample in your own project, please perform the following steps: +1. Install the latest version of Mirror. + * The latest version of Mirror can be obtained from either the [Unity Asset Store](https://assetstore.unity.com/packages/tools/network/mirror-129321) or the [Mirror repository on Github](https://github.com/vis2k/mirror). +2. Install the latest version of the `com.unity.jobs` package using the Unity Package Manager. +3. Install the latest version of the `com.unity.services.relay` package using the Unity Package Manager. +4. Copy the `Assets/UTPTransport` folder from this sample into the `Assets/` directory of your own project. + +## Community and Feedback +The Unity Relay Mirror Sample is an open-source project and we encourage and welcome +contributions. If you wish to contribute, be sure to review our +[contribution guidelines](CONTRIBUTING.md). \ No newline at end of file diff --git a/Third Party Notices.md b/Third Party Notices.md new file mode 100644 index 0000000..8882c69 --- /dev/null +++ b/Third Party Notices.md @@ -0,0 +1,162 @@ +This package contains third-party software components governed by the license(s) indicated below: +--------- + +Component Name: Mirror + +License Type: MIT + +Copyright (c) 2015, Unity Technologies + +Copyright (c) 2019, vis2k, Paul and Contributors + +https://github.com/vis2k/Mirror + +``` +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. +``` + +--------- + +Component Name: ParrelSync + +License Type: MIT + +Copyright (c) 2018 Greg M + +Copyright (c) 2020 Ian and Contributors + +https://github.com/VeriorPies/ParrelSync + +``` +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. +``` + +--------- + +Component Name: kcp2k + +License Type: MIT + +Copyright 2016 limpo1989 + +Copyright 2020 Paul Pacheco + +Copyright 2020 Lymdun + +Copyright 2020 vis2k + +https://github.com/vis2k/kcp2k + +``` +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. +``` +--------- + +Component Name: Mono.Cecil + +License Type: MIT + +Copyright 2008 - 2015 Jb Evain + +Copyright 2008 - 2011 Novell, Inc. + +https://github.com/jbevain/cecil + +``` +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. +``` + +--------- + +Component Name: Telepathy + +License Type: MIT + +Copyright (c) 2018, vis2k + +https://github.com/vis2k/Telepathy + +``` +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. +```